Live Webinar
The Festive Growth Sessions: How Top Brands Drive More Leads· 23 Sept, 3:30 PM ISTSave Your Seat
Wati

Can Two AI Agents Use the Same MCP Server at Once?

Krithika M
6 mins read
Fact-checked by: Namitha Sudhakar
|According to: Editorial Policies
Banner showing can wo AI agents share one MCP server?
CategoriesAI Agent

Too Long? Read This First

  • MCP allows concurrent clients by design. The protocol has no session limit and doesn't serialize requests.
  • The 2026-07-28 MCP spec removed protocol-level sessions and the Mcp-Session-Id header, so any request can go to any server instance.
  • Removing sessions doesn't make your app stateless. A multi-step workflow still needs an explicit handle, like a job_id, stored in a shared database instead of process memory.
  • A session ID was never a lock. Two agents can hold different sessions and still overwrite the same record.
  • Optimistic concurrency, a conditional update that fails if the record changed since it was read, is what the C# SDK's task docs recommend for this.

Two independent MCP clients can call the same server at the same time. That covers two separate agents or a single agent running two sessions, and the protocol doesn't serialize those calls or assume a single client.

What MCP doesn't do is protect a shared record for you.

If two agents update the same row, file, or contact at once, nothing stops one write from silently overwriting the other.

This is the exact scenario you'll run into with a Wati MCP server handling live conversations and contacts.

In this guide, we cover what changed in the July 2026 MCP spec, the race condition it leaves open, and five patterns that keep concurrent agents from overwriting each other's work.

MCP Sessions vs. Stateless Servers: What Changed?

MCP's approach to sessions has changed, but the underlying need for application state hasn't. Understanding that distinction is important when you're building a server that needs to handle multiple clients and keep track of work across calls.

Earlier MCP Revisions Used Protocol Sessions

Earlier MCP revisions were session-based. A client sentinitialize, the server returned its negotiated capabilities, and the client then sent initialized. With Streamable HTTP, the server could also return an Mcp-Session-Id that the client had to include on every later request.

Two clients performing that handshake independently received different session IDs, allowing a session-aware server to maintain separate per-client state, such as browser contexts or carts.

The 2026-07-28 Specification Removed Protocol Sessions

The 2026-07-28 specification removed protocol-level sessions and the mandatory initialize/initialized handshake.

Version and capability metadata now travel with each request, so requests can be routed to any server instance without sticky sessions or a shared protocol-session store.

This simplifies horizontal scaling, but it doesn't eliminate application state. If a workflow spans several calls, the server still needs to track that state explicitly rather than relying on an implicit session object.

The C# SDK documents both modes: stateless mode treats each request independently, while stateful mode maintains an in-memory session per client when session-scoped state, unsolicited server-to-client messages, or subscriptions are needed. The current SDK defaults to stateless HTTP, with stateful mode available when required.

MCP Race Conditions: Why Shared Records Conflict?

Here's the simplest way to see the problem. Agent A reads a record at version 10. Agent B reads that same record before A's update, so it also sees version 10. A writes its change first. B then writes its own update using the old version 10, quietly overwriting A's change. Neither agent knows a conflict occurred.

Separate sessions don't prevent this when both agents are working with the same database row, file, or API object. Statelessness isn't what creates the race, either.

The real issue is that MCP doesn't manage concurrency for shared application data. Your server needs its own safeguards to decide what happens when two agents try to change the same thing at once.

Here's the full rewritten section, ready to paste in:

5 Patterns for Safe Concurrent MCP Operations

Five patterns protect a shared record from concurrent writes. Each one fits a different kind of MCP server.

Pattern

How it works

Best for

Optimistic concurrency

Conditional update: UPDATE records SET value=:v, version=version+1 WHERE id=:id AND version=:expected. Zero rows updated means a conflict. Return a structured error telling the model to reread and retry.

Most record updates where conflicts are occasional

Database transactions and row locks

Wrap the full read-modify-write in one transaction. Keep lock duration short and define a deadlock-retry policy.

Tightly coupled operations within a single database

Per-record distributed locks

Acquire a lock keyed to the resource, such arecord:{id}, with leases, fencing tokens, and expiry. An in-process mutex alone fails once the server has multiple replicas.

