Skip to content

fix: startup logging messages never reached clients - #139

Open
mattpodwysocki wants to merge 2 commits into
mainfrom
fix/log-messages-before-connect
Open

fix: startup logging messages never reached clients#139
mattpodwysocki wants to merge 2 commits into
mainfrom
fix/log-messages-before-connect

Conversation

@mattpodwysocki

@mattpodwysocki mattpodwysocki commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Two compounding issues meant startup logging (.env load status, tracing status, detected client capabilities) never reached any client:

  1. The McpServer never declared the logging capability in its constructor options, so sendLoggingMessage was an unconditional no-op (the SDK checks this._capabilities.logging before doing anything) regardless of timing.
  2. Independently, several sendLoggingMessage calls ran before await server.connect(transport) — a notification sent before the transport is connected has no client to reach, so those calls would have been dropped even with the capability declared correctly.

Fixed both:

  • Added logging: {} to the server's declared capabilities.
  • Moved all startup sendLoggingMessage calls to run after connect(), matching the ordering @mapbox/mcp-server's src/index.ts already uses (with the same "now that we're connected, send all the logging messages" pattern).

Follow-up commit, same root cause: getClientVersion()/getClientCapabilities() are only populated once the client's initialize request has been processed — reading them right after connect() (as the existing capability-gated tool registration did) reliably saw them as unset, confirmed live even for a client that explicitly declared elicitation support. Both reads now happen inside a server.server.oninitialized callback, which only fires once the handshake is fully done. As a result:

  • The connected client's identity (name/version from its initialize request) is now logged, e.g. Client identified as: claude-ai v1.0.0 — useful for support/debugging across Claude Desktop, Cursor, VS Code, etc.
  • It's also recorded as mcp.client.name/mcp.client.version on every subsequent tool-execution trace span, so OTel-backed traces can be filtered/grouped by client.
  • The capability-gated elicitation-tool registration path is fixed too (currently dormant since ELICITATION_TOOLS is empty, but was silently broken for whenever a tool is added there).

Matches the equivalent changes landing in @mapbox/mcp-server.

Test plan

  • New integration test (test/integration/loggingOrder.test.ts) spawns the real built server and asserts a real MCP client receives at least one startup log message. Verified it fails against each fix in isolation (capability alone still drops messages sent pre-connect; ordering alone still no-ops without the capability) and passes with both applied.
  • New unit tests (test/utils/tracing.test.ts) cover createToolSpan picking up mcp.client.name/mcp.client.version once setClientInfo is called.
  • Live-verified against the real built server with a real MCP client declaring a custom name/version and elicitation capability: server now logs Client identified as: pretend-codex-client v4.2.0 and Client capabilities detected: { "elicitation": { "form": {} } } (previously undefined).
  • npx vitest run — all 641 tests pass
  • npm run build succeeds
  • CHANGELOG.md updated

🤖 Generated with Claude Code

Two compounding issues: the McpServer never declared the `logging`
capability, so sendLoggingMessage was an unconditional no-op regardless of
timing; and several of those calls also ran before server.connect(),
which would drop them anyway since there's no connected client yet.

Fixed both -- declared logging: {} in capabilities, and moved startup
logging (.env status, tracing status, detected client capabilities) to
after connect(), matching the ordering @mapbox/mcp-server already uses.
Added an integration test that spawns the real built server and asserts
a real client receives at least one startup log message.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@mattpodwysocki
mattpodwysocki requested a review from a team as a code owner August 25, 2026 16:16
server.server.getClientVersion()/getClientCapabilities() are only
populated once the client's initialize request has been processed --
reading them synchronously right after server.connect() races that
request, since connect() only waits for the transport to start, not for
the handshake to finish. Confirmed live: reading capabilities immediately
after connect() reliably returned undefined even for a client that
declared them.

Moved both reads into the existing server.server.oninitialized callback
(added in the prior commit for logging-order correctness), which fires
only once the handshake is fully done. Also adds client identification as
a byproduct: the client's name/version from its initialize request is now
logged on connect and recorded as mcp.client.name/mcp.client.version on
every subsequent tool-execution trace span, matching the equivalent change
in @mapbox/mcp-server.

Co-Authored-By: Claude Sonnet 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.

2 participants