Field note progress0%
← All field notes

MCP analytics / 8 min

Which AI Clients Call Your MCP Server, and How to Identify Users Safely

See which AI clients call your MCP server from per-request clientInfo, and attribute calls to users with verified token claims and pseudonymous IDs.

On this page

To see which AI client calls your MCP server, read the self-reported clientInfo that the 2026-07-28 protocol sends with every request. To attribute calls to users, use the identity from the validated OAuth access token, store it as a pseudonymous ID, and never treat the client name as proof of anything.

This guide covers where client and user identity come from in MCP, what each signal can and cannot prove, and how to answer the questions developers ask most often: which clients call my server, how do I read the user inside a tool handler, and how do I filter tools by JWT. For the analytics you can build on top, see MCP user analytics.

Which AI clients are calling my MCP server?

Read the io.modelcontextprotocol/clientInfo field in each request's _meta. Under the 2026-07-28 revision, MCP is stateless: the initialize handshake is gone, and every request carries its protocol version and client capabilities in _meta. The specification says clients SHOULD also include clientInfo on every request "unless specifically configured not to do so".

A tools/call request over Streamable HTTP looks like this:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_weather",
    "arguments": { "location": "Seattle, WA" },
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientInfo": { "name": "ExampleClient", "version": "1.0.0" },
      "io.modelcontextprotocol/clientCapabilities": {}
    }
  }
}

clientInfo uses the Implementation type, where name and version are required. Protocol version and client capabilities are required on every request; clientInfo is optional. Servers that still serve legacy clients (2025-11-25 and earlier) receive clientInfo once, in the initialize request, and must associate it with that stdio process or HTTP session.

Because there is no handshake, do not cache client identity per connection. The specification is explicit that servers MUST NOT rely on prior requests over the same connection to establish client identity, and that an open connection is not a conversation or session. Read clientInfo on each request and record it with that request.

The exact name strings differ between Claude, ChatGPT, Cursor, VS Code and other clients, and can change between releases. Build a small mapping from observed raw values to client families rather than hard-coding guesses, and keep the raw value for debugging.

Is the client name proof of which client called?

No. The specification says clientInfo and serverInfo "are self-reported by the sender and are not verified by the protocol", are meant for display, logging and debugging, and SHOULD NOT be relied on for security decisions. Any script can send the name of a popular client.

SignalWhere it comes fromVerified?Use it for
clientInfo.name and versionRequest _meta, set by the clientNoClient mix, version adoption, client-specific bugs
OAuth client_idAccess token or introspectionYes, by your authorization serverWhich registered client application obtained the token
Issuer plus subValidated access tokenYesThe user or account behind the call
Granted scopesValidated access tokenYesWhat the caller may list and call
IP addressNetwork layerNo, and personal dataAbuse controls, not user identity

The OAuth client_id is stronger than clientInfo, with caveats. With Client ID Metadata Documents, which the 2026-07-28 revision prefers, the client_id is an HTTPS URL that the authorization server fetches, so it names a real client application. The MCP security guidance still warns that a local process can claim a legitimate metadata URL with a localhost redirect. With Dynamic Client Registration, now deprecated, each installation can get a random client_id that does not map to a product name at all.

How do I read the user's identity inside a tool handler?

Take it from the validated access token, not from the request body. On an HTTP server, your authorization middleware validates the bearer token, including that it was issued for your server as the audience, as the authorization specification requires. The SDK then passes the token information to request handlers. In the TypeScript SDK this is an AuthInfo object with clientId, scopes, expiresAt, resource, extra and the raw token. Check your SDK version for the exact property path.

Three rules keep this safe:

  1. Derive identity from the verified token only. The MCP security guidance recommends keying server-side state by a user ID "derived from the verified token rather than supplied by the client". Never read a user ID from tool arguments or _meta.
  2. Use issuer plus subject as the stable key. OpenID Connect defines sub as unique within an issuer, so iss and sub together identify a user. If your tokens are opaque, get the same values from token introspection.
  3. Never log the auth object whole. AuthInfo contains the raw access token. Copy the fields you need and drop the rest.

For stdio servers, the specification says not to use the OAuth flow and to read credentials from the environment instead. The user is whoever owns the API key your server was started with, so attribute calls to the account behind that key on your backend.

If token validation itself is failing, see how to debug MCP OAuth failures.

Can I filter which tools a user sees based on their JWT?

