SAP Insiders
Articles/Artificial Intelligence/Model Context Protocol (MCP) — A Deep Dive From Both Sides of the Wire
Artificial Intelligence

Model Context Protocol (MCP) — A Deep Dive From Both Sides of the Wire

A practitioner's deep dive on Anthropic's Model Context Protocol — the open standard that lets any AI app talk to any tool or data source. JSON-RPC anatomy, three primitives (Tools, Resources, Prompts), three transports (stdio, HTTP+SSE, Streamable HTTP), full security model, when to use it and when to skip it. Heavy on code, sample messages, and clear architecture from both client and server sides.

Model Context Protocol — the open standard that lets any AI host talk to any server. Left half purple representing the client/host side (Claude, Cursor, custom agents). Right half teal representing the server side (filesystem, GitHub, Slack, your own server). A protocol pill runs down the middle.

In late 2024 Anthropic released an open specification with the kind of unassuming name engineers usually ignore: Model Context Protocol. A year and a half later it's the closest thing the AI industry has to an actual standard — a shared plug that lets any AI host (Claude, Cursor, Zed, custom agents) talk to any tool or data source (the filesystem, GitHub, Postgres, Slack, your internal API) without bespoke glue code for every combination.

This is the protocol explained from the inside. No marketing, no "AI changes everything." Just: here is the wire format, here is the lifecycle, here is what runs on the left of the wire and what runs on the right, here is what you write to ship one.

Everything is two-sided on purpose — every diagram below is split: left half (purple) is the client/host, right half (teal) is the server. The line down the middle is the protocol.

Model Context Protocol overview poster — the whole picture in one frame. Left half purple for the client/host side, right half teal for the server side. Seven sections: the core idea (client AI app wants context, server owns the data), the protocol (JSON-RPC 2.0 in three lifecycle stages handshake then discovery then invocation), three primitives (Tools, Resources, Prompts), three transports (stdio, HTTP+SSE legacy, Streamable HTTP modern), security model (user consent, OAuth 2.0, sandboxing, audit trail), when to use it versus when to skip it, ecosystem at a glance with reference servers, hosts and SDKs. Bottom takeaway: MCP is USB-C for AI — one open plug, every host, every tool.

What MCP actually is

MCP is a JSON-RPC 2.0 protocol with a fixed set of methods and a fixed message lifecycle. It defines:

  • Three primitives a server can expose — tools, resources, prompts.
  • Three transports the messages can travel over — stdio (local), HTTP + SSE (legacy remote), Streamable HTTP (modern remote).
  • A handshake (initialize) that negotiates capabilities between client and server.
  • A consent model the host is responsible for enforcing — every tool call goes through user approval.

Notice what MCP is not. It is not a model, not an agent framework, not a runtime. It is a cable. The intelligence lives in the client (the model deciding what to call) and the capability lives in the server (the code that actually does the thing). The protocol just defines how they talk.

A common one-liner: MCP is USB-C for AI. One open plug, every host, every tool.

The split — client side and server side

The protocol is symmetric only at the message layer. Conceptually each side does very different work.

MCP anatomy of one round-trip — the same exchange seen twice, once from the client/host side on the left in purple and once from the server side on the right in teal. Left side shows the host app (Claude.ai, Cursor), the client SDK that opens the transport, and a 5-step decision loop (plan, pick a tool, send call_tool, wait, continue reasoning). Right side shows the MCP server process, the server SDK with handler registrations, and the exposed capabilities (tools, resources, prompts). The middle shows JSON-RPC messages flowing both ways: initialize handshake, tools/list discovery, tools/call invocation, each with a typed response. Same wire format, different roles.

The client/host side

The host is the user-facing application — Claude Desktop, Cursor, a custom agent loop you build. Three responsibilities:

  • Run the model. The host owns the inference loop, the system prompt, the conversation history.
  • Manage MCP connections. A host can be connected to many servers at once. Each gets its own JSON-RPC session.
  • Mediate consent. Every tool call surfaces a user-visible approval (allow once / allow for session / deny). No exceptions.

Inside the host there's a client SDK (the Anthropic-maintained @modelcontextprotocol/sdk is the reference; other languages have ports) that handles the transport, message framing, and request/response correlation. Application code only sees await session.callTool("name", { arg: value }).

The server side

