# 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.

Source: https://flowlines.ai/blog/identify-mcp-clients-and-users · Updated 2026-10-22 · By Flowlines
Index of all Flowlines pages for AI agents: https://flowlines.ai/llms.txt

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](https://flowlines.ai/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:

```json
{
  "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.

| Signal | Where it comes from | Verified? | Use it for |
|---|---|---|---|
| `clientInfo.name` and `version` | Request `_meta`, set by the client | No | Client mix, version adoption, client-specific bugs |
| OAuth `client_id` | Access token or introspection | Yes, by your authorization server | Which registered client application obtained the token |
| Issuer plus `sub` | Validated access token | Yes | The user or account behind the call |
| Granted scopes | Validated access token | Yes | What the caller may list and call |
| IP address | Network layer | No, and personal data | Abuse 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](https://flowlines.ai/questions/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:

```text
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](https://flowlines.ai/questions/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](https://flowlines.ai/mcp-server-analytics) and the production signals in the [MCP observability guide](https://flowlines.ai/blog/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

- [MCP 2026-07-28 changelog](https://modelcontextprotocol.io/specification/2026-07-28/changelog)
- [MCP 2026-07-28 base protocol: `_meta` and statelessness](https://modelcontextprotocol.io/specification/2026-07-28/basic/index)
- [MCP 2026-07-28 tools](https://modelcontextprotocol.io/specification/2026-07-28/server/tools)
- [MCP 2026-07-28 authorization](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization)
- [MCP security best practices](https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices)
- [MCP TypeScript SDK `AuthInfo`](https://ts.sdk.modelcontextprotocol.io/v2/api/@modelcontextprotocol/server-legacy/auth.html)
- [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html)
- [GDPR Recital 26](https://gdpr-info.eu/recitals/no-26/)
- [The 2026-07-28 MCP specification release](https://blog.modelcontextprotocol.io/posts/2026-07-28/)
## 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.
