Skip to content

MCP: A Cross-System Standard for Tool Integration

Tool integrations often repeat the same work across hosts: discover available operations, describe arguments, dispatch a call, and interpret the result. Without a shared interface, each host/tool pair can need its own adapter.

Model Context Protocol standardizes that interface. Anthropic introduced it in November 2024. A host implements an MCP client; a tool provider implements an MCP server. The contract is independent of which model generates the tool request.


N × M integrations

Suppose you want to support several hosts: Claude Code, Cursor, a custom agent you wrote, maybe an internal copilot. Call that number NN. There are also some number of tools you want them all to reach: a filesystem, a GitHub client, a database, a Slack client, whatever. Call that MM.

If each (host, tool) pair needs its own custom adapter, you write N×MN \times M adapters. Each adapter may handle different authentication, different argument conventions, different result formats. A new host needs adapters for the tools it uses; a new tool needs adapters for the hosts that use it. A shared protocol can reduce repeated adapter work, and the idealized adapter count drops to N+MN + M. This counts one client per host and one server per tool; authentication, schema compatibility, and host capabilities still require integration work.

With six hosts and eight tools, the idealized count is 48 custom integrations or 14 standard adapters. Change the counts below to compare them. The shared protocol is a contract implemented at each endpoint; it does not require a central network broker.

SCALING·N × M vs N + M
add a host or a tool, watch the two totals diverge
HOSTS (N = 6)
  • ●Claude Code
  • ●Cursor
  • ●Custom agent
  • ●OpenAI Responses
  • ●Internal copilot
  • ●CI bot
TOOLS (M = 8)
  • ●Filesystem
  • ●GitHub
  • ●Postgres
  • ●Slack
  • ●Sentry
  • ●Jira
  • ●S3
  • ●Stripe
BESPOKE · N × M
6 × 8 = 48 adapters
MCP · N + M
6 + 8 = 14 adapters
The bespoke adapter count is 3.43× the standard adapter count here. Counts do not measure implementation effort.

Each step adds a host on the left or a tool on the right. The red bar is the bespoke count (N×MN \times M); the green bar is the MCP count (N+MN + M). These counts assume every host needs access to every tool.

We will use a local subprocess to examine what a server exposes and how a host calls it. Remote MCP, OAuth, and Enterprise Auth covers HTTP transport and authorization.


Prereqs

This builds on:

  • Function calling as structured generation from the function-calling post. This defines the relationship between tool_use arguments, schemas, and host validation.
  • The agent loop from the-agent-loop. In the model → runtime → tool → resume cycle, the model emits a request and the host executes it.
  • Transcript formats from transcript-formats. Providers represent calls and results differently in their transcripts.

MCP standardizes how the host discovers and calls tools. The host still represents those calls in the model provider’s transcript format.


The minimal formalism

MCP is plain JSON-RPC 2.0 over a transport. A request has an id, a method name, and optional params; a response has the same id and either a result or an error.

request={ jsonrpc, id, method, params }\text{request} = \{\, \texttt{jsonrpc},\, \texttt{id},\, \texttt{method},\, \texttt{params}\,\} response={ jsonrpc, id, result  ∣  error }\text{response} = \{\, \texttt{jsonrpc},\, \texttt{id},\, \texttt{result} \;\mathbin{|}\; \texttt{error}\,\}

JSON-RPC supplies the envelope. MCP adds transport framing, schemas, and method contracts; the core discovery calls include:

  • initialize — hello, what protocol version do you speak, what capabilities do you offer?
  • tools/list, tools/call — discover and invoke tools.
  • resources/list, resources/read — discover and read resources.
  • prompts/list, prompts/get — discover and fetch prompt templates.
  • notifications/… — one-way messages, e.g. "the tool list just changed."

File-reading and database-querying servers can both answer tools/list with schemas and tools/call with results, despite using different code to perform the work.

The specification defines two transports:

  • stdio for local processes. The host launches the server as a subprocess and talks to it over stdin/stdout. This is a common local-development arrangement, and it's what we'll use here.
  • streamable HTTP for remote servers. HTTP requests, optional server-sent event streams, and an optional authorization framework based on OAuth. The next post covers HTTP framing and authorization.

