fix(mcp): suppress responses to JSON-RPC notifications
Add JSON-RPC notification detection and response suppression per MCP spec. Notifications (requests without "id" field) must never receive responses, even if the upstream server returns an error. Prevents handshake failures when Claude Code sends notifications/initialized and receives unexpected error responses. Changes: - Add isJsonRpcNotification() to detect requests without "id" field - Skip error responses for notifications - Suppress upstream responses to notifications - Add detailed comments explaining JSON-RPC 2.0 notification semantics
This commit is contained in:
33
main.go
33
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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user