The server is a process you run that exposes some capability — the filesystem on the local machine, a connection to your Postgres, a wrapper over your internal API. Three responsibilities:

  • Advertise capabilities. On initialize, declare which primitives you support (tools, resources, prompts, sampling, roots).
  • Implement handlers. Register a function for each tool. The SDK calls them with parsed arguments.
  • Return typed content. Results are JSON-serialisable content blocks (text, image, resource references).

The server SDK pattern looks identical across languages — McpServer instance, decorators or register() calls, a start() that opens the chosen transport.

The wire format — actual JSON-RPC

Everything is JSON-RPC 2.0 over the chosen transport. Three lifecycle stages:

Stage 1 — handshake

The client opens the transport and sends an initialize request:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": { "roots": { "listChanged": true } },
    "clientInfo": { "name": "ClaudeDesktop", "version": "0.9.4" }
  }
}

The server replies with its own info and the capabilities it supports:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "tools": { "listChanged": true },
      "resources": { "subscribe": true, "listChanged": true },
      "prompts": { "listChanged": true }
    },
    "serverInfo": { "name": "my-mcp-server", "version": "1.0.0" }
  }
}

The client then sends notifications/initialized and the connection is live. From here on, either side can send requests or notifications.

Stage 2 — discovery

The client asks what's available:

{ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }

The server returns a typed catalog. Each tool has a JSON Schema for its input — this is what the model uses to decide how to call it:

{
  "jsonrpc": "2.0", "id": 2,
  "result": {
    "tools": [
      {
        "name": "query_pg",
        "description": "Run a read-only SQL query against the analytics warehouse.",
        "inputSchema": {
          "type": "object",
          "properties": {
            "sql": { "type": "string" },
            "limit": { "type": "integer", "default": 100 }
          },
          "required": ["sql"]
        }
      }
    ]
  }
}

The same pattern repeats for resources/list and prompts/list.

Stage 3 — invocation

The model picks a tool. The client gets user consent. The client sends:

{
  "jsonrpc": "2.0", "id": 3,
  "method": "tools/call",
  "params": {
    "name": "query_pg",
    "arguments": { "sql": "select count(*) from users", "limit": 1 }
  }
}

The server runs the handler and returns content blocks:

{
  "jsonrpc": "2.0", "id": 3,
  "result": {
    "content": [
      { "type": "text", "text": "[{\"count\": 12483}]" }
    ],
    "isError": false
  }
}

That's the whole loop. The model reads the result, decides whether to call more tools, eventually replies to the user.

The three primitives

Everything a server exposes falls into one of three buckets.

MCP three primitives — Tools in purple (functions the model can call, side-effect-bearing, schema name plus description plus inputSchema, methods tools/list and tools/call, examples create_issue, run_query, send_message), Resources in teal (read-only data addressed by URI, idempotent, schema uri plus name plus mimeType, methods resources/list, read, subscribe, examples file://logs/2026-06-04.log), Prompts in amber (reusable parameterised templates a user can pick from a slash menu, schema name plus arguments, methods prompts/list and prompts/get, examples slash-summarise-pr, slash-rca-incident).

Tools — actions the model takes

The headline primitive. A tool is a function the model can call:

  • Has side effects. Sends a message, creates an issue, runs a query that writes data.
  • Schema-validated. The model only sees the JSON Schema; the SDK validates the arguments before calling your handler.
  • Always user-approved. The host pops a confirmation. No exceptions.

If you're only going to expose one primitive, expose tools. Resources and prompts are optional improvements.

Resources — data the model reads

Resources are URIs the model can fetch. Read-only by design. The host typically pre-loads important ones into the conversation context, and the model can ask for more via resources/read:

  • file:///workspace/README.md
  • postgres://localhost/mydb/schema/users
  • git://repo/HEAD/path/file.py

Resources can be subscribed to — the server pushes notifications/resources/updated events when something changes. That's how a file-watching MCP server tells Claude to re-read a file.

Prompts — templates users pick

Prompts are reusable, parameterised conversation starters. They show up as slash commands in hosts that support them (/summarise-pr, /rca-incident). When the user picks one and fills in arguments, the host calls prompts/get and gets a multi-message conversation template that becomes the start of the dialogue.

Prompts are the smallest of the three primitives but the most under-used. They're how you ship best-practice workflows to your team alongside the tools — "the right way to ask the GitHub MCP server about a PR is this prompt, not free-text."

The three transports

Same protocol, three ways to get the bytes from client to server.

MCP three transports — Local stdio in purple (host spawns server as child process, JSON-RPC over stdin and stdout line-delimited, used by Claude Desktop, Claude Code and Cursor, strengths are zero networking and zero auth needed sub-millisecond latency trivial debugging, limits are local-machine-only and binary must exist, example npx -y filesystem server tilde), Legacy remote HTTP plus SSE in teal (client POSTs requests server pushes responses over SSE, two-endpoint pattern, used by first-generation hosted servers, works through firewalls, server-pushed notifications native, two endpoints to manage and stateful), Modern remote Streamable HTTP in amber (single endpoint, POST receives chunked HTTP responses stream events back, recommended for modern hosted MCP servers, one endpoint stateless-friendly serverless-compatible, long-lived chunked HTTP must be proxy-aware).

stdio — for local servers

The host spawns the server as a child process. JSON-RPC messages travel line-delimited over stdin and stdout. Notifications and errors go on stderr (logged but not parsed as protocol).

# Claude Desktop config:
{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/projects"]
    }
  }
}

