Skip to content

feat(analytics): AUTO-265 emit MCP usage events to Amplitude via @amplitude/mcp-analytics - #206

Open
xsvfat wants to merge 9 commits into
mainfrom
devin/1785183776-amplitude-mcp-analytics
Open

feat(analytics): AUTO-265 emit MCP usage events to Amplitude via @amplitude/mcp-analytics#206
xsvfat wants to merge 9 commits into
mainfrom
devin/1785183776-amplitude-mcp-analytics

Conversation

@xsvfat

@xsvfat xsvfat commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds Amplitude's official MCP analytics SDK alongside (not replacing) the existing SQS pipeline, so MCP usage lands in Amplitude with the standard [MCP] … event set (Session Initialized/Ended, Tools Listed, Tool Call Response/Rejected) instead of only our custom MCP Tool Request events.
  • Off by default: the client is only constructed when AMPLITUDE_API_KEY is set. ANALYTICS_ENABLED (SQS) is untouched.

The interesting part is that the SDK is written for the raw @modelcontextprotocol/sdk McpServer and we run FastMCP, which gives us neither of the two hook points the SDK needs:

1. instrumentServer() must run before connect(), and FastMCP owns session creation. FastMCP builds a FastMCPSession per client connection and connects it internally; session.server (the low-level SDK Server) is only reachable from server.on('connect') — i.e. after the transport is up, at which point binding silently no-ops and every tool wrapper stays a passthrough. So we hook the class instead of the instance:

Server.prototype.connect = function (transport) {
  if (this[CONNECT_GUARD]) return this[ORIGINAL_CONNECT](transport);   // re-entry ↓
  this[CONNECT_GUARD] = true;
  this[ORIGINAL_CONNECT] = originalConnect.bind(this);
  analytics.instrumentServer(this);   // installs its own instance-level connect wrapper
  return this.connect(transport);     // → SDK wrapper → back here, now guarded
};

The guard exists because instrumentServer captures instance.connect.bind(instance) as "the original" — which resolves to our prototype wrapper. Without it the second entry recurses forever.

This requires the SDK class we patch to be the same class fastmcp instantiates, so @modelcontextprotocol/sdk becomes a direct dependency pinned to fastmcp's own range (^1.24.3, deduped to a single 1.30.0 copy). A connect-time instanceof check warns once if a future dedupe break silently unbinds instrumentation.

2. Tool wrapping. Rather than threading the client through registerSurface and all ten tool modules, addTool is wrapped once so every registered tool is instrumented at the FastMCP boundary and defineTool/tool modules are untouched:

server.addTool = (tool) =>
  originalAddTool({ ...tool, execute: analytics.instrumentTool(tool.execute, { name: tool.name }) });

Inside the handler, identity and rationale use the SDK's context-scoped free functions, gated on getCurrentContext() (they throw outside an instrumented frame, i.e. when Amplitude is disabled). user_id is the verified accountId for OAuth/Supabase sessions — now threaded onto the session by resolveBrowserlessAuth — and token-<djb2 hash> otherwise; the raw Browserless token is never sent. The model-reported _prompt (already redactSecrets-ed) becomes [MCP] Rationale.

Note instrumentTool receives FastMCP's context object rather than the SDK's RequestHandlerExtra; the SDK degrades gracefully on it (it reads sessionId, which FastMCP provides), so nothing is faked.

Because a SIGTERM/SIGINT listener suppresses Node's default termination, the shutdown handler awaits the flush and then exits explicitly (143/130) — otherwise docker stop would wait out the full grace period on every deploy.

Related issues

n/a

Changes

  • src/lib/amplitude-analytics.ts (new): client construction, the Server.prototype.connect pre-connect hook, the addTool wrapper, identity resolution, shutdown flush.
  • src/index.ts: init + instrumentation + signal handling + the instanceof Server sanity warning.
  • src/lib/define-tool.ts: setIdentity / setRationale inside the tool frame (18 added lines, no restructuring).
  • src/lib/{http-auth,account-resolver}.ts: thread the verified accountId onto BrowserlessSession.
  • New deps: @amplitude/mcp-analytics@^0.3.0 (0.4.0 skipped — 3 days old), its peer @amplitude/analytics-node, and the direct @modelcontextprotocol/sdk pin described above.
  • README.md / llms-install.md: document AMPLITUDE_API_KEY.

