diff --git a/main.go b/main.go index d5dca2c..810e9c2 100644 --- a/main.go +++ b/main.go @@ -90,10 +90,22 @@ func main() { 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 response, err := forwardToGitea(line) if err != nil { debugLog("Forward error: %v", err) + if isNotification { + // Notification — no response allowed, just swallow. + continue + } // Send error response errorResp := map[string]interface{}{ "jsonrpc": "2.0", @@ -110,11 +122,30 @@ func main() { 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) } } +// 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) { mcpURL := giteaURL + "/api/v2/mcp"