This is by far the simplest mode. Zero networking concerns, zero auth, sub-millisecond latency. Use it for anything that runs on the same machine as the host — file access, git, local databases, dev-tool integrations.

HTTP + SSE — the legacy remote mode

The first hosted-server design. The client opens a long-lived GET /sse connection to receive server-to-client messages; it POSTs to /messages to send client-to-server messages. Two endpoints, stateful sessions.

It works, but the two-endpoint pattern is awkward to deploy on serverless platforms and makes session resumption tricky. Most new code skips it and goes straight to Streamable HTTP.

Streamable HTTP — the modern remote mode

Single endpoint. The client POSTs a request; the response is either a single JSON body (for simple replies) or a chunked HTTP response with Content-Type: text/event-stream for streaming results and server-to-client notifications.

This is the recommended transport for new remote servers. One endpoint, stateless friendliness (each request is independent), works on serverless platforms.

The trade-off: chunked HTTP over middle-boxes (corporate proxies, ALBs with aggressive idle timeouts) needs careful tuning. For Cloud Run, Fly.io, Workers — generally fine.

A complete server in 30 lines

Here's a minimal MCP server in TypeScript that exposes one tool. It's complete; you can run it.

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({
  name: "weather",
  version: "1.0.0",
});

server.tool(
  "get_forecast",
  "Get the 7-day forecast for a city.",
  {
    city: z.string().describe("City name, e.g. 'Munich'"),
    units: z.enum(["metric", "imperial"]).default("metric"),
  },
  async ({ city, units }) => {
    const r = await fetch(
      `https://api.example.com/forecast?q=${city}&u=${units}`,
    );
    const data = await r.json();
    return {
      content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
    };
  },
);

const transport = new StdioServerTransport();
await server.connect(transport);

That's it. No framework, no schema-defining DSL, no message-handling boilerplate. The SDK does the protocol; you write the business logic.

To wire it into Claude Desktop, drop this into claude_desktop_config.json:

{
  "mcpServers": {
    "weather": {
      "command": "node",
      "args": ["/abs/path/to/weather-server.js"]
    }
  }
}

Restart Claude. The get_forecast tool now appears.

The security model

MCP is designed with the host as the enforcement point. The protocol itself trusts servers; the host trusts the user. Concretely:

  • User consent on every tool call. Hosts must show a permission prompt. Users can allow once, allow for session, or deny. There is no spec-allowed bypass.
  • Auth on remote servers. OAuth 2.0 with PKCE is the canonical pattern. API keys work for narrower deployments. Dynamic client registration handles the bootstrap problem (the host doesn't know in advance which server it'll connect to).
  • Sandboxing via roots. A server can advertise the URIs it will operate over; the client confirms. The filesystem server restricts itself to declared directories.
  • TLS for remote. Cleartext transports are not allowed for remote servers in production. The host should refuse a non-TLS remote URL.
  • isError, not exceptions. Tool failures come back with isError: true and a human-readable text content block. The model sees the error and adapts. No stack traces leak to the user.

The threat model is not "server is hostile" — the user explicitly chose to connect to that server. The threat model is "model decides to do something the user didn't intend" and the answer is the consent prompt.

When to use MCP

It's not a fit for everything. Worth being honest about both ends.

Strong fit:

  • You want multiple AI clients to share the same tools (Claude Desktop + Cursor + a custom agent all using the same internal MCP server).
  • The user will run an interactive session where the model decides which tools to call.
  • The list of available tools changes per project, per user, per workspace.
  • You want OAuth, consent, audit built in.
  • You're a vendor exposing an API to AI clients and want to ride one protocol instead of N integrations.