A follow-up PR on browserless/browserless-workspace-template wires AMPLITUDE_API_KEY into the local stack (empty by default, so the local MCP stays silent).

Test plan

  • npm test passes locally (549 passing, incl. new specs: disabled without a key, account-id vs hashed-token identity, connect hook binds without recursing, addTool wrapper enabled/disabled)
  • npm run lint passes locally
  • npm run coverage thresholds still met
  • Manual smoke test against a real Browserless token (needs an Amplitude project key)
  • N/A — docs / chore / refactor only

Checklist

  • I have read CONTRIBUTING.md
  • My commits follow the conventional-commit prefix convention
  • I have updated documentation where relevant (README, CHANGELOG, etc.)
  • I have not introduced new dependencies without flagging them in the summary

Link to Devin session: https://app.devin.ai/sessions/7b9256f2b329451cb84eb1bfcd962ca1
Requested by: @xsvfat


Open in Devin Review

Summary by CodeRabbit

  • New Features

    • Added optional Amplitude analytics for MCP tool usage, enabled via AMPLITUDE_API_KEY.
    • Improved analytics identity to prefer authenticated session accountId, with a deterministic fallback when absent.
    • Added FastMCP tool execution instrumentation when analytics is enabled.
    • Added graceful Amplitude shutdown on SIGTERM/SIGINT (once per process).
  • Documentation

    • Updated self-hosted environment variable and local installation guidance to include AMPLITUDE_API_KEY.
  • Tests

    • Added automated test coverage for analytics enablement, identity selection, server/tool instrumentation, and disabled-analytics behavior.

devin-ai-integration Bot and others added 4 commits July 27, 2026 20:27
Co-Authored-By: Sean Yang <sean@browserless.io>
Co-Authored-By: Sean Yang <sean@browserless.io>
Co-Authored-By: Sean Yang <sean@browserless.io>
Co-Authored-By: Sean Yang <sean@browserless.io>
@xsvfat xsvfat self-assigned this Jul 27, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor
Original prompt from Sean Yang

can you setup amplitude mcp analytics for our mcp?

https://github.com/amplitude/Amplitude-MCP-Analytics-Node

@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

Co-Authored-By: Sean Yang <sean@browserless.io>

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 2 additional findings.

Open in Devin Review

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 29 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4ef1185b-9293-4de0-a3e3-31c2336387a1

📥 Commits

Reviewing files that changed from the base of the PR and between cd75e20 and 86206d0.

📒 Files selected for processing (3)
  • src/lib/amplitude-analytics.ts
  • src/lib/define-tool.ts
  • test/lib/amplitude-analytics.spec.ts

Walkthrough

The PR adds optional Amplitude MCP analytics configuration, propagates authenticated account IDs, instruments MCP servers and tools, sets tool identity and rationale context, handles shutdown signals, and adds analytics tests and documentation.

Changes

Amplitude MCP analytics

Layer / File(s) Summary
Analytics configuration and dependency wiring
package.json, src/@types/types.d.ts, src/config.ts, README.md, llms-install.md
Adds Amplitude packages, configuration typing and environment-variable loading, plus setup documentation.
Authenticated account identity propagation
src/@types/types.d.ts, src/lib/account-resolver.ts, src/lib/http-auth.ts
Carries accountId from account resolution into authentication and session data.
Analytics hooks and tool instrumentation
src/lib/amplitude-analytics.ts
Adds conditional initialization, guarded server and tool instrumentation, identity derivation, shutdown handling, and test reset support.
Server lifecycle and tool context integration
src/index.ts, src/lib/define-tool.ts
Wires analytics into server creation and termination, and sets identity and rationale during tool execution.
Analytics behavior tests
test/lib/amplitude-analytics.spec.ts
Tests initialization, identity selection, instrumentation, tool wrapping, and shutdown behavior.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MCPClient
  participant FastMCP
  participant Server
  participant AmplitudeMCPAnalytics
  MCPClient->>FastMCP: connect
  FastMCP->>Server: create MCP session
  Server->>AmplitudeMCPAnalytics: instrument on first connect
  FastMCP->>AmplitudeMCPAnalytics: wrap tool execution
  AmplitudeMCPAnalytics-->>MCPClient: return instrumented tool result
Loading

Possibly related PRs

Suggested reviewers: gikoskos

Poem

I’m a rabbit with metrics to send,
Through each MCP tool, I hop and attend.
Accounts leave clues,
Prompts bring their views,
And shutdowns safely mark the end.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main change: emitting MCP usage events to Amplitude via the new analytics SDK.
Description check ✅ Passed The description matches the template with summary, related issues, changes, test plan, and checklist sections filled in.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch devin/1785183776-amplitude-mcp-analytics

Comment @coderabbitai help to get the list of available commands.

@devin-ai-integration devin-ai-integration Bot changed the title feat(analytics): emit MCP usage events to Amplitude via @amplitude/mcp-analytics AUTO-265 feat(analytics): emit MCP usage events to Amplitude via @amplitude/mcp-analytics Jul 27, 2026
@devin-ai-integration devin-ai-integration Bot changed the title AUTO-265 feat(analytics): emit MCP usage events to Amplitude via @amplitude/mcp-analytics feat(analytics): AUTO-265 emit MCP usage events to Amplitude via @amplitude/mcp-analytics Jul 27, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@llms-install.md`:
- Around line 48-49: Remove AMPLITUDE_API_KEY from the main copy-paste
configuration in llms-install.md, leaving only the required settings and
BROWSERLESS_TOKEN. Add a separate opt-in note stating that users should add
AMPLITUDE_API_KEY only when they have a real project API key.

In `@src/lib/amplitude-analytics.ts`:
- Around line 106-109: Update getAmplitudeIdentity so the token fallback uses a
collision-resistant cryptographic digest or keyed HMAC instead of the 32-bit
djb2 hash, while preserving session.accountId as the preferred identity and the
existing token-prefixed format.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 75e8b883-05ff-4c71-bd92-8d855cb2d1eb

📥 Commits

Reviewing files that changed from the base of the PR and between 73af19e and 7cb9ac2.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (11)
  • README.md
  • llms-install.md
  • package.json
  • src/@types/types.d.ts
  • src/config.ts
  • src/index.ts
  • src/lib/account-resolver.ts
  • src/lib/amplitude-analytics.ts
  • src/lib/define-tool.ts
  • src/lib/http-auth.ts
  • test/lib/amplitude-analytics.spec.ts

Comment thread llms-install.md Outdated
Comment thread src/lib/amplitude-analytics.ts Outdated
Co-Authored-By: Sean Yang <sean@browserless.io>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
test/lib/amplitude-analytics.spec.ts (2)

2-2: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert a fixed identity value instead of duplicating the implementation.

The test recomputes the same hash expression as production, so an algorithm or encoding regression can be changed in both places without detection. Use a known token fixture with a hard-coded expected token-* value.

Also applies to: 40-41

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/lib/amplitude-analytics.spec.ts` at line 2, Update the tests using
createHash to stop recomputing the production hash; replace that logic with a
known token fixture and a hard-coded expected token-* identity value. Remove the
now-unused createHash import while preserving assertions for the generated
identity.

75-119: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add shutdown coverage.

The PR adds shutdown flushing, but the suite does not verify that shutdownAmplitudeAnalytics awaits successful shutdown or swallows shutdown failures. Add both cases to protect the process-termination path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/lib/amplitude-analytics.spec.ts` around lines 75 - 119, Extend the
amplitude analytics tests around the existing instrumentation coverage to test
shutdownAmplitudeAnalytics: verify it awaits a successful analytics shutdown,
and separately verify it resolves without propagating an error when shutdown
fails. Use the existing mock analytics setup and assert the shutdown method is
invoked in both cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@test/lib/amplitude-analytics.spec.ts`:
- Line 2: Update the tests using createHash to stop recomputing the production
hash; replace that logic with a known token fixture and a hard-coded expected
token-* identity value. Remove the now-unused createHash import while preserving
assertions for the generated identity.
- Around line 75-119: Extend the amplitude analytics tests around the existing
instrumentation coverage to test shutdownAmplitudeAnalytics: verify it awaits a
successful analytics shutdown, and separately verify it resolves without
propagating an error when shutdown fails. Use the existing mock analytics setup
and assert the shutdown method is invoked in both cases.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a5cd06d6-1565-43e4-ae32-5fdc8a1ef2c6

