Model Context Protocol (MCP)
From the Server to the Host and back, a deep dive into the inner workings of the MCP protocol
In the following I will dive into the inner workings of the MCP protocol from the underlying technologies to the abstracting SDKs and frameworks.
What is MCP?
Think of MCP like a USB-C port for AI applications
. Just as USB-C provides a standardized way to
connect your devices to various peripherals and accessories, MCP provides a standardized way to
connect AI models to different data sources and tools.
Why should you care?
-
The MCP specification was originally proposed by Anthropic but OpenAI and Google have also adopted it. Making it the industry standard for for Model Prodivers.
-
Big Techs like Attlassian, Cloudflare, Github, and others are adopting MCP and exposing their APIs as MCP servers.
-
More and more companies want to prevent vendor lock-in and reduce complexity in building and maintaining non-standard integrations
How does it work? (A technical deep dive)
To those familiar with HTTP and APIs, MCP is a simple HTTP request and response protocol. But instead of using JSON over HTTP, MCP uses JSON-RPC.
The MCP server
The server exposes tools (think post requests), resources (think get requests) and prompts (templates of how to interact with the server).
1. Data Layer: JSON-RPC (Remote Procedure Call)
JSON-RPC provides consistent message structure and three message types:
- Requests with IDs that expect responses
- Responses that match request IDs
- Notifications for one-way communication without responses
As the name Remote Procedure Call suggests, it enables to ask another computer to run a function (procedure) and get the result, as if you were running it locally.
To an LLM engineers familiar with tool calling, the structure of a JSON-RPC message may be familiar. Tools are essentially functions that the LLM can "call" remotely, using a JSON-RPC-like protocol to specify the function name, parameters.
Example web search tool call:
1{
2 "tool": "web_search",
3 "parameters": {
4 "query": "latest AI developments"
5 }
6}
7
2. MCP protocol methods (most methods are optional):
Method | Purpose | Returns |
---|---|---|
ping | Check if server is responsive | Empty result object |
initialize | Get server capabilities | Server capabilities and info |
tools/list | Discover available tools | Array of tool definitions with schemas |
tools/call | Execute a specific tool | Tool execution result |
resources/list | List available direct resources | Array of resource descriptors |
resources/read | Retrieve resource contents | Resource data with metadata |
resources/templates/list | Discover resource templates | Array of resource template definitions |
resources/subscribe | Monitor resource changes | Subscription confirmation |
here is a simple Python fastapi example in JSON-RPC format:
1# mcp.py
2import uvicorn
3from fastapi import FastAPI, Request
4
5app = FastAPI()
6
7@app.post("/mcp")
8async def json_rpc(request: Request):
9 body = await request.json()
10 method = body.get("method")
11 params = body.get("params")
12 id_ = body.get("id")
13
14 if method == "tools/list":
15 return {
16 "jsonrpc": "2.0",
17 "result": {
18 "tools": [
19 {
20 "name": "add_numbers",
21 "description": "Adds two numbers",
22 "inputSchema": {"type": "object", "properties": {"a": {"type": "number"}, "b": {"type": "number"}}, "required": ["a", "b"]},
23 }
24 ]
25 },
26 "id": id_,
27 }
28
29 if method == "tools/call":
30 tool_name = params.get("name")
31
32 if tool_name == "add_numbers":
33 a = params["arguments"]["a"]
34 b = params["arguments"]["b"]
35 return {"jsonrpc": "2.0", "result": {"sum": a + b}, "id": id_}
36
37 return {"jsonrpc": "2.0", "error": {"code": -32601, "message": f"Unknown tool: {tool_name}"}, "id": id_}
38
39 return {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": id_}
40
41if __name__ == "__main__":
42 uvicorn.run(app, host="0.0.0.0", port=8080)
43 # pip install fastapi uvicorn
44 # python mcp.py
45
You can test this server from your terminal using curl
.
To list the available tools:
1curl -X POST http://localhost:8080/mcp \
2-H "Content-Type: application/json" \
3-d '{
4 "jsonrpc": "2.0",
5 "method": "tools/list",
6 "id": 1
7}'
8
To call the add_numbers
tool:
1curl -X POST http://localhost:8080/mcp \
2-H "Content-Type: application/json" \
3-d '{
4 "jsonrpc": "2.0",
5 "method": "tools/call",
6 "params": {
7 "name": "add_numbers",
8 "arguments": { "a": 5, "b": 3 }
9 },
10 "id": 2
11}'
12
3. Transport Layer
Now that we know what format / purpose the data has we we need to transfer it. The MCP protocol supports multiple transport layers:
Local Servers:
- Stdio transport (standard input and output) used for locally running MCP servers, most server use this transport layer but its not a scalable or long term solution for corporations.
Remote Servers:
- Streamable HTTP uses HTTP POST & GET requests for client-to-server messages with optional Server-Sent Events for streaming capabilities, can be identified by
/mcp
endpoint. - SSE (Server-Sent Events) this is a legacy transport layer servers using it will have a
/sse
endpoint.
4. Authentication
Remote servers can handle multiple client connections, to identify users authentication can be implemented bearer tokens, API keys, and custom headers. MCP recommends using OAuth to obtain authentication tokens.
To start tinkering I recommend using simple API keys passed as headers.
But for production the MCP specification suggests that clients and servers should support the OAuth 2.0 Dynamic Client Registration (DCR) Protocol RFC7591 to allow MCP clients to obtain OAuth client IDs without user interaction spec Which lets clients register with an OAuth 2.0 server automatically and get their own credentials, making it easier and more scalable to onboard new clients.
After DCR the authentication token is obtained through the OAuth 2.0 Authorization Code flow more on this in my OAuth Compendium.
Building an MCP server
To build an MCP server you won't have to write JSON-RPC methods, you can use one of the many SDKs for TypeScript, Python, Rust, Go, Java, C#, Kotlin, Ruby, Swift and C# theres even one for NextJS by Vercel.
When it comes to choosing your tech stack it differ much from traditional API development. Choose an SDK in a language you are familiar with, determine if and to what extent authentification is needed. Depending on your needs deploy to a cloud provider or self-host, implement load balancing and monitoring if need be.
My must have methods: initialize, ping, tools/list, tools/call (since most clients only support tools) Optional methods: resources/list, resources/read only look into other methods if your provider supports them.
Security considerations are similar to traditional web development and outlined int the specification security best practices. Although I like to urge to consider Prompt Injections as they are the biggest attack vector.
Preventing prompt injections, there is not set playbook yet but generally consider what information is being exposed and what kind of actions the model can take. If you are using a Host application that doesnt support tool call confirmation be vary of direct calls mitigatating user control. Currently the best way to prevent hostile tool inputs from messing with your data is using an other LLM as a judge (evaluation paper) to determine if the tool call is malicious or not.
here is a simple express server:
1import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
3import express from "express";
4import { randomUUID } from "node:crypto";
5import { z } from "zod";
6
7const app = express();
8app.use(express.json());
9
10const server = new McpServer({ name: "example-server", version: "2025-06-18" });
11
12server.registerTool(
13 "add_numbers",
14 {
15 description: "Adds two numbers",
16 inputSchema: { a: z.number(), b: z.number() },
17 },
18 async ({ a, b }: { a: number; b: number }) => {
19 return { content: [{ type: "text", text: `Result: ${a + b}` }] };
20 }
21);
22
23app.post("/mcp", async (req, res) => {
24 const transport = new StreamableHTTPServerTransport({
25 sessionIdGenerator: () => randomUUID(),
26 });
27 await server.connect(transport);
28 await transport.handleRequest(req, res, req.body);
29});
30
31app.listen(8080, () => console.log("MCP server running on port 8080"));
32
To test / debug your MCP server you can use the MCP Inspector
1pnpx @modelcontextprotocol/inspector
2
Supplying MCP Server Data to a Model
MCP clients communicate with MCP servers and are instantiated by host applications like Langdock, Cursor, Claude or ChatGPT.
Host -> has many Clients
Clients -> have 1on1 connection with Servers
Host applications need to be able to discover MCP Servers through their URLs, usually containing the subdomain mcp.
and a mcp endpoint usually /mcp
(for legacy servers /sse
).
Building a MCP host
1. Discover MCP Server
This will be inciated by the user entering the URL of the MCP server in the host application.
ping
the server to see if it is responsive
** 2. Determine Transport Layer**
This can be done by parsing the URL and checking for the /mcp
or /sse
endpoint.
3. Authentication
Try to initialize
the server to get the server capabilities and info, and list
the tools available.
If this fails you now that authentication is needed.
Let the user determine what type either OAuth 2.0 or API key.
If API key, let the user enter the API key and send it in the request headers.
For OAuth 2.0, you'll need to:
- discover the Authorization URL and Token URL, this can be done through trying to call different
/.well-known/...
endpoints. - once your have the URLs you can start the dynamic client registration flow, meaning you can register a client with the server and get a client ID and client secret.
- than with the cleint id you can redirect the user through the OAuth flow where they can login and get an access token.
- this Auth access token will be used to authenticate the user when calling the server.
4. Create a Connection
Now a connection can be established find some pseudo code from the TypeScript SDK creating a streamable HTTP transport connection below.
1import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
3
4if (authenticationType === "oauth") {
5 transport = new StreamableHTTPClientTransport(new URL(config.url), {
6 authProvider: oauthProvider,
7 });
8} else if (authenticationType === "apiKey") {
9 transport = new StreamableHTTPClientTransport(new URL(config.url), {
10 requestInit: { headers },
11 });
12} else {
13 transport = new StreamableHTTPClientTransport(new URL(config.url));
14}
15
16const client = new Client({ name: "client-name", version: "2025-06-18" });
17
18await client.connect(transport);
19
5. Negotiate MCP protocol version
Next the client and server need to agree on the MCP protocol version.
1const version = await client.getServerVersion();
2
6. Calling Tools
Now the client can start calling tools on the server.
1const toolCallResult = await client.callTool({
2 name: "add_numbers",
3 arguments: { a: 1, b: 2 },
4});
5
6client.close(); // close the connection
7
MCP tool calling can also be facilitated by LLM-frameworks like the Vercel SDK which can be useful for unifying tool calling across different LLMs.
If you have any further questions, feedback or just want to chat about MCP please don't hesitate to reach out!