Poor fit:

  • One app, one tool call, no other clients — direct API is simpler.
  • Deterministic backend orchestration where you know exactly which step runs when — use a workflow tool, not an MCP loop.
  • Sub-100ms latency matters; the JSON-RPC + consent loop adds overhead.
  • You can call the underlying API directly without a model in the loop.
  • Headless batch jobs running on a schedule — no consent UI to show.

The simplest test: does the user pick from a menu of capabilities at runtime? If yes, MCP. If you know all the calls at write-time, just call the API.

The ecosystem today

A snapshot of what's actually shipping:

  • Reference servers (Anthropic-maintained): filesystem, postgres, sqlite, git, github, slack, gdrive, puppeteer, brave-search, memory, fetch.
  • Hosts: Claude Desktop, Claude Code, Cursor, Zed, Continue, LibreChat, Cody, plus custom agent loops people build with the SDKs.
  • SDKs: TypeScript, Python (Anthropic-maintained), Rust, Kotlin, C#, Swift, Go, others (community).

The pattern that's emerging: every developer tool vendor publishes an official MCP server. Linear, Stripe, Notion, Datadog, Postgres, Sentry. The differentiation moves from "we have an API" to "we have a great MCP server that the model actually uses well." Tool description writing becomes a craft.

Common pitfalls

A short list from real deployments:

  • Tool descriptions matter more than tool code. The model reads only the description and the schema. Vague descriptions → wrong tool picks. Spend ten minutes on the description for each tool.
  • Too many tools. Models start mis-picking past ~30 tools. Group related operations behind one tool with a kind argument, or split into multiple servers.
  • Heavyweight schemas. A 200-property input schema is hostile to the model. Keep them tight; use enums where possible.
  • Long-running tools without progress. A 30-second tool call blocks the conversation. Either return immediately with a token and let the model poll, or use notifications (notifications/progress).
  • Silently failing on isError. Always include a text content block describing what went wrong. The model can recover from a clear error message but not from isError: true alone.
  • stdio buffering. Forgot to flush stdout after a JSON-RPC reply? The host hangs. Most SDKs handle this; if you're writing your own, line-flush.
  • Remote auth on first connect. Dynamic client registration is the protocol-level answer. If you don't implement it, every user has to manually register an OAuth client.

Frequently asked questions

Is MCP only for Anthropic models? No — the protocol is open. Any model in any host can use any MCP server. Anthropic created it but doesn't gate it.

Does the model see the JSON-RPC? No. The model sees the tool descriptions and the textual content of results. The framing is abstracted by the host.

Can a server call back into the model? Yes — the sampling/createMessage flow lets the server ask the host to run an LLM completion. Used for tools that need sub-reasoning. Optional capability; few servers implement it yet.

How does MCP compare to Function Calling? Function calling is one model talking to one app's predefined functions. MCP is many models / hosts talking to many servers via one shared protocol with consent and auth built in. Function calling is the message layer; MCP is the integration standard.

What about Tool Use APIs (Anthropic Messages API tools)? That's the model-facing tool interface. MCP is the server-side discovery + invocation interface. They compose: the host receives tool descriptions via MCP and feeds them to the Messages API as tool definitions.

Will MCP replace OpenAPI / GraphQL? No — they solve different problems. OpenAPI describes machine-to-machine REST. MCP describes a consent-mediated, model-discoverable integration. They coexist; many MCP servers wrap REST APIs.

How stable is the spec? Stable in shape, still evolving in detail. Protocol version 2025-06-18 (current at time of writing) is mature; each version negotiates capability sets at handshake so older servers keep working.

Key takeaways

  • MCP is a protocol, not a runtime. It defines messages and lifecycle; intelligence lives in the host, capability lives in the server.
  • Three primitives — Tools, Resources, Prompts. Most servers only need Tools. The other two are powerful for advanced use.
  • Three transports — stdio (local), HTTP+SSE (legacy), Streamable HTTP (modern). Start with stdio; promote to Streamable HTTP when you go remote.
  • Consent is host-enforced, every call. The protocol can't bypass user approval, and that's the point.
  • OAuth + dynamic registration for remote auth. API keys allowed, OAuth preferred, plaintext never.
  • Quality of integration depends on quality of tool descriptions. This is the new craft. Practice it.

If you build AI products, MCP is now table stakes. If you maintain an API or platform, not shipping an MCP server is increasingly a decision you'll have to justify. The plug is open; the question is just whether your stack speaks the standard or not.