The lifecycle, step by step

The example follows initialization, discovery, and one tool call through nine steps, including the initialization notification.

STEP 1 / 9·MCP LIFECYCLE
initialize
CLIENT (HOST)
Claude Code, Cursor, your app…
→
SERVER
Filesystem, GitHub, calc…
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-11-25",
    "capabilities": {},
    "clientInfo": { "name": "demo", "version": "0.1" }
  }
}
client says hello, declares protocol version

Follow the request IDs through initialization, discovery, and a tool call. The initialization notification has no response.

During initialize, the client proposes a protocol version; the server either accepts or counter-proposes. Capabilities are declared up front, so a host knows before it asks whether a server exposes resources, prompts, or tools. Sampling is a client capability that a server can request. After the client sends notifications/initialized, everything else is steady-state discovery and dispatch.

The tools/list call gives the host tool names, descriptions, and input schemas. The host adapts those definitions to the model API’s tool format and supported schema subset. Discovery is standardized; provider-specific translation and validation still belong to the host.

The host mediates between the model and the MCP server. The model emits a tool_use block into its transcript (exactly as in the function-calling post); the host reads that block and issues a tools/call; the server does its work and returns a result; the host injects that result back into the transcript as a tool_result; the model resumes. The host’s MCP client handles the server exchange while its model adapter preserves the provider’s transcript format.


Three primitives, not one

MCP exposes tools to invoke, resources to read, and prompt templates to retrieve. The host can present these through different controls:

Example payload excerpts; JSON-RPC envelopes and some content are omitted.

control: the model can request them; the host controls executionlist: tools/listcall: tools/call
EXAMPLE
{
  "name": "github_create_issue",
  "description": "Open a new GitHub issue in a repo.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "repo":  { "type": "string" },
      "title": { "type": "string" },
      "body":  { "type": "string" }
    },
    "required": ["repo", "title"]
  }
}
Tools are functions the model can call. The server publishes a JSON-schema contract; the model fills the arguments; the host runs the call.

Select a primitive to inspect its discovery and invocation methods, along with an example result.

The model can request a tool from its schema; the host decides whether to execute it. Resources let the host supply context: a code editor might attach git://repo/HEAD to a turn, or a user might select a file by URI. Prompts can appear as slash commands, menus, or saved workflows for the user to invoke.

For example, a database-backed server could expose a postgres://orders/last_24h resource or a run_query tool. The resource supplies a document chosen and fetched by the host. The tool lets the model request SQL queries, which require validation and permission checks before execution.


Worked example

Consider a calculator server exposing add and multiply. Here is what happens when a host connects to it.

The server comes up. It registers two tools with JSON schemas. A host (imagine Claude Code) launches it as a subprocess over stdio, sends an initialize request. The server returns the negotiated version and capabilities, including { tools: {} }. Once it accepts that response, the host sends notifications/initialized. The host calls tools/list, gets back the two schemas, and hands them to the model on the next turn.

Now the user types "what is 7 times 9?" The model sees two tools with their schemas. It picks multiply and emits a tool_use with {a: 7, b: 9}. The host sees the tool_use, maps it back to multiply, and issues a JSON-RPC tools/call request over stdin to the calculator subprocess. The subprocess does the arithmetic, sends back {content: [{type: "text", text: "63"}]} over stdout, and the host injects that result into the next model turn. The model reads "63" and replies to the user.

A compatible host can reuse this server. It discovers add and multiply through tools/list, without hardcoding those names into its MCP client.


Implementation sketch

Here is the whole server in the official Python SDK. The example targets Python SDK v1.

# pip install 'mcp>=1,<2'
from mcp.server.fastmcp import FastMCP
 
mcp = FastMCP("calc")
 
@mcp.tool()
def add(a: float, b: float) -> float:
    """Add two numbers."""
    return a + b
 