Yes. The 2026-07-28 tools specification says the tools/list result MUST NOT vary per connection, but MAY vary "by the authorization presented on the request", for example by returning only the tools the caller's granted scopes permit. Credentials are per-request input, so this fits the stateless model.

  • Map each tool to the scopes or roles it needs, and filter tools/list against the validated token's claims.
  • Check the same rule again in tools/call. A client can call a tool name it never saw in the list.
  • Results from tools/list now carry ttlMs and cacheScope. When the list depends on the caller, return cacheScope: "private" so shared intermediaries do not serve one caller's list to another.
  • Keep the order deterministic for each caller, as the specification recommends, so clients can cache the list.

How do I recognise the same user across Claude, ChatGPT and Cursor?

Only with a stable identifier you are permitted to use. If a user connects from Claude on Monday and from Cursor on Tuesday, and both clients send tokens from your authorization server, the same iss and sub arrive in both cases. That is the only reliable link.

Do not try to join users across clients with IP addresses, clientInfo, timing, or values found in tool arguments. These guesses produce false matches, and they create personal data you did not need to collect. If a server has no authentication, report clients and calls, not users.

For B2B servers, also record a pseudonymous account or tenant ID from the token. Many product questions are about accounts, not individuals: which customers adopted the server, and which ones stopped using it.

How do I store user identifiers without leaking them?

Replace the raw identifier with a keyed hash before it leaves the server:

user_pid    = HMAC-SHA256(server_secret, iss + "|" + sub)
account_pid = HMAC-SHA256(server_secret, tenant_id)

Keep server_secret in your secret manager, not in the analytics backend. A plain unsalted hash of an email address can be reversed by hashing a list of known addresses, so use a keyed hash. Rotating the key resets continuity, so decide that deliberately.

Pseudonymous IDs reduce exposure but do not remove privacy duties. GDPR Recital 26 treats pseudonymised data that can be attributed to a person with additional information as personal data. Document the purpose, set retention, and support deletion by recomputing the ID from the user's issuer and subject. To monitor usage without storing arguments or results, see MCP monitoring without payloads.

What should I do with unknown or missing clients?

Expect them. clientInfo is optional, older clients may not send it, and scripts and agent frameworks send whatever their authors chose.

  • Record a literal unknown value when clientInfo is missing, and report it as its own bucket. Its share is a useful number.
  • Normalise known raw names to a short list of client families, and put everything else in other with the raw string kept for debugging. This caps cardinality in metrics.
  • Do not block or change behaviour based on the client name. The specification says implementations SHOULD NOT use clientInfo to change behaviour. Use authentication and scopes for access control.
  • Treat a sudden rise in unknown traffic as a question to investigate, not as an attack by default.

Flowlines shows which AI clients call your server and which users come back, from the telemetry your server already emits. The same fields also power MCP server analytics and the production signals in the MCP observability guide.

Checklist

  • [ ] Read io.modelcontextprotocol/clientInfo from _meta on every request, and from initialize for legacy clients.
  • [ ] Record client name and version as analytics context, never as authentication.
  • [ ] Validate tokens, including audience, before reading any identity.
  • [ ] Derive users from issuer plus sub in the verified token, never from arguments or _meta.
  • [ ] Store a keyed hash of user and account IDs, with the key outside analytics.
  • [ ] Filter tools/list by scopes if needed, enforce the same rule in tools/call, and return cacheScope: "private" for per-caller lists.
  • [ ] Report unknown and other clients as explicit buckets.
  • [ ] Never log the raw token or the full auth object.

Sources

Frequently asked questions

How do I see which AI client is calling my MCP server?

Under the 2026-07-28 protocol, read io.modelcontextprotocol/clientInfo from the metadata of each request; it carries the client's name and version. Servers on earlier revisions receive clientInfo once in the initialize request instead.

Can I trust the client name an MCP client sends?

No. The specification says clientInfo is self-reported and not verified, and that servers should not use it for security decisions. Use it for analytics and debugging only.

How do I identify the user behind an MCP tool call?

On a remote server with OAuth, derive the user from the validated access token: the issuer plus the subject claim, or the equivalent from token introspection. Never take a user ID from tool arguments or from request metadata.

Can I filter MCP tools based on the caller's JWT?

Yes. The 2026-07-28 specification allows the tools/list result to vary by the authorization presented on the request, for example by granted scopes. Enforce the same rule in tools/call, because hiding a tool is not authorization.

How do I recognise the same user in Claude and in ChatGPT?

Only when both clients authenticate the user against the same authorization server, so the same issuer and subject arrive with each call. Do not match users across clients by IP address, client metadata or argument contents.

Should I store raw user IDs in analytics?

Usually not. Store a keyed hash of issuer and subject instead, keep the key outside the analytics system, and remember that pseudonymous data is still personal data under GDPR.

100 MCP sessions a month, free

See what people do with your MCP.