Skip to content

Fix silent MCP-server initialize-crash + contact-link regression + source=mcp telemetry#54

Merged
milstan merged 1 commit into
mainfrom
milstan/contact-linkedin-link-fallback
May 20, 2026
Merged

Fix silent MCP-server initialize-crash + contact-link regression + source=mcp telemetry#54
milstan merged 1 commit into
mainfrom
milstan/contact-linkedin-link-fallback

Conversation

@milstan
Copy link
Copy Markdown
Contributor

@milstan milstan commented May 20, 2026

Summary

  • Silent initialize-handshake crash. resolveClientFromEnv() and the missing-LEADBAY_TOKEN handler both process.exit(1)'d before server.connect() ran, killing the MCP stdio process mid-JSON-RPC handshake (host showed 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 + tools/list and the auth failure surfaces on first tool call. Added an early uncaughtException / unhandledRejection safety net so future startup crashes leave a real stack on stderr + Sentry, plus a new mcp.startup PostHog event keyed on auth_state ∈ {ok,missing,expired,probe_failed} for bucketing disconnect reports. runDoctor() keeps its interactive hard-exit.
  • Contact-name link regression (post-0.9.0). The agent started rendering plain-text contact names on rows whose linkedin_page was null because linking/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), tightened pull-leads-table Column 3, and replaced followups-map's inline priority duplicate with {{include:linking/contact-linkedin}}. New deterministic audit packages/mcp/test/audit/contact-link-invariant.test.ts asserts 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 with source: "mcp"; Sentry initialScope.tags carries 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 typecheck clean across all 5 packages
  • pnpm -r test — 491 tests passing (260 core + 12 leadclaw + 203 mcp + 16 promptforge)
  • New audit (contact-link-invariant) and new telemetry regression (every captured event type carries source=mcp) green
  • Server-test regression for the silent-initialize-crash fix passes — broken-client returned on missing-token and AUTH_EXPIRED paths instead of process.exit(1)
  • Smoke: with a bad token + no region pinned, start the MCP server and confirm Claude Desktop reports it as connected, lists tools, and calling leadbay_account_status returns a clear AUTH envelope (vs the previous "Server disconnected")
  • PostHog dashboard: confirm new events carry source=mcp and mcp.startup events show up with the four auth_state buckets

🤖 Generated with Claude Code

…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>
@milstan milstan merged commit 3916783 into main May 20, 2026
1 check passed
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