|$ curl https://forge-ai.dev/api/markdown?path=docs/ai/mcp
$cat docs/mcp-protocol.md
updated Recently·35 min read·published

MCP Protocol

MCPAIProtocolAdvanced
What is MCP?

The Model Context Protocol (MCP) is an open protocol that standardizes how applications provide context and tools to large language models. Think of MCP as the USB-C for AI applications — it provides a universal interface for connecting LLMs with external data sources, tools, and services.

MCP was designed to solve the fragmentation problem in AI tooling. Before MCP, every LLM integration required custom code — a custom API wrapper for databases, another for file systems, another for web search. MCP provides a single, standardized protocol that any MCP-compatible server can implement and any MCP-compatible client can consume.

info

MCP is not a framework or library — it is a protocol specification. This means it is language-agnostic and can be implemented in Python, TypeScript, Go, Rust, or any language. The protocol defines the wire format, not the implementation.
Architecture Overview

MCP follows a client-server architecture with three core components: hosts initiate connections, clients maintain sessions, and servers provide capabilities. The protocol defines how these components discover, negotiate, and communicate capabilities.

Core Concepts

Host — The application or environment that initiates the connection (e.g., Claude Desktop, an IDE, a custom web app). The host is responsible for user authentication and authorization.

Client — A stateful session within the host that maintains a 1:1 connection with a server. The client handles message routing, capability negotiation, and lifecycle management.

Server — A lightweight process that exposes specific capabilities (tools, resources, prompts). Each server operates independently and can be composed to provide complex functionality.

Transport — The underlying communication mechanism. MCP supports stdio (for local processes) and Server-Sent Events over HTTP (for remote services).

CapabilityDescriptionExample
ToolsExecutable actions that the server can perform (with user approval)Search web, query database, send email
ResourcesExposed data or content that can be read by the clientFiles, database records, API responses
PromptsPre-defined prompt templates with dynamic argumentsCode review prompt, summarization template
SamplingServer requests LLM generation from the client (server-to-client)Agentic loops, delegated reasoning
📝

note

MCP uses JSON-RPC 2.0 as its message format. All messages are structured as JSON-RPC requests, responses, or notifications. This makes the protocol simple to implement and debug — every message is a valid JSON object with a predictable structure.
Transport Layer

MCP defines two transport mechanisms. The choice depends on whether the server runs locally or remotely.

stdio Transport

The server runs as a child process of the client, communicating over standard input/output. This is the simplest deployment model — the client spawns the server, sends JSON-RPC messages on stdin, and reads responses from stdout.

stdio_server.py
Python
1# stdio server: reads from stdin, writes to stdout
2import sys
3import json
4
5def handle_request(request: dict) -> dict:
6 method = request.get("method")
7 if method == "tools/list":
8 return {
9 "jsonrpc": "2.0",
10 "id": request["id"],
11 "result": {
12 "tools": [{
13 "name": "get_weather",
14 "description": "Get current weather",
15 "inputSchema": {
16 "type": "object",
17 "properties": {
18 "location": {"type": "string"}
19 }
20 }
21 }]
22 }
23 }
24 # ... handle other methods
25
26for line in sys.stdin:
27 request = json.loads(line)
28 response = handle_request(request)
29 sys.stdout.write(json.dumps(response) + "\n")
30 sys.stdout.flush()

SSE Transport (HTTP)

For remote servers, MCP uses Server-Sent Events over HTTP. The client connects to a URL, receives events on an SSE stream, and sends requests via HTTP POST. This enables MCP to work across network boundaries.

sse_server.py
Python
1# SSE server using FastAPI
2from fastapi import FastAPI, Request
3from fastapi.responses import StreamingResponse
4import asyncio
5import json
6
7app = FastAPI()
8
9@app.post("/mcp")
10async def mcp_endpoint(request: Request):
11 body = await request.json()
12 method = body.get("method")
13
14 if method == "tools/call":
15 tool_name = body["params"]["name"]
16 args = body["params"]["arguments"]
17 result = await execute_tool(tool_name, args)
18 return {"jsonrpc": "2.0",
19 "id": body["id"],
20 "result": {"content": result}}
21 # ... other methods
22
23@app.get("/sse")
24async def sse_endpoint():
25 async def event_stream():
26 while True:
27 # Send server events (e.g., tool responses)
28 yield f"data: {json.dumps({'type': 'ping'})}\n\n"
29 await asyncio.sleep(30)
30 return StreamingResponse(event_stream(),
31 media_type="text/event-stream")

