Fix silent MCP-server initialize-crash + contact-link regression + source=mcp telemetry#54
Merged
Merged
Conversation
…lemetry
Three fixes bundled on one branch since they all came up in the same
debugging session.
1) Silent `initialize`-handshake crash. resolveClientFromEnv() and
the LEADBAY_TOKEN-missing handler both `process.exit(1)`'d before
server.connect() ran, killing the MCP stdio process mid-JSON-RPC
handshake. Claude Desktop's log showed a bare "Server disconnected"
with no stderr captured. Replaced both exits with a BrokenLeadbayClient
sentinel whose every request method rejects with a pre-baked
LeadbayError (AUTH_MISSING / AUTH_EXPIRED). The server now answers
initialize and tools/list; the auth failure surfaces on first
tool call as a clean envelope. Added an early
uncaughtException/unhandledRejection net so future startup crashes
leave a real stack on stderr + Sentry. New `mcp.startup` PostHog
event keyed on `auth_state ∈ {ok,missing,expired,probe_failed}`
lets us bucket disconnect reports without reading user logs.
runDoctor() keeps its interactive hard-exit — CLI subcommands
should still process.exit(1) when the user is watching a terminal.
2) Contact-name link regression. After 0.9.0, the agent started
rendering plain-text contact names on rows whose linkedin_page
was null. Root cause: the linking/contact-linkedin snippet's
"When … link … Otherwise fall back …" framing read as a two-option
choice, and the agent picked plain text. Rewrote the snippet with
a leading MANDATORY directive: every contact name MUST be wrapped
in markdown link syntax; the search URL is always constructable
from name + company. Tightened the pull-leads-table Column 3 spec
and replaced followups-map's inline duplicate of the priority
logic with `{{include:linking/contact-linkedin}}`. New deterministic
audit (packages/mcp/test/audit/contact-link-invariant.test.ts)
asserts the MANDATORY language exists in the snippet AND lands in
every contact-rendering tool description (pull_leads, pull_followups,
followups_map, research_lead_by_id, research_lead_by_name_fuzzy,
prepare_outreach). Any future softening, removed include, or new
contact-rendering tool added without the rule trips the audit.
3) PostHog/Sentry source tagging. baseProps() now stamps every
captured PostHog event with `source: "mcp"`; Sentry initialScope
gets the same `source=mcp` tag so any captured exception is tagged
at the surface level. New regression test exercises every event
type (tool call, quota hit, topup link, startup) in one session
and asserts the tag lands on all of them. Enables splitting MCP
usage from web/other surfaces in PostHog dashboards without
per-call work.
491 tests passing workspace-wide; pnpm -r typecheck clean.
Co-Authored-By: Claude Opus 4.7 (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.
Summary
initialize-handshake crash.resolveClientFromEnv()and the missing-LEADBAY_TOKENhandler bothprocess.exit(1)'d beforeserver.connect()ran, killing the MCP stdio process mid-JSON-RPC handshake (host showed bare "Server disconnected" with no stderr captured). Replaced both exits with aBrokenLeadbayClientsentinel whose every request method rejects with a pre-bakedLeadbayError(AUTH_MISSING/AUTH_EXPIRED); the server now answersinitialize+tools/listand the auth failure surfaces on first tool call. Added an earlyuncaughtException/unhandledRejectionsafety net so future startup crashes leave a real stack on stderr + Sentry, plus a newmcp.startupPostHog event keyed onauth_state ∈ {ok,missing,expired,probe_failed}for bucketing disconnect reports.runDoctor()keeps its interactive hard-exit.linkedin_pagewasnullbecauselinking/contact-linkedin.md's "When … link … Otherwise fall back …" framing read as a two-option choice. Rewrote the snippet with a leading MANDATORY directive (every contact name MUST be wrapped in markdown link syntax; the search URL is always constructable from name + company), tightenedpull-leads-tableColumn 3, and replacedfollowups-map's inline priority duplicate with{{include:linking/contact-linkedin}}. New deterministic auditpackages/mcp/test/audit/contact-link-invariant.test.tsasserts the mandate exists in the snippet AND lands in every contact-rendering tool description — any softening, removed include, or new contact-rendering tool added without the rule trips the audit.source: "mcp"telemetry tagging.baseProps()now stamps every captured PostHog event withsource: "mcp"; SentryinitialScope.tagscarries the same tag so exceptions are bucketed at the surface level. New regression test exercises tool-call / quota-hit / topup-link / startup events in one session and asserts the tag lands on all four.Test plan
pnpm -r typecheckclean across all 5 packagespnpm -r test— 491 tests passing (260 core + 12 leadclaw + 203 mcp + 16 promptforge)contact-link-invariant) and new telemetry regression (every captured event type carries source=mcp) greeninitialize-crash fix passes — broken-client returned on missing-token and AUTH_EXPIRED paths instead ofprocess.exit(1)leadbay_account_statusreturns a clear AUTH envelope (vs the previous "Server disconnected")source=mcpandmcp.startupevents show up with the fourauth_statebuckets🤖 Generated with Claude Code