The MCP 2026-07-28 specification went final today.
The maintainers call it the largest revision since launch.
Most coverage is focusing on the stateless core and the new Extensions framework.
Two changes in this release affect how you track agent costs — and both push responsibility into your application layer.
The session primitive is gone
Mcp-Session-Id and the initialize/initialized handshake are removed.
Every request is now self-describing.
Correct call for scalability: MCP servers can run behind a plain round-robin load balancer, no sticky sessions, no shared session store.
The side effect: there's no longer a session scope at the protocol level to group calls under.
Before today, teams anchoring budget tracking to the session ID had a natural grouping unit:
typescript
// Before 2026-07-28: session ID was a protocol primitive
const sessionId = request.headers['mcp-session-id'];
const budget = await store.get(budget:${sessionId});
That's gone.
The spec's own guidance is direct: "If your server needs to carry state across calls, mint an explicit handle from a tool and have the model pass it back as an argument."
Which means your budget scope is now something you define, not something the protocol gives you:
typescript
// After 2026-07-28: you own the scope
const agentRunId = context.runId; // your construct, not the protocol's
const budget = await store.get(budget:${agentRunId});
This is a cleaner model.
It's also opt-in work.
If you weren't tracking session-scoped cost before, you definitely don't get it automatically now.
Tasks shift cost accumulation async
The Tasks extension is now first-class under io.modelcontextprotocol/tasks.
A tools/call can return a task handle instead of a synchronous result.
The client drives the task forward with tasks/get, tasks/update, and tasks/cancel.
typescript
const result = await mcp.callTool({ name: 'deep-research', params });
if (result.type === 'task') {
// Pre-call budget check has already run and exited.
// LLM work is happening async from here.
let taskResult;
do {
taskResult = await mcp.tasks.get(result.taskId);
} while (taskResult.status !== 'complete');
}
The cost tracking problem: your pre-call guard ran before tools/call went out.
It saw the call, checked the session budget, let it through.
The server converted it to a Task.
The actual LLM work — potentially many internal calls, potentially expensive models — runs async while your polling loop waits.
The guard did its job.
It just had no visibility into what happened inside the Task.
Tasks are the right primitive for long-running agent work.
But they move cost accumulation to a point that call-level-only guards can't reach.
What to do about it
- Define session scope explicitly — don't assume the protocol gives it to you.
Use your agent run ID, a user session ID, or a handle the model threads between tool calls.
Make the budget scope an explicit construct in your application:
typescript
const session = {
id: crypto.randomUUID(),
budgetCents: 50,
spentCents: 0,
reservedCents: 0,
};
- Carry budget tracking through the Task polling lifecycle.
Check on each poll.
If the session is over budget, cancel the task — tasks/cancel is part of the spec:
typescript
while (taskResult.status !== 'complete') {
await updateSpend(session, taskResult.progressTokens);
if (session.spentCents + session.reservedCents >= session.budgetCents) {
await mcp.tasks.cancel(taskResult.taskId);
throw new BudgetExceededError(session.id);
}
taskResult = await mcp.tasks.get(taskResult.taskId);
}
- Reserve budget at task start, not just at task check.
Estimate the maximum cost of the task before you let the tools/call through. Reserve that amount.
If the reservation would exceed the session limit, block it before the task starts:
typescript
const estimatedMaxCost = estimateTaskCost(toolName, params);
if (session.spentCents + estimatedMaxCost > session.budgetCents) {
throw new BudgetExceededError(Task would exceed session budget);
}
session.reservedCents += estimatedMaxCost;
// Now let the tools/call through
Estimation will be imperfect.
A reasonable ceiling is still better than no guard against a task that runs to completion at full cost.
The broader pattern
Supabase's note in the spec blog is worth reading: they're using the new MRTR (Multi Round-Trip Requests) to ask the user for confirmation before executing a tool that has a real cost — like creating a new project or running a destructive query.
The tool pauses mid-execution and gets explicit sign-off.
That's the right instinct. The same confirmation pattern applies to budget: a tool that's about to start an expensive Task should check whether the session can absorb the cost before it commits, not trust that the pre-call check was sufficient.
This is the architectural pattern AI CostGuard is built around — session-scoped budget with a pre-call decision point, not just a per-call token counter.
The MCP spec today made that distinction explicit at the protocol level by removing the session primitive and making async Task execution first-class at the same time.
If you were relying on Mcp-Session-Id for budget scope, migrate now.
If you weren't tracking session-level cost at all, this release is a good moment to start.
https://github.com/salimassili62-afk/ai-costguard
Top comments (0)