Skip to content

Log each MCP POST's JSON-RPC method and timings - #242

Merged
brianglass merged 1 commit into
mainfrom
mcp-request-logging
Oct 6, 2026
Merged

brianglass merged 1 commit into
mainfrom
mcp-request-logging

Conversation

@brianglass

Copy link
Copy Markdown
Owner

Why

After #241 deployed, Claude clients still stall on one POST /mcp per connection: 4 of the first 7 claude.ai requests on the new revision took exactly 21s, with the same 457-byte response as before. Switching from SSE to JSON should have changed the reply's size and didn't, so #241's unclosed-stream theory was wrong. None of ~30 hand-built probes reproduce it, and Cloud Run's request log doesn't show the JSON-RPC method or where the time goes.

What

  • mcp_svc/request_log.py wraps the MCP app and prints one JSON line per POST. Cloud Run turns these into a searchable jsonPayload. Each line has:
    • The message: JSON-RPC method, id, tool name, and the names of the params and _meta keys. Argument values are never logged, so search text doesn't end up in the logs.
    • Headers: Accept, Content-Type, Content-Length, Transfer-Encoding, MCP-Protocol-Version and User-Agent, plus whether Mcp-Session-Id, Last-Event-ID, Origin and Authorization are present (presence only, no values).
    • Trace id: the Cloud Run trace id, so each line can be joined to its request-log entry.
    • Timings: when the body finished arriving, when the response started and finished, and when the client disconnected.
    • Slow requests: a "stage": "slow" snapshot after 5s, because a request Cloud Run abandons at its timeout may never log "done".
  • orthocal/asgi.py: wraps the MCP app and corrects the json_response comment, which claimed Answer MCP POSTs with plain JSON instead of an SSE stream #241 fixed the stalls. json_response=True itself stays; it's harmless.

The log volume is about 3k lines a day at current traffic. Once the cause is found, the wrapper can come out or move to sampling.

After deploy

gcloud logging read 'jsonPayload.mcp_request_log=true AND jsonPayload.stage="slow"' --limit=50 --format=json

For each stall this will show:

  • No body_complete_ms: the client never finished sending the body.
  • body_complete_ms but no response_start_ms: the handler never answered.
  • response_complete_ms present: the reply was finished but the connection stayed open.

Test plan

  • New unit tests: method, tool and _meta keys are logged and argument values aren't; trace id is captured; the slow snapshot fires; batch and unparseable bodies are handled; exceptions are logged and re-raised; non-POST and lifespan requests pass through untouched
  • Local uvicorn server prints one line per POST with the expected fields
  • Full suite passes (224 tests, 1 skipped)

🤖 Generated with Claude Code

Claude clients still stall 21s on one POST /mcp per connection after
#241, with the same 457-byte response as before -- so the unclosed-SSE
theory behind #241 was wrong, and Cloud Run's request log can't say
what that POST is or where its time goes.

Wrap the MCP app to print one JSON line per POST: the JSON-RPC method,
id, tool name and parameter/_meta key names (never argument values),
the Accept/protocol-version headers, the Cloud Run trace id, and when
the body finished arriving, the response started, and it finished. A
request still running after 5s also gets a "slow" snapshot, since one
Cloud Run abandons at its timeout may never log "done".

Also correct asgi.py's json_response comment, which claimed #241 fixed
the stalls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brianglass
brianglass merged commit 8b7a7df into main Oct 6, 2026
4 checks passed
brianglass added a commit that referenced this pull request Oct 6, 2026
… 20s (#243)

#242's request log identified the 21s POSTs: Claude clients on protocol
2026-07-28 open a subscriptions/listen stream right after server/discover.
The SDK acknowledges it and then holds it open waiting for change
notifications orthocal never sends, until Cloud Run's 20s timeout cuts it;
the client then re-listens (listen:0, listen:1, ... ~25s apart), each
billed for the full 20s. It's the POST-era form of the GET stream #180
rejected.

MCPServer serves listen by default, and at 2026-07-28 the SDK advertises
listChanged/subscribe capabilities exactly when it does. Dropping the
handler makes server/discover advertise none, so clients have no reason
to listen, and a listen anyway gets an immediate "Method not found".
MCPServer has no option for this, so it goes through the low-level
server; test_server.py fails if an SDK upgrade changes that.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant