Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4eb7f78b6b | |||
| c6ed87cdcf |
38
main.go
38
main.go
@@ -90,10 +90,22 @@ func main() {
|
|||||||
|
|
||||||
debugLog("Received: %s", string(line))
|
debugLog("Received: %s", string(line))
|
||||||
|
|
||||||
|
// JSON-RPC notifications (no "id" field) must NEVER receive a response.
|
||||||
|
// The MCP spec defines notifications/initialized, notifications/cancelled,
|
||||||
|
// notifications/progress, etc. — these are fire-and-forget. Even if the
|
||||||
|
// remote gitcaddy server returns an error for an unknown notification, we
|
||||||
|
// must suppress it on the way back to the client (Claude Code), otherwise
|
||||||
|
// the client sees an unexpected response and fails the handshake.
|
||||||
|
isNotification := isJsonRpcNotification(line)
|
||||||
|
|
||||||
// Forward to Gitea's MCP endpoint
|
// Forward to Gitea's MCP endpoint
|
||||||
response, err := forwardToGitea(line)
|
response, err := forwardToGitea(line)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
debugLog("Forward error: %v", err)
|
debugLog("Forward error: %v", err)
|
||||||
|
if isNotification {
|
||||||
|
// Notification — no response allowed, just swallow.
|
||||||
|
continue
|
||||||
|
}
|
||||||
// Send error response
|
// Send error response
|
||||||
errorResp := map[string]interface{}{
|
errorResp := map[string]interface{}{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
@@ -110,11 +122,30 @@ func main() {
|
|||||||
|
|
||||||
debugLog("Response: %s", string(response))
|
debugLog("Response: %s", string(response))
|
||||||
|
|
||||||
// Write response to stdout with Content-Length framing
|
if isNotification {
|
||||||
|
// Notification — never respond, even if the remote sent something.
|
||||||
|
debugLog("Suppressing response to notification")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write response to stdout as newline-delimited JSON
|
||||||
writeFramed(response)
|
writeFramed(response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isJsonRpcNotification reports whether a raw JSON-RPC message is a notification
|
||||||
|
// (a request without an "id" field). Per JSON-RPC 2.0, servers MUST NOT reply to
|
||||||
|
// notifications.
|
||||||
|
func isJsonRpcNotification(raw []byte) bool {
|
||||||
|
var probe map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(raw, &probe); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, hasId := probe["id"]
|
||||||
|
_, hasMethod := probe["method"]
|
||||||
|
return hasMethod && !hasId
|
||||||
|
}
|
||||||
|
|
||||||
func forwardToGitea(request []byte) ([]byte, error) {
|
func forwardToGitea(request []byte) ([]byte, error) {
|
||||||
mcpURL := giteaURL + "/api/v2/mcp"
|
mcpURL := giteaURL + "/api/v2/mcp"
|
||||||
|
|
||||||
@@ -215,10 +246,7 @@ func writeResponse(resp interface{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func writeFramed(data []byte) {
|
func writeFramed(data []byte) {
|
||||||
// MCP stdio transport uses newline-delimited JSON (NDJSON), not LSP-style
|
fmt.Fprintf(os.Stdout, "Content-Length: %d\r\n\r\n%s", len(data), data)
|
||||||
// Content-Length framing. Claude Code and other MCP clients reject
|
|
||||||
// Content-Length headers on stdio, so we emit one JSON object per line.
|
|
||||||
fmt.Fprintf(os.Stdout, "%s\n", data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func debugLog(format string, args ...interface{}) {
|
func debugLog(format string, args ...interface{}) {
|
||||||
|
|||||||
Reference in New Issue
Block a user