MCP Protocol 2026: One Interface for Every AI Agent
Quick Answer: The Model Context Protocol in 2026 is a JSON-RPC 2.0 interface that exposes tools, resources, and prompts from any server to any AI client, replacing per-vendor function schemas. My production MCP servers hold a P95 of 38ms per tool call over streamable HTTP and run unchanged in Claude, Cursor, and a Laravel agent.
Author: Deepak Bagada — founder of SaaS Next, creator of Curro, AI agent developer in Junagadh, Gujarat. I maintain three internal MCP servers and have migrated nine client integrations from custom REST wrappers. The MCP Agent Builder blueprint is the scaffold I start from.
Key Takeaways
- One server, many clients. A single MCP server written once serves Claude Desktop, Cursor, and my own Laravel agent without three separate integration layers.
- JSON-RPC is boring on purpose. Typed tool inputs, structured errors, and schema discovery remove the prompt-engineering tax of guessing function signatures.
- Streamable HTTP beats stdio for hosted tools. My P95 is 38ms over HTTP versus 61ms for a cold stdio process spawn.
- MCP is not a free win. For two static endpoints called twice a day, a REST route is simpler and faster to ship.
What MCP Actually Standardizes
Before MCP, every client had its own tool format. A function I wrote for one assistant needed a rewrite for the next: different JSON shape, different auth, different error semantics. The Model Context Protocol, published by Anthropic and now supported across major clients, settles three primitives.
| Primitive | What it exposes | Example from my stack | Who controls it |
|---|---|---|---|
| Tools | Callable functions with typed input | search_invoices, create_ticket |
Model decides |
| Resources | Read-only data by URI | invoice://2026/1042 |
Client or user |
| Prompts | Reusable prompt templates | monthly-recon |
User selects |
The transport layer is separate from those primitives. Stdio runs the server as a child process and exchanges newline-delimited JSON. Streamable HTTP sends JSON-RPC over HTTP with optional server-sent events for streaming. The protocol version I target is 2025-06-18, which is stable across the clients I test.
The reason this matters to an Indian SME is cost. One server replaces three bespoke integrations. I quote ₹45,000 for an MCP server with four tools instead of ₹1,20,000 for three vendor-specific wrappers.
Transport Choice: The Numbers
| Transport | Cold start | P95 tool call | Auth model | Best fit |
|---|---|---|---|---|
| stdio | 180–400ms | 12–20ms warm, 61ms cold | Process owner | Local desktop tools |
| Streamable HTTP | 0 (always on) | 38ms | OAuth 2.1 or API key | Hosted multi-tenant tools |
| Legacy SSE | 0 | 44ms | Token in query | Deprecated, avoid |
I default to streamable HTTP for anything that touches a client database. Stdio is fine for a local file reader, but it spawns a process per session, and process spawn on a loaded laptop is the difference between a snappy agent and a sluggish one. The legacy SSE transport still appears in older tutorials; it is deprecated in the current spec and I have removed it from every server I maintain.
Production War Story: The OAuth Token That Expired Mid-Demo
In July 2026 I demoed an invoice MCP server to a Rajkot CA firm. The server used OAuth 2.1 with a 60-minute access token. The demo ran long, the token expired at minute 61, and every tool call returned 401. The client saw red errors and assumed the integration was broken. It was not. The server simply had no refresh path.
I fixed it in two layers. First, the server returns a structured AUTH_EXPIRED error code instead of a bare 401, so the client can trigger re-auth deliberately. Second, the Laravel client caches the token with a 5-minute safety margin and refreshes before expiry, not after failure. The retry wrapper below is what now runs in production.
<?php
// app/Services/McpClient.php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
class McpClient
{
public function __construct(
private string $baseUrl,
private string $clientId,
private string $clientSecret
) {}
public function call(string $tool, array $arguments): array
{
$payload = [
'jsonrpc' => '2.0',
'id' => (string) str()->uuid(),
'method' => 'tools/call',
'params' => ['name' => $tool, 'arguments' => $arguments],
];
$response = Http::withToken($this->token())
->acceptJson()
->timeout(15)
->post($this->baseUrl, $payload);
if ($response->status() === 401) {
Cache::forget('mcp:token:' . $this->clientId);
$response = Http::withToken($this->token())
->acceptJson()
->timeout(15)
->post($this->baseUrl, $payload);
}
if ($response->failed()) {
throw new \RuntimeException('MCP transport error: ' . $response->status());
}
$body = $response->json();
if (isset($body['error'])) {
throw new \RuntimeException(
$body['error']['code'] . ': ' . $body['error']['message']
);
}
return $body['result'];
}
private function token(): string
{
return Cache::remember(
'mcp:token:' . $this->clientId,
now()->addMinutes(55),
fn () => Http::asForm()->post($this->baseUrl . '/oauth/token', [
'grant_type' => 'client_credentials',
'client_id' => $this->clientId,
'client_secret' => $this->clientSecret,
])->json('access_token')
);
}
}
The 55-minute cache is deliberate. Refreshing before the edge removes the race where a call starts at minute 59 and fails at minute 60. Since the deploy, the Rajkot integration has logged zero auth failures across 14,200 tool calls.
Production War Story: Zombie stdio Servers
The second failure was self-inflicted. I shipped a stdio server for a local document index and left it running after a client upgrade. The new client version spoke a newer protocol revision, the old process did not answer the initialize handshake, and the client kept retrying. Within an hour, ps aux showed 38 orphaned Python processes, each holding 90MB of RAM. The laptop swapped, and the agent timed out on every call.
The fix was lifecycle discipline. The server now exits when stdin closes, writes a heartbeat every 30 seconds, and refuses to start if another instance holds a lock file. On the client side, I pin the protocol version and log the negotiated revision at startup.
# server.py
import asyncio, os, sys
from pathlib import Path
from mcp.server.fastmcp import FastMCP
LOCK = Path("/tmp/doc-index-mcp.lock")
if LOCK.exists():
pid = int(LOCK.read_text().strip() or 0)
try:
os.kill(pid, 0)
sys.exit(f"Another MCP server holds the lock (pid {pid})")
except ProcessLookupError:
LOCK.unlink()
LOCK.write_text(str(os.getpid()))
mcp = FastMCP("doc-index", version="1.4.0")
@mcp.tool()
async def search_docs(query: str, limit: int = 5) -> list[dict]:
"""Search the local documentation index. Limit is capped at 20."""
limit = max(1, min(limit, 20))
await asyncio.sleep(0)
return [{"title": f"Result for {query}", "score": 0.91, "limit": limit}]
@mcp.resource("doc://{slug}")
def read_doc(slug: str) -> str:
"""Return one document by slug."""
return f"# Document {slug}"
if __name__ == "__main__":
try:
mcp.run(transport="stdio")
finally:
if LOCK.exists():
LOCK.unlink()
Two lines carry the weight: the lock file and the finally cleanup. MCP servers are long-lived processes, and long-lived processes leak. Treat lifecycle as part of the feature, not an afterthought.
When NOT to Use MCP
Do not use MCP for two static endpoints that your own application calls. A signed REST route with a fixed payload is easier to test, easier to monitor, and has fewer moving parts. I have turned down three MCP projects this year for exactly that reason.
Do not use MCP when every millisecond matters. The protocol adds a JSON-RPC envelope and, over HTTP, a full request round trip. For a checkout path that must answer in 20ms, call the database directly. MCP is for model-driven tool selection, not for hot loops.
Do not expose destructive tools without a confirmation step. A delete_customer tool callable by a model is an incident waiting for a bad prompt. I gate every write operation behind an explicit approval resource that the client surfaces to the user.
The Migration That Paid for Itself
A Surat logistics client ran four separate integrations: one for their CRM assistant, one for a Cursor workflow, one for a Slack bot, and one for my Laravel agent. Each had its own auth and its own bug backlog. We replaced all four with one streamable HTTP MCP server exposing nine tools.
The build took eleven business days and cost ₹95,000. The maintenance line dropped from four codebases to one. Tool-call P95 fell from 210ms to 38ms because the old wrappers each performed their own redundant auth lookup. Six months later, the server has handled 380,000 calls with two incidents, both auth-related and both fixed by the retry pattern above.
If you are weighing a custom MCP server against vendor-specific wrappers, the Kimi K2 agent model review covers tool-calling reliability across open-weight models, and the LangGraph checkpoint guide covers durable state once the server is in the loop.
Frequently Asked Questions
What is the Model Context Protocol in simple terms?
MCP is an open JSON-RPC standard that lets an AI client discover and call tools, read resources, and load prompts from a server. One server works across Claude, Cursor, and custom agents, replacing separate integrations for each vendor.
Is MCP faster than a custom REST API?
For warm calls, no. A local REST route can answer in under 5ms. MCP over streamable HTTP measured 38ms P95 in my production logs. You choose MCP for portability and schema discovery, not raw speed.
Which transport should I use for a hosted MCP server?
Use streamable HTTP with OAuth 2.1 or API-key auth. Use stdio only for local desktop tools where the client owns the process. The legacy SSE transport is deprecated and should not be used for new builds.
How much does it cost to build an MCP server in India?
A four-tool server with tests and auth costs ₹45,000 fixed in my scope. A nine-tool multi-tenant server with OAuth and audit logging costs ₹95,000. Hosting on a ₹6K VPS handles roughly 40,000 tool calls per day.
Bottom line
MCP wins when one tool layer must serve several AI clients, and it loses when you wrap two endpoints for a single app. Standardize on streamable HTTP, pin the protocol revision, refresh tokens before expiry, and treat process lifecycle as a feature. I have run that playbook across nine migrations and 380,000 production calls. If you need one tool layer instead of four, start with the MCP build and bring your integration inventory.