@mcp.tool()
def multiply(a: float, b: float) -> float:
    """Multiply two numbers."""
    return a * b
 
if __name__ == "__main__":
    # Serve over stdio. The host launches this as a subprocess and
    # talks to us on stdin/stdout using JSON-RPC.
    mcp.run(transport="stdio")
# And a tiny client, also using the SDK. In practice the host (Claude Code,
# Cursor, your app) is doing this for you, but it helps to see it.
import asyncio
from mcp.client.stdio import stdio_client, StdioServerParameters
from mcp import ClientSession
 
async def main():
    params = StdioServerParameters(command="python", args=["calc_server.py"])
    async with stdio_client(params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()
            tools = await session.list_tools()
            print([t.name for t in tools.tools])   # ['add', 'multiply']
            out = await session.call_tool("multiply", {"a": 7, "b": 9})
            print(out.content[0].text)             # SDK rendering may show 63 or 63.0
 
asyncio.run(main())

The @mcp.tool() decorator registers the function and derives its tool definition from the signature, type hints, and docstring. The SDK publishes that definition through tools/list. When the host issues tools/call with {"name": "multiply", "arguments": {"a": 7, "b": 9}}, the SDK validates the arguments against the schema, invokes the Python function, wraps the return value as {"content": [{"type": "text", "text": "63"}]}, and sends it back.

The server publishes tool schemas without depending on the client’s model. The client also need not use the server’s implementation language: this Python client could exchange the same messages with a TypeScript server.


Misconceptions

"MCP is just function calling with extra steps." Function calling represents requests and results between a model API and its caller. MCP connects the host to tool providers, including discovery before a model turn. A shared interface can reduce repeated adapter work across hosts, but its benefit depends on how many integrations can reuse it.

"MCP is Anthropic-only." The protocol is open and independent of a particular model. A host and server still need compatible protocol versions, capabilities, and authorization arrangements.

"You need an LLM to use MCP." An MCP client is anything that speaks the protocol. Plain scripts can invoke MCP servers as a unified interface to local tools without any model involved: call the filesystem server's read_file tool, pipe it into a different server's analyze_log tool, wire together something like a Unix pipeline over JSON-RPC.

"MCP is a framework." It isn't. It's a spec, and a set of SDKs that make the spec easy to implement. You can write an MCP server with no SDK at all if you want, because the wire format is documented and it's JSON. The SDKs give you decorators and transport plumbing.

What's next

The next post covers Remote MCP, OAuth, and Enterprise Auth: what changes when the server is an HTTP endpoint instead of a subprocess, how streamable HTTP and server-sent events carry the same JSON-RPC traffic, and how the authorization framework uses OAuth 2.1 with PKCE for interactive delegation.


Additional reading (and watching)

  • Anthropic. (2024). Introducing the Model Context Protocol. The original announcement post, November 2024, laying out the motivation and the initial open-source release.

  • Model Context Protocol. (2025–2026). Specification. The canonical specification of the protocol, covering lifecycle, primitives, and transports. Use the version negotiated during initialization; this post references the 2025-11-25 lifecycle and transport contracts.

  • JSON-RPC Working Group. (2013). JSON-RPC 2.0 Specification. The underlying RPC envelope MCP uses. It defines request, response, and notification shapes.

  • Model Context Protocol. Python SDK. The mcp package on PyPI. The FastMCP helper used in the worked example is the high-level path; the low-level server API is also in the same repo.

  • Model Context Protocol. TypeScript SDK. The other reference SDK. Together with the Python one these set the shape most third-party implementations follow.

  • OpenAI. (2025). Agents SDK: MCP support. Documentation of how the OpenAI Agents SDK consumes MCP servers as a tool source; MCP support also landed in the Responses API in 2025.

  • Microsoft. (2016–). Language Server Protocol. The canonical prior art: same N×M→N+MN \times M \to N + M argument for editors and language analyzers. LSP standardizes communication between editors and language servers.

  • Model Context Protocol. Server Registry. Public directory of MCP servers, with publisher and version metadata. Inclusion is not a trust or compatibility guarantee.