Multi-step operations spanning several calls or external systems

Queue or single-writer serialization

Enqueue commands keyed by record or tenant so one worker processes each key at a time. Return a job_id for long-running work.

Hot, inherently sequential resources

Idempotency keys

An application-level argument, distinct from the JSON-RPC request id, persisted with a uniqueness constraint like (tenant_id, idempotency_key). A retry with the same key returns the original result instead of repeating the mutation.

Any mutating call where a retry could otherwise duplicate a side effect

MCP's JSON-RPC request ID correlates a response to a request. It is not a business idempotency key.

Reusing it for deduplication doesn't work, because it's scoped to the transport, not your application's mutation history.

Build a separate idempotency argument, bind it to the operation and its parameters, and persist it at the actual side-effect boundary.

How to Build a Safe Concurrent MCP Server?

When building a new MCP server, start with these assumptions rather than discovering them under load:

  • Expect concurrent requests: Multiple clients and tool calls can arrive at the same time.
  • Keep security in the request: Authentication, tenant identity, and authorization belong in each request's security context, not in a session object.
  • Give workflows an explicit handle: If a workflow spans multiple calls, use an explicit, authorized handle rather than relying on an implicit session.
  • Make mutations safe to retry: Use an idempotency key whenever repeating an operation could cause a duplicate side effect.
  • Use the simplest concurrency control that works: Start with database transactions and optimistic version checks for ordinary updates. Add distributed locks or keyed queues only when operations genuinely can't run concurrently.
  • Return actionable statuses: Use clear conflict, duplicate, or busy statuses instead of silently overwriting data, so the calling agent knows how to respond.
  • Don't rely on transport backpressure: Treat HTTP/2 backpressure as capacity protection, not a concurrency or correctness mechanism. SSE connections and background task modes may not have a built-in concurrency limit, so rate limiting and queueing still need to be handled at the application level.

Managing Concurrent AI Agents in a WhatsApp MCP Server

Two agents sharing a WhatsApp MCP server is not hypothetical. A support agent handling live conversations and a scheduled campaign agent updating contact segments can easily target the same contact record within seconds of each other.

If both write without a version check, whichever writes last silently wins, and the other agent's update simply disappears with no error to explain why.

The fix follows the patterns above directly. A contact update should be a conditional write keyed to the contact's current version.

A send that could plausibly be retried, whether by the agent itself after a timeout or by a human re-running a failed step, needs an idempotency key so a retry doesn't duplicate a message to a real customer.

An unprotected race condition doesn't just corrupt data. It can also let one agent's action silently undo another's, one of the trust issues covered in the MCP security guidance for WhatsApp AI agents.

If you're running Astra AI Agents by Wati, the same explicit-handle approach applies across agent runs.

Bonus Read: How AI agents track context across sessions on WhatsApp

Keep Shared MCP Data Safe at Scale

MCP allows multiple agents to use the same server at the same time, but it doesn't manage conflicts between them. The safest approach is to handle concurrency at the application level with version checks, transactions, locks, queues, and idempotency keys where they make sense.

For WhatsApp use cases, this means protecting shared contact updates and preventing duplicate message sends when requests are retried.

Want to see how this works in practice? Book a Wati demo

Frequently asked questions

Does removing MCP's protocol sessions make servers less safe for concurrent use?

Not by itself. Sessions were never a locking mechanism. Removing them just makes explicit what was already true: shared-state safety has to be built at the application level.

Can I just use sticky routing so the same agent always hits the same server instance?

That helps with routing consistency, but it doesn't protect a record two different agents both touch. Sticky routing solves "does my session land on a consistent process," not "did two writers just race on the same row."

Is a job_id or basket_id the same thing as a lock?

No. A handle lets a server find the right piece of state across calls. It does nothing to stop two callers from using that same handle at the same time. You still need a version check, a transaction, or a keyed lock on top of it.

Do I need idempotency keys on every mutating tool?

Not every one. Any tool where a retried call could duplicate a real-world side effect, like a send, a charge, or a ticket creation, needs one. Read-only tools and mutations that are naturally safe to repeat, like setting a field to a fixed value, don't strictly require it.

Related posts