# MCP Went Stateless: Measuring Sessions, Users and Latency After the 2026-07-28 Spec

> The 2026-07-28 MCP spec removed initialize and protocol sessions. How to identify AI clients, group sessions, count users and time retried calls.

Source: https://flowlines.ai/blog/mcp-analytics-stateless-spec · Updated 2026-10-22 · By Flowlines
Index of all Flowlines pages for AI agents: https://flowlines.ai/llms.txt

After the 2026-07-28 MCP specification, there is no `initialize` handshake and no protocol session: each request carries its own protocol version, client capabilities and, ideally, client info in `_meta`, so analytics must identify clients per request, group sessions with its own rule, and link retried requests into one logical call. **Flowlines groups stateless MCP traffic into sessions, shows the users and AI clients behind every tool call, and labels sessions it infers.**

The revision made MCP servers easier to scale behind load balancers. It also removed the two things many dashboards leaned on: the `clientInfo` sent once at startup and the `Mcp-Session-Id` header. This guide covers what replaces them for measuring clients, sessions, users and latency. Facts are checked against the [2026-07-28 changelog](https://modelcontextprotocol.io/specification/2026-07-28/changelog) and specification pages.

## What changed for analytics in the 2026-07-28 MCP spec?

Client identity moved from a one-time handshake to every request, protocol sessions disappeared, and one logical call can now span several JSON-RPC requests; Flowlines handles all three, but your own instrumentation needs to know about them too.

| Topic | Before (2025-11-25) | After (2026-07-28) | Analytics impact |
|---|---|---|---|
| Handshake | `initialize` with `clientInfo` and capabilities | Removed; `server/discover` returns server details | No single moment that says "a client connected" |
| Client info | Once per connection | `_meta["io.modelcontextprotocol/clientInfo"]` on each request, optional | Record the client on every span |
| Protocol version | Negotiated once | `_meta["io.modelcontextprotocol/protocolVersion"]` on each request | Track the revision mix per request |
| Sessions | `Mcp-Session-Id` header on Streamable HTTP | Removed; servers ignore the header and never mint one | Sessions are an analytics construct you define |
| Server-to-client requests | Elicitation and sampling sent by the server | A result with `resultType: "input_required"`; the client retries with a new request ID | One logical call, several requests |
| Resent requests | Resumable SSE streams | Resumability removed; a broken stream is resent as a new request with a new ID | Retries look like new calls unless you link them |
| HTTP headers | `MCP-Protocol-Version` | Adds required `Mcp-Method` and `Mcp-Name` | Gateways can count calls per tool without parsing bodies |

Other changes, such as `ping` and `logging/setLevel` being removed and tasks moving to an extension, matter less for measurement. Error codes changed too; the [MCP error codes guide](https://flowlines.ai/blog/mcp-error-codes) lists them.

## How do I identify the AI client without the initialize handshake?

Read the client info from `_meta` on each request and record its name and version on every tool span; Flowlines shows tool calls by AI client with an explicit unknown bucket for requests that do not say.

A stateless `tools/call` request carries its context with it:

```json
{
  "jsonrpc": "2.0",
  "id": 7,
  "method": "tools/call",
  "params": {
    "name": "search_customers",
    "arguments": { "query": "acme" },
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities": {},
      "io.modelcontextprotocol/clientInfo": { "name": "example-client", "version": "1.4.0" }
    }
  }
}
```

The protocol version and client capabilities are required. Client info is optional, and the specification says clients should send it on every request, so some requests will arrive without it.

- **Record name and version as low-cardinality attributes,** normalized to a known list of client families.
- **Keep "unknown" visible.** A rising unknown share usually means a new client or an integration that stopped sending the field.
- **Do not cache across requests.** Two requests from one user can come from different clients.
- **Never use it for authorization.** Client info is self-reported.

[MCP user analytics](https://flowlines.ai/mcp-user-analytics#calls-by-client) shows how to track usage by AI client and recognize the same user across clients.

## How do I group MCP requests into sessions now?

Group requests with a rule you define, usually a permitted user identity plus an inactivity gap, and label the result as inferred; Flowlines groups stateless traffic this way and uses supplied session context when your integration provides it.

| Grouping signal | Strength | Watch out for |
|---|---|---|
| Authenticated user plus an inactivity gap, such as 30 minutes | Works for most HTTP servers with OAuth | Splits a long job with a pause; merges two jobs close together |
| Trace context propagated by the client | Links calls inside one agent turn when the client sends it | Not every client propagates it; one turn is not one job |
| An application identifier your product controls, such as a project or conversation ID | The most faithful grouping | Only where you have permission and a real identifier |
| `Mcp-Session-Id` from older clients | Available for clients on earlier revisions | Identifies a connection, not a person or a job; absent on 2026-07-28 |

The 2026-07-28 revision also defines conventions for carrying `traceparent`, `tracestate` and `baggage` in `_meta`, so trace context can cross from client to server. Use it when it is present, and do not depend on it.

Whatever rule you choose, write it down and apply it to every client. [MCP session analytics](https://flowlines.ai/mcp-session-analytics) explains how Flowlines separates supplied, observed and inferred sessions, and the [glossary entry for MCP session](https://flowlines.ai/glossary#mcp-session) defines the term.

## How do I count MCP users without a session ID?

Count users from the identity your authentication establishes, exactly as before: a protocol session never identified a person, it identified a connection. Flowlines counts distinct and returning users from a permitted, stable user identifier.

| Identity source | Use it for | Limits |
|---|---|---|
| OAuth token subject on a remote server | Distinct and returning users | Shared service accounts are not people |
| Your own account or workspace ID | Account-level adoption | Several people can share one account |
| Local stdio server with no authentication | Installation-level counts at most | No user identity unless the user opts in |

Retention and activation depend on this identity, not on sessions, so the [MCP server KPIs](https://flowlines.ai/blog/mcp-server-kpis) keep working after the revision. What changes is the session count, because it now depends on your grouping rule. Do not compare session counts across the migration without saying so.

## How do I time tool calls that span several requests?

Record every request as its own span, then link the rounds of one logical call and report two durations: server time per request and end-to-end time per call. Flowlines shows p50, p95 and p99 latency per tool next to the users and clients affected.

A logical call spans several requests in two cases:

1. **Multi round-trip requests.** The server answers with `resultType: "input_required"`, for example to ask the user for a confirmation, and the client sends the request again with the answers. The specification requires a new JSON-RPC ID for each round.
2. **Resent requests.** With SSE resumability removed, a client whose stream breaks resends the request with a new ID.

| Duration | Measured from | Measured to | Use it for |
|---|---|---|---|
| Server time per request | Request received | Response sent | Tool performance and p95 objectives |
| End-to-end time per logical call | First request of the call | The round that returns `resultType: "complete"` | What the user experienced |
| Waiting for input | An `input_required` result | The next round arriving | Exclude from tool latency; it is human time |

Do not link rounds by JSON-RPC ID: request IDs only have to be unique among requests still in flight, and every round uses a new one. If your server keeps state between rounds, key it with an identifier you generate and record that identifier on each span. Otherwise, link by user, tool and a short time window, and treat the result as approximate.

Timeouts also need care. The specification asks implementations to set timeouts but gives no default, and on HTTP, closing the response stream counts as cancellation. Record cancellations as their own outcome, not as errors and not as successes. [MCP tool analytics](https://flowlines.ai/mcp-tool-analytics#tool-latency) covers per-tool latency, and the [MCP monitoring guide](https://flowlines.ai/blog/mcp-observability-guide) covers histograms and alert rules.

## How do I avoid counting a resent call twice?

Count attempts and logical calls separately: every request is an attempt, and attempts that a client resent for the same work form one logical call. The specification has no idempotency key, only the `idempotentHint` annotation, so deduplication is your job.

- **Success rate** should use logical calls, so a call that failed once and succeeded on resend counts as one success.
- **Retry rate** is attempts divided by logical calls, reported per tool and per client.
- **Side effects** matter most. A tool that creates records can run twice when a client resends after a broken stream. Make write tools idempotent on your side, and set `idempotentHint` honestly.

Repeated calls with identical arguments also show up when a tool's result does not tell the agent what to do next; [why agents ignore your MCP tools](https://flowlines.ai/blog/unused-mcp-tools) covers that pattern.

## Which new HTTP headers help MCP observability?

`Mcp-Method` and `Mcp-Name` let a gateway or load balancer count requests per method and per tool without parsing the JSON body, and the specification names observability tooling as one of their purposes. A mismatch between the headers and the body is rejected with HTTP 400 and error -32020.

| Header | Carries | Example use |
|---|---|---|
| `MCP-Protocol-Version` | The revision of the request | Revision mix per client at the edge |
| `Mcp-Method` | The JSON-RPC method | Split tools/call from list and discovery traffic |
| `Mcp-Name` | The tool, prompt or resource name | Calls and errors per tool at the gateway |

Edge counts are useful for traffic and errors. They do not know the user, the job or the outcome, so read them with server-side spans.

## What should I do if my clients use different spec revisions?

Support both paths and record the protocol version on every request, so you can see the revision mix and compare each client with itself. Older clients still send `initialize` with client info once per connection, and may send `Mcp-Session-Id`; newer clients send everything per request.

Use this checklist when you migrate a server's analytics:

- Read client info from `_meta` per request, and fall back to the `initialize` values for older clients.
- Record `protocolVersion` on every span and chart the share of each revision.
- Replace any metric keyed on `Mcp-Session-Id` with your own session rule, labeled as inferred.
- Keep user counts on authenticated identity, unchanged.
- Add a round number and result type to tool spans, and link rounds with your own identifier.
- Report attempts, logical calls and cancellations separately.
- Annotate the migration date on every chart whose definition changed.

**Key takeaway:** the stateless revision moved client info to every request and left sessions to you. Identify clients per request, keep users on authenticated identity, define and label your session rule, and link retried requests before you measure latency or success. Flowlines does this grouping for stateless MCP traffic and shows the users, clients and sessions behind every tool. See [MCP observability](https://flowlines.ai/mcp-observability) for the product walkthrough; the [Free plan](https://flowlines.ai/pricing) includes 100 analyzed MCP sessions per month.

## Sources

- [MCP 2026-07-28 changelog](https://modelcontextprotocol.io/specification/2026-07-28/changelog)
- [MCP 2026-07-28 base protocol](https://modelcontextprotocol.io/specification/2026-07-28/basic)
- [MCP 2026-07-28 Streamable HTTP transport](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http)
- [MCP 2026-07-28 cancellation](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation)
- [MCP 2026-07-28 release post](https://blog.modelcontextprotocol.io/posts/2026-07-28/)
## Frequently asked questions

### What changed for MCP analytics in the 2026-07-28 specification?

The initialize handshake and protocol sessions are gone. Each request now carries its protocol version and client capabilities in its metadata, and should carry client info. Servers ignore Mcp-Session-Id. Multi round-trip requests and resent requests use new JSON-RPC IDs, so one logical tool call can span several requests.

### How do I know which AI client called my MCP server without initialize?

Read the client info in each request's metadata, under the io.modelcontextprotocol/clientInfo key, and record the name and version on every tool span. The field is optional and self-reported, so keep an explicit unknown bucket and never treat it as authenticated identity.

### How do I group MCP requests into sessions after the stateless spec?

Group by a permitted user identity from your authentication plus an inactivity gap, or by trace context or an application-level identifier when you have one. Label the result as inferred. Flowlines groups stateless traffic into sessions and uses supplied session context when it is available.

### How do I count MCP users without a session ID?

The same way as before: from the identity your authentication establishes, such as the OAuth token's subject or your own account ID. A protocol session never identified a person; it identified a connection.

### How do I measure latency for tool calls that span several requests?

Record every request as its own span with its result type, then link the rounds of one logical call with an identifier your server controls. Report server time per request and end-to-end time per logical call separately, and exclude time spent waiting for the user.

### Do I need to change my MCP analytics if some clients are still on older revisions?

Support both. Older clients still send initialize with clientInfo once per connection and may send Mcp-Session-Id. Record the protocol version on every request so you can see the revision mix and compare like with like.
