Skip to content

Report resource reads by registered name, not URI, in analytics - #271

Merged
tnsardesai merged 2 commits into
mainfrom
hypeship/resource-read-analytics-categories
Nov 10, 2026
Merged

tnsardesai merged 2 commits into
mainfrom
hypeship/resource-read-analytics-categories

Conversation

@tnsardesai

@tnsardesai tnsardesai commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Summary

$mcp_resource_read events sent the requested resource URI to PostHog as $mcp_resource_name. Resource URIs are client-controlled, so whatever a client put in the path (ids, names, or a bearer-like value) was captured, including for reads that matched no registered resource and for reads that failed.

Resource reads now report a bounded category instead:

  • the registered resource name when the URI resolves to a registered resource or template (browser, browsers, profile, profiles, app, apps, browser_pool, browser_pools, kernel-managed-auth-login)
  • unregistered otherwise, including URIs that don't parse

The category is computed in the existing eventProperties hook from the server's live registrations (_registeredResources / _registeredResourceTemplates), matched the same way McpServer's resources/read handler matches them, so there is no hand-maintained list. beforeSend (sanitizeMcpAnalyticsEvent) swaps it into $mcp_resource_name for resource reads and drops the property if the category is missing. The allowlist is unchanged. Resource behavior and responses are unchanged.

Unregistered tool calls

The same applies to tools/call: the SDK builds the event from request.params.name before looking the tool up, so a call to a tool that doesn't exist sent the client's string as $mcp_tool_name and $mcp_resource_name. eventProperties now checks the name against the server's registered tools (_registeredTools, the field the SDK itself reads), and beforeSend drops the event when the tool isn't registered. In prod that is 7 events in the last 90 days, compared with about 1.3M real tool calls in the last 30 days alone, so no tool analytics are lost. Calls to registered tools are unchanged.

What @posthog/mcp 0.17.0 sends today

  • resources/read goes through captureResourceRequest (src/extensions/instrumentation.ts, line 540; dist/extensions/instrumentation.js, line 259), which sets resourceName: request.params?.uri for every read. The event is published from the same prepared event on success and when the handler throws, so unknown and failed reads carry the URI too.
  • addCommonEventProperties (src/extensions/posthog-events.ts, line 121) maps resourceName to $mcp_resource_name.
  • The SDK's own sanitization (sanitizeCapturedValue) only rewrites URL userinfo and sensitive query keys. Path segments pass through. On main, a read of https://<token>@files.example.test/<token>?key=<token> was captured as https://%5Bredacted%5D@files.example.test/<token>?key=%5Bredacted%5D, and kernel://orgs/.../secrets/<token> was captured verbatim.
  • Other properties: the URI also appears in $mcp_parameters and $mcp_error_message (for example Resource ... not found), but neither is on the allowlist. $mcp_error_type is the exception class name (Error, ProtocolError). $mcp_intent is not set for reads. $exception events are already dropped, and $identify (which would carry the URI through getRequestResourceName) is never sent because identify is null.
  • resources/list and resources/templates/list set no resource name. Their response goes to $mcp_response, which is not allowlisted. prompts/get and prompts/list are not instrumented by this SDK version.

Tests

New tests in src/lib/mcp/analytics.test.ts go through the real instrumentation and registrations (registerMcpCapabilities with MCP Apps enabled) over an in-memory client, using synthetic ids and tokens:

  • reads of a browser, the browser collection, a profile, an app, and the managed-auth app resource report the registered name, is_error: false, and a duration. No URI, project id, or identifier appears in the events.
  • a not-found browser reports browser. A kernel:// URI and an https:// URI containing a synthetic token report unregistered. All three report is_error: true, and neither the token nor the URI appears anywhere in the captured events.
  • calls to an unregistered tool (a synthetic token, and constructor to cover inherited object keys) produce no $mcp_tool_call event, and the token appears nowhere in the captured events. This test fails against the previous commit.
  • control: a get_connection_context tool call still reports the tool name in $mcp_tool_name / $mcp_resource_name, plus is_error and a duration.

Both resource tests fail against main's analytics.ts, which reports the raw URIs. The control test passes on both.

Ran:

  • bun test: 1143 pass, 0 fail
  • bunx tsc --noEmit: clean
  • bunx prettier --check on the changed files: clean. Repo-wide format:check flags only AGENTS.md, which this PR doesn't touch.
  • bun run lint can't run on this repo (next lint was removed in Next.js 16, as noted in AGENTS.md).

Notes

  • The registry lookup reads McpServer private fields, the same way the PostHog SDK reads _registeredTools. If a future @modelcontextprotocol/server renames them, reads report unregistered, which still sends no URI, and the registered-name test fails.
  • browser_pools / browser_pool aren't read in the tests because that registration ignores injected dependencies and would call the real API. They use the same template path as the others.

Note

Medium Risk
Changes privacy-sensitive MCP analytics (what is sent to PostHog) and uses McpServer private registry fields; behavior is test-covered but could mislabel resources if SDK internals change.

Overview
MCP PostHog analytics no longer emit client-controlled URIs or arbitrary tool names on resource reads and invalid tool calls.

For $mcp_resource_read, the instrumentation hook resolves each resources/read URI against the server's live resource registry (same matching logic as the MCP handler) and records a bounded $mcp_resource_name—registered names like browser / profile / kernel-managed-auth-login, or unregistered when nothing matches. sanitizeMcpAnalyticsEvent maps that category into the public property and strips internal fields via the existing allow-list, so path segments, tokens, and project ids from the request URI do not reach PostHog (including on failed reads).

For tools/call, calls to tools that are not in _registeredTools are flagged and the entire $mcp_tool_call event is dropped in beforeSend, so client-supplied names (e.g. secrets or constructor) are not captured. Registered tool calls behave as before.

Integration tests exercise real instrumentMcpAnalytics + registerMcpCapabilities paths and assert registered names, error flags, and absence of sensitive strings in captured events.

Reviewed by Cursor Bugbot for commit fa2811e. Bugbot is set up for automated code reviews on this repo. Configure here.

@vercel

vercel Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
mcp Ready Ready Preview Nov 10, 2026 5:04am UTC

@Sayan- Sayan- 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.

Checked the resources/read path end to end: the category lookup follows McpServer's own resources/read matching, and the success, not-found, unparseable-URI and hook-throw paths all leave the URI out of captured events.

  • p2 (pre-existing): $mcp_tool_call still sends a client-chosen string as $mcp_resource_name and $mcp_tool_name when the requested tool does not exist. A tools/call with name sk_secret_TOKEN123 on this branch captured sk_secret_TOKEN123 in both properties. The SDK builds the event from request.params.name before the tool lookup (@posthog/mcp dist/extensions/instrumentation.js ~136-145), and both properties are in SENT_PROPERTIES (src/lib/mcp/analytics.ts:160).

@tnsardesai
tnsardesai requested a review from Sayan- November 10, 2026 05:03
@tnsardesai
tnsardesai merged commit 112c7be into main Nov 10, 2026
10 checks passed
@tnsardesai
tnsardesai deleted the hypeship/resource-read-analytics-categories branch November 10, 2026 05:27

This branch was successfully deployed

1 active deployment
Preview — fa2811e2 Deployed Nov 10, 2026 by vercel[bot]
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