📥 Commits

Reviewing files that changed from the base of the PR and between 7cb9ac2 and 25e5f94.

📒 Files selected for processing (3)
  • llms-install.md
  • src/lib/amplitude-analytics.ts
  • test/lib/amplitude-analytics.spec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • llms-install.md

Co-Authored-By: Sean Yang <sean@browserless.io>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/lib/amplitude-analytics.spec.ts`:
- Around line 119-143: Update the “awaits successful Amplitude shutdown” and
“swallows rejected Amplitude shutdown” tests to use deferred promises instead of
immediately settled Sinon stubs. Assert that shutdownAmplitudeAnalytics remains
pending before resolving or rejecting the deferred promise, then settle it and
verify the wrapper completes without throwing and shutdown is called once.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6caee05f-b142-4e2d-9dda-8c2e51928816

📥 Commits

Reviewing files that changed from the base of the PR and between 25e5f94 and cd75e20.

📒 Files selected for processing (1)
  • test/lib/amplitude-analytics.spec.ts

Comment thread test/lib/amplitude-analytics.spec.ts
Co-Authored-By: Sean Yang <sean@browserless.io>

@ashwinsingh2007 ashwinsingh2007 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few comments from claude code -

  1. shutdown() awaits the Amplitude flush with no timeout before process.exit(). A hung flush means the process never exits — worse than the old default termination and the opposite of the stated "exit fast on docker stop" goal. Race the flush against a short timeout, then exit regardless.

  2. getCurrentContext() is called on every tool invocation, including when Amplitude is disabled (the default). The guard assumes it returns falsy outside an instrumented frame; if the SDK throws there, this line is outside the try/catch and every tool call breaks with analytics off. Confirm the SDK contract + add a disabled-mode test that actually runs a tool's execute.

  3. accountId is threaded via hybridAuthenticate, which only runs for transport === 'httpStream'. The ticket cites "OAuth/Supabase sessions" — when auth: oauthProvider is active, does authenticate still run and set session.accountId? If not, OAuth sessions silently fall back to token- and the "verified account id" goal isn't met.

Rest looks fine

@ashwinsingh2007 ashwinsingh2007 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to check the comment

Co-Authored-By: Sean Yang <sean@browserless.io>
@devin-ai-integration

Copy link
Copy Markdown
Contributor

@ashwinsingh2007 thanks — 1 and 2 were real, fixed in 86206d0. 3 checks out as-is.

1. Unbounded flush. Correct, a hung flush would have hung the process. shutdownAmplitudeAnalytics now races analytics.shutdown() against a 2s AMPLITUDE_SHUTDOWN_TIMEOUT_MS; the timer is unref()'d so it can't hold the loop open and is cleared in finally, and we exit regardless of which side wins. Covered by a test that stubs shutdown() with a never-settling promise and asserts the wrapper still resolves.

2. getCurrentContext() outside the try/catch. You're right that the SDK isn't safe to call blind — in @amplitude/mcp-analytics@0.3.0, setIdentity() explicitly throws outside a context (dist/context/als.js:48-50), and getCurrentContext() is a bare AsyncLocalStorage.getStore(). The whole block moved into setAmplitudeToolContext, which returns early on our own activeAnalytics state — so with Amplitude off we never touch an SDK context API at all — and everything else, getCurrentContext() included, is now inside one try/catch. Added a test that registers a real tool with analytics disabled and invokes its execute end-to-end.

3. OAuth sessions and accountId. These do reach the account-id path. FastMCP prefers options.authenticate over options.auth.authenticate when both are set (fastmcp/dist/chunk-3YTRRGCE.cjs:1280-1285) and invokes it per HTTP Stream request (:1633-1645), and we set both (src/index.ts:128-133), so OAuth requests still run through hybridAuthenticateresolveBrowserlessAuthresolveApiKey, which returns the verified accountId. getAmplitudeIdentity prefers it, so no silent fallback to token-<hash>. The hash fallback is only for plain-token sessions, which is intended.

553 tests passing, CI green.

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