best practice

Use stdio for local development and single-user tools. It is simpler, has lower latency, and does not require network configuration. Use SSE for multi-user, remote, or cloud-hosted servers where clients connect over the network.
Tool Definitions

Tools are the primary way MCP servers expose executable functionality. Each tool has a name, description, and JSON Schema input definition. The client discovers available tools via the tools/list method and invokes them via tools/call.

mcp_tools_server.py
Python
1# Complete MCP server with tools
2from mcp.server import Server, NotificationOptions
3from mcp.server.models import InitializationOptions
4import mcp.server.stdio
5
6async def main():
7 server = Server("data-tools")
8
9 @server.list_tools()
10 async def list_tools():
11 return [
12 {
13 "name": "query_database",
14 "description": "Run SQL query on the analytics DB",
15 "inputSchema": {
16 "type": "object",
17 "properties": {
18 "sql": {
19 "type": "string",
20 "description": "SQL query to execute"
21 },
22 "limit": {
23 "type": "integer",
24 "default": 100
25 }
26 },
27 "required": ["sql"]
28 }
29 },
30 {
31 "name": "fetch_url",
32 "description": "Fetch content from a URL",
33 "inputSchema": {
34 "type": "object",
35 "properties": {
36 "url": {"type": "string"},
37 "format": {
38 "type": "string",
39 "enum": ["text", "html", "markdown"]
40 }
41 },
42 "required": ["url"]
43 }
44 }
45 ]
46
47 @server.call_tool()
48 async def call_tool(name: str, arguments: dict):
49 if name == "query_database":
50 results = await run_query(arguments["sql"])
51 return {"content": str(results)}
52 elif name == "fetch_url":
53 content = await fetch_url(arguments["url"])
54 return {"content": content}
55
56 async with mcp.server.stdio.stdio_server() as (read, write):
57 await server.run(read, write,
58 InitializationOptions(
59 server_name="data-tools",
60 server_version="1.0.0"
61 ))
62
63if __name__ == "__main__":
64 import asyncio
65 asyncio.run(main())

info

Use descriptive tool names and detailed descriptions. The LLM uses these to decide which tool to invoke. A tool named db with no description is far less useful to the model than query_database with a clear description and well-structured parameters.
Resources and Resource Templates

Resources expose data to the LLM. Unlike tools (which perform actions), resources are read-only data sources. They can be static (like a documentation file) or dynamic (like the contents of a specific database record identified by a URI template).

mcp_resources.py
Python
1# Resource exposure with templates
2@server.list_resources()
3async def list_resources():
4 return [
5 {
6 "uri": "docs://mcp",
7 "name": "MCP Documentation",
8 "mimeType": "text/markdown"
9 },
10 {
11 "uri": "docs://rag",
12 "name": "RAG Guide",
13 "mimeType": "text/markdown"
14 }
15 ]
16
17@server.read_resource()
18async def read_resource(uri: str):
19 if uri.startswith("docs://"):
20 doc_name = uri.replace("docs://", "")
21 content = load_documentation(doc_name)
22 return {
23 "contents": [{
24 "uri": uri,
25 "mimeType": "text/markdown",
26 "text": content
27 }]
28 }
29
30# Resource templates for dynamic access
31@server.list_resource_templates()
32async def list_templates():
33 return [
34 {
35 "uriTemplate":
36 "analytics://reports/{report_id}",
37 "name": "Analytics Report",
38 "description":
39 "Access a specific analytics report by ID",
40 "mimeType": "application/json"
41 }
42 ]
📝

note

Resources in MCP follow the URI scheme pattern. A resource template like analytics://reports/{report_id} allows the LLM to dynamically construct URIs to access specific data. The client resolves templates before reading.
Prompt Templates

MCP servers can expose reusable prompt templates. These are predefined prompts with dynamic arguments that clients can present as ready-to-use interactions. Prompt templates are particularly useful for standardizing common AI workflows.

mcp_prompts.py
Python
1# Prompt template definitions
2@server.list_prompts()
3async def list_prompts():
4 return [
5 {
6 "name": "code_review",
7 "description":
8 "Review code changes and suggest improvements",
9 "arguments": [
10 {
11 "name": "code",
12 "description":
13 "The code snippet to review",
14 "required": True
15 },
16 {
17 "name": "language",
18 "description":
19 "Programming language",
20 "required": False
21 }
22 ]
23 },
24 {
25 "name": "summarize_doc",
26 "description":
27 "Summarize a document at a specified level",
28 "arguments": [
29 {
30 "name": "document",
31 "description": "Document text to summarize",
32 "required": True
33 },
34 {
35 "name": "style",
36 "description": "Summary style: brief/detailed",
37 "required": False
38 }
39 ]
40 }
41 ]
42
43@server.get_prompt()
44async def get_prompt(name: str, arguments: dict):
45 if name == "code_review":
46 lang = arguments.get("language", "unknown")
47 return {
48 "messages": [
49 {
50 "role": "user",
51 "content": {
52 "type": "text",
53 "text": f"Review this {lang} code:\n"
54 f"\n{arguments['code']}"
55 }
56 }
57 ]
58 }

info

Prompt templates are especially useful for teams. Standardize your code review, documentation, and analysis prompts as MCP prompt templates so every team member and every LLM client gets consistent, high-quality interactions.
Client Implementation

An MCP client connects to a server, discovers its capabilities, and invokes tools or reads resources. Here is a minimal client implementation.

mcp_client.py
Python
1import asyncio
2import json
3import subprocess
4import uuid
5
6class MCPClient:
7 def __init__(self, command: list[str]):
8 self.process = subprocess.Popen(
9 command,
10 stdin=subprocess.PIPE,
11 stdout=subprocess.PIPE,
12 stderr=subprocess.PIPE,
13 text=True
14 )
15 self.pending = {}
16
17 async def send_request(self, method: str,
18 params: dict = None) -> dict:
19 request_id = str(uuid.uuid4())
20 request = {
21 "jsonrpc": "2.0",
22 "id": request_id,
23 "method": method,
24 "params": params or {}
25 }
26 self.process.stdin.write(
27 json.dumps(request) + "\n"
28 )
29 self.process.stdin.flush()
30
31 response = json.loads(
32 self.process.stdout.readline()
33 )
34 return response.get("result")
35
36 async def list_tools(self) -> list:
37 result = await self.send_request("tools/list")
38 return result["tools"]
39
40 async def call_tool(self, name: str,
41 arguments: dict) -> dict:
42 result = await self.send_request("tools/call", {
43 "name": name,
44 "arguments": arguments
45 })
46 return result
47
48 def close(self):
49 self.process.terminate()
50 self.process.wait()
51
52# Usage
53async def main():
54 client = MCPClient(["python", "server.py"])
55 tools = await client.list_tools()
56 print(f"Available tools: {tools}")
57 result = await client.call_tool("get_weather",
58 {"location": "Tokyo"})
59 print(f"Result: {result}")
60 client.close()
61
62asyncio.run(main())

best practice

Always handle connection errors and server crashes gracefully. A production MCP client should implement reconnection logic, request timeouts, and proper cleanup. The close() method should be called in a finally block or context manager.
Security Considerations

MCP servers execute arbitrary code on behalf of LLMs. This creates significant security surface area that must be addressed at the protocol, transport, and application levels.

User Confirmation

All tool invocations require explicit user approval. The host must never auto-execute tools. Present the tool name, arguments, and a clear description of what will happen before asking for confirmation.

Data Access Control

Resources should respect the host's authentication and authorization boundaries. An MCP server for file access must not allow reading files outside the allowed directory (path traversal protection).

Input Validation

Every tool parameter must be validated and sanitized. An LLM may inject SQL, shell commands, or malicious paths through tool arguments. Never pass user-generated arguments directly to system calls.

Transport Security

Remote MCP connections over SSE must use TLS (HTTPS). Authentication tokens should never be hardcoded — use environment variables or secure credential stores. Implement rate limiting to prevent abuse.

Sandboxing

Run MCP servers in sandboxed environments (containers, restricted users, seccomp profiles). A compromised MCP server should not be able to access the host system beyond its intended scope.

warning

The most dangerous attack vector in MCP is prompt injection. A malicious resource could inject instructions into the LLM's context that cause it to invoke dangerous tools. Always use system-level safeguards: tool confirmation dialogs, output filtering, and least-privilege server design.
$Blueprint — Engineering Documentation·Section ID: AI-MCP·Revision: 1.0