feat(mcp-server): log the initialize handshake - #4299
Merged
Conversation
The connector recently returned summary lines with no rows, because rows lived only in `structuredContent` and the client's handling of that field changed. Establishing that took a rollback test plus reading Claude Desktop's own log directory, because this server records nothing about who connects: `initialize` never read `request.params` and never logged. Every `initialize` now emits one line tagged `event: "mcp_initialize"`, joining `tool_call` as the second selectable event. It carries clientName / clientVersion, requestedProtocolVersion vs servedProtocolVersion, a protocolVersionMismatch boolean, clientCapabilities (names only), plus userId and correlationId so a session joins across both events. Logged from `dispatchMcpRequest` rather than the `case 'initialize'` that builds the response: `handleRpcRequest` is the pure, env-free half and has neither the caller nor the correlation id. Delegating to it afterwards keeps the response in one place. `describeInitializeParams` is total — `params` is `unknown` off the wire, so a malformed handshake still logs a line instead of throwing, which is the case most worth seeing. Caller-derived fields are spread beneath the canonical ones, so `clientInfo` cannot attribute a call to another userId. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gilgardosh
temporarily deployed
to
accounter-fullstack
August 27, 2026 05:57 — with
GitHub Actions
Inactive
gilgardosh
temporarily deployed
to
accounter-fullstack
August 27, 2026 05:57 — with
GitHub Actions
Inactive
Contributor
🚀 Snapshot Release (
|
| Package | Version | Info |
|---|---|---|
@accounter/client |
0.1.0-alpha-20260827074134-8e15b7dbde9c5f6555c22f5a0b64e97df041cd7a |
npm ↗︎ unpkg ↗︎ |
@accounter/green-invoice-graphql |
0.8.7-alpha-20260827074134-8e15b7dbde9c5f6555c22f5a0b64e97df041cd7a |
npm ↗︎ unpkg ↗︎ |
@accounter/hashavshevet-mesh |
0.2.13-alpha-20260827074134-8e15b7dbde9c5f6555c22f5a0b64e97df041cd7a |
npm ↗︎ unpkg ↗︎ |
@accounter/israeli-vat-scraper |
0.1.13-alpha-20260827074134-8e15b7dbde9c5f6555c22f5a0b64e97df041cd7a |
npm ↗︎ unpkg ↗︎ |
@accounter/modern-poalim-scraper |
0.11.0-alpha-20260827074134-8e15b7dbde9c5f6555c22f5a0b64e97df041cd7a |
npm ↗︎ unpkg ↗︎ |
@accounter/payper-mesh |
0.2.13-alpha-20260827074134-8e15b7dbde9c5f6555c22f5a0b64e97df041cd7a |
npm ↗︎ unpkg ↗︎ |
@accounter/scraper-app |
0.0.3-alpha-20260827074134-8e15b7dbde9c5f6555c22f5a0b64e97df041cd7a |
npm ↗︎ unpkg ↗︎ |
@accounter/server |
0.2.0-alpha-20260827074134-8e15b7dbde9c5f6555c22f5a0b64e97df041cd7a |
npm ↗︎ unpkg ↗︎ |
@accounter/shaam-uniform-format-generator |
0.2.7-alpha-20260827074134-8e15b7dbde9c5f6555c22f5a0b64e97df041cd7a |
npm ↗︎ unpkg ↗︎ |
@accounter/shaam6111-generator |
0.1.9-alpha-20260827074134-8e15b7dbde9c5f6555c22f5a0b64e97df041cd7a |
npm ↗︎ unpkg ↗︎ |
Contributor
There was a problem hiding this comment.
🟢 Approval recommended
The change is low-risk (additive logging + tests + docs) and does not alter protocol responses or tool execution behavior.
Pull request overview
Adds structured logging for the MCP initialize handshake so the server can attribute behavior changes to specific clients (name/version/capabilities/protocol) without relying on client-side logs—complementing existing per-tool-call logging.
Changes:
- Emit a single structured
event: "mcp_initialize"log line perinitializecall (including client info, protocol versions, capabilities, userId, correlationId). - Add parsing/clipping helper (
describeInitializeParams) plus unit tests and handshake logging tests. - Document the new log event in the package README + operations runbook, and publish via a changeset.
File summaries
| File | Description |
|---|---|
| packages/mcp-server/src/mcp/handler.ts | Introduces mcp_initialize event logging on initialize and adds total parsing + clipping helpers for client-supplied fields. |
| packages/mcp-server/src/mcp/tests/handler.test.ts | Adds unit tests for parameter parsing and verifies exactly-one handshake log line + invariants. |
| packages/mcp-server/README.md | Documents handshake logging semantics and how it relates to tool-call usage logging. |
| packages/mcp-server/docs/operations-runbook.md | Adds a dedicated section for handshake log fields and jq recipes for analysis/alerting. |
| .changeset/mcp-log-initialize-handshake.md | Bumps @accounter/mcp-server with a patch changeset describing the new logging behavior. |
Review details
Suppressed comments (1)
packages/mcp-server/src/mcp/tests/handler.test.ts:556
dispatchMcpRequestis exercised here with a primitive stringparams, but the real HTTP path rejects primitiveparamsinasJsonRpcRequest. Using an array keeps the “unparseable handshake still logs” intent while matching a shape that can actually arrive over JSON-RPC.
await initialize('not-an-object');
- Files reviewed: 5/5 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…ents Review follow-ups on the initialize handshake logging. - `clipClientLabel` appended the ellipsis *after* slicing to the cap, so a long client name emitted MAX_CLIENT_LABEL_LENGTH + 1 characters. The marker now counts towards the cap — a cap its own truncation marker can push past is not a cap. The test asserts against the exported constant rather than a literal, plus a boundary case for a name exactly at the cap. - The constant's docblock said it "mirrors" MAX_MISS_LABEL_LENGTH, which is 40 while this is 60. Reworded to say what it shares (motivation) and why the numbers differ: that one bounds a /metrics label, this bounds a log field. - A test comment claimed every params shape it exercises is reachable from the wire, but asJsonRpcRequest rejects primitive and null params before dispatch. Reworded to give the real reason the helper is total: `params` is typed `unknown`, and the parser's validation is the kind of upstream promise that quietly stops holding. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gilgardosh
temporarily deployed
to
accounter-fullstack
August 27, 2026 07:38 — with
GitHub Actions
Inactive
gilgardosh
temporarily deployed
to
accounter-fullstack
August 27, 2026 07:38 — with
GitHub Actions
Inactive
The repo runs 63:9 behavior/behaviour and 58:4 recognized/recognised; match it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gilgardosh
temporarily deployed
to
accounter-fullstack
August 27, 2026 07:39 — with
GitHub Actions
Inactive
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gilgardosh
temporarily deployed
to
accounter-fullstack
August 27, 2026 07:40 — with
GitHub Actions
Inactive
gilgardosh
temporarily deployed
to
accounter-fullstack
August 27, 2026 07:40 — with
GitHub Actions
Inactive
This was referenced Aug 27, 2026
gilgardosh
added a commit
that referenced
this pull request
Aug 27, 2026
* docs(mcp-server): postmortem for the blind-connector incident Records the Aug 18–26 outage where every tool returned its summary line without rows: what happened, why the connector broke, which properties of our own design turned a third-party client change into a total outage, and what changed in response (#4295 payload mirroring + contract test, #4299 handshake logging). The structural section is the point. The rows depended on a field a client may ignore; the tests asserted the object rather than the channel, so the suite passed 760/760 straight through the outage; and the handshake was the one hop with no log, so dating the change required the client's own local logs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(mcp-server): use the repo's American spellings in the postmortem Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(mcp-server): correct the tool count and record the merge Seventeen tools, not nineteen — the larger number counted shapeListResult call sites (16, since terminology.ts calls it twice and lookups.ts three times) rather than registered tools. Also updates the status row now that #4295 and #4299 have merged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(mcp-server): bring the postmortem up to date Written before the follow-up work landed. Updates it to record what investigating the incident actually produced: - Resolution now covers #4302 (money-shape drift the audit surfaced) and #4306 (documenting results in descriptions), alongside #4295 and #4299. - The outputSchema decision was a judgement call ("the fix no longer depends on it") and is now a measurement: Desktop does not surface a declared schema to the model, established on one canary tool after ruling out a cached tools/list. So the model sees neither structuredContent nor outputSchema, and `content` is the only channel on two independent measurements rather than inference. - Records that Desktop defers tool definitions, showing roughly the first sentence until the model loads one — which is why the scope-discovery instruction had to move into sentence one. - Lesson 3 was incomplete. Handshake logging cannot detect a client that stops handshaking: a revision without `initialize` produces silence, not a changed version, so we would still learn at failure time. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The connector went blind between Aug 18 and Aug 26 — every tool returned its summary line and no
rows. #4295 fixes the cause (rows lived only in
structuredContent, a field a client may ignore).This PR addresses the second problem that incident exposed: the server had no idea the client
had changed.
Establishing that it was the client took a rollback test — checking out
e31e8066, the committhat demonstrably worked on Aug 18 including a successful tag write, and running it today reproduces
the failure exactly. Same commit, same server, opposite outcome. Confirming it then meant reading
Claude Desktop's own log directory:
main.logMaking remote MCP tool call: accounter_get_charges→Remote tool call succeededclaude.ai-web.log[MCP] tool_approval_gate {"toolName":"Accounter:accounter_search_charges",…}None of that should have required someone's
~/Library/Logs.initialize(handler.ts) never readrequest.paramsand never logged, so there was no record of the client's name, its version, theprotocol revision it asked for, or the capabilities it declared. The connector already logs every
tool call and every auth failure — the handshake was the one hop with no trace.
What
Every
initializeemits one structured line taggedevent: "mcp_initialize", joiningtool_callas the second selectable event:
clientName,clientVersionrequestedProtocolVersion/servedProtocolVersionprotocolVersionMismatchclientCapabilitiesuserId,correlationIdtool_call, so a session joins across both eventsThree decisions worth reviewing
Logged from
dispatchMcpRequest, not from thecase 'initialize'that builds the response.handleRpcRequestis the pure, env-free half and takes only the request — it has neither the callernor the correlation id to log. Delegating to it afterwards keeps the response built in exactly one
place, so the two cannot drift. The sync
handleMcpBodypath has no production call sites and stayssilent, which conveniently leaves its existing test of the pure response shape untouched (there's a
test asserting that silence).
describeInitializeParamsis total.paramsisunknownoff the wire and validated only as anon-null object or array, so every field is narrowed there and anything unexpected degrades to
null/[]. A malformed handshake must still produce a line — a client sending something the servercannot parse is precisely the event worth seeing, and an exception would lose it. Covered with
paramsasnull, a string, an array, a number, and aclientInfothat is variously a string, anarray,
null, or has non-string fields.Caller-derived fields are spread beneath the canonical ones, matching the
tool_callline.Without it,
clientInfowould be an authenticated way to attribute a call to a differentuserId—there's a test for that specifically. Client strings are clipped before reaching the log.
Deliberately excluded
labeledTotalscounter keyed by client version./metricsis unauthenticated while callinga tool requires a token — already flagged in the runbook as worth closing — and client identity is
a fingerprint of the deployment. It belongs in the log rather than on a public endpoint. Worth
revisiting once
/metricsis gated.2025-06-18unconditionally; thisonly records what was asked. Changing what the server advertises is a live behavioural change to a
connector that has just broken once, and should be decided against a logged mismatch rather than a
guess — which is what
protocolVersionMismatchnow provides.Verification
initializeresponse is unchanged — asserted directly; this adds a log line, not a newresponse
jqrecipes (the cross-reference from README was renumbered to match)Note the first real run will answer something this investigation could not: which protocol revision
Claude Desktop
1.37937.1actually negotiates.🤖 Generated with Claude Code