Introduction to the Model Context Protocol (MCP)
The Model Context Protocol (MCP) is an open standard introduced by Anthropic that defines a unified way for AI assistants like Claude to connect with external data sources, tools, and services. Think of MCP as the "USB-C for AI applications" — a single, well-defined interface that lets a language model reach into your filesystem, databases, APIs, and internal tools without bespoke integrations for every single connection.
Before MCP, extending Claude with custom tools meant writing provider-specific function-calling schemas, manually serializing tool results, and managing the entire request/response lifecycle inside your application code. MCP abstracts this away by defining a client-server architecture where a lightweight MCP server exposes capabilities — tools, resources, and prompts — and an MCP client (such as the Claude Desktop app or your own application) consumes them through a standardized JSON-RPC transport.
Why MCP Matters
- Standardization: Write a tool once, plug it into any MCP-compatible client. No more rewriting integrations per provider.
- Separation of concerns: Your application logic lives in the MCP server; the LLM orchestration lives in the client. Clean boundaries.
- Security by design: Servers run locally or in controlled environments, and clients explicitly approve which tools are exposed to the model.
- Composability: A single client can connect to multiple MCP servers simultaneously, combining a filesystem server, a GitHub server, and a custom internal API server in one conversation.
- Ecosystem growth: Anthropic and the community maintain reference servers for common services (Slack, GitHub, Postgres, Google Drive, etc.), so you often don't need to build from scratch.
Core Concepts of MCP
Understanding MCP requires familiarity with three primitives that a server can expose:
1. Tools
Tools are executable functions the model can call to perform actions or retrieve computed data. Each tool has a name, a description, and a JSON Schema describing its input parameters. When Claude decides a tool is relevant, it emits a tool-call request; the client forwards it to the server, executes the function, and returns the result back to the model.
2. Resources
Resources are read-only data sources identified by URIs (e.g., file:///project/README.md or postgres://users/schema). They let the model pull in context on demand. Unlike tools, resources are passive — they don't perform computation, they just return content.
3. Prompts
Prompts are reusable, parameterized templates that servers can publish. A client can list available prompts and let users invoke them, providing a structured way to bootstrap common workflows (e.g., "Analyze this repository's architecture").
Transports
MCP servers communicate with clients over a JSON-RPC 2.0 message layer. The two most common transports are:
- stdio: The client spawns the server as a subprocess and communicates over standard input/output. Ideal for local tools.
- HTTP + SSE: The server runs as a remote service, and the client connects over HTTP with Server-Sent Events for streaming. Ideal for shared or cloud-hosted tools.
Setting Up Your Environment
The fastest way to build an MCP server is with the official TypeScript or Python SDKs. This tutorial uses Python because of its concise syntax and broad appeal, but the concepts transfer directly to TypeScript.
First, create a virtual environment and install the SDK:
python -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
pip install "mcp[cli]"
Verify the installation:
mcp --version
You'll also need the Claude Desktop app if you want to test your server interactively without writing a custom client. The desktop app natively supports MCP server configuration.
Building Your First MCP Server
Let's build a practical MCP server that gives Claude the ability to query a fictional issue tracker. The server will expose two tools: one to list issues for a project, and one to fetch a single issue by ID. We'll back it with an in-memory data store for simplicity, but you could swap in a real database or REST API call.
Create a file named issue_server.py:
from mcp.server.fastmcp import FastMCP
from typing import Optional
mcp = FastMCP("issue-tracker")
# In-memory data store. In production, replace with a real backend.
ISSUES = {
"1": {"id": "1", "title": "Login page crashes on Safari", "status": "open", "priority": "high"},
"2": {"id": "2", "title": "Add dark mode toggle", "status": "in_progress", "priority": "medium"},
"3": {"id": "3", "title": "Docs missing API reference", "status": "open", "priority": "low"},
}
@mcp.tool()
def list_issues(status: Optional[str] = None) -> list[dict]:
"""List all issues in the tracker. Optionally filter by status
(e.g., 'open', 'in_progress', 'closed')."""
if status:
return [i for i in ISSUES.values() if i["status"] == status]
return list(ISSUES.values())
@mcp.tool()
def get_issue(issue_id: str) -> dict:
"""Fetch a single issue by its unique ID. Returns the full issue record."""
issue = ISSUES.get(issue_id)
if issue is None:
return {"error": f"Issue {issue_id} not found"}
return issue
if __name__ == "__main__":
mcp.run(transport="stdio")
The FastMCP class handles JSON-RPC wiring, schema generation, and tool dispatch automatically. The docstrings you write become the descriptions Claude reads when deciding whether to call a tool — so write them carefully and clearly.
How Schema Generation Works
When the client connects, it requests the server's tool list. FastMCP inspects each decorated function's signature and type hints to produce a JSON Schema. For list_issues, the generated schema looks roughly like this:
{
"name": "list_issues",
"description": "List all issues in the tracker. Optionally filter by status ...",
"inputSchema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"description": "Optional status filter"
}
}
}
}
Claude uses this schema to construct valid tool calls. This is why precise type hints matter — str vs int vs list[str] directly shapes what the model is allowed to send.
Connecting the Server to Claude Desktop
To let Claude Desktop use your server, register it in the app's configuration file. The file location varies by OS:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
Edit (or create) the file to include your server:
{
"mcpServers": {
"issue-tracker": {
"command": "python",
"args": ["/absolute/path/to/issue_server.py"]
}
}
}
Use absolute paths — relative paths often fail because the desktop app may not launch from the directory you expect. After saving the file, fully quit and restart Claude Desktop. You should see a small hammer icon indicating tools are available. Try prompting:
What issues are currently open in the tracker?
Claude will call list_issues with status="open", receive the JSON result, and compose a natural-language answer.
Adding Resources and Prompts
Tools are great for actions, but sometimes you want to expose static or semi-static context. Let's extend the server with a resource that returns project metadata and a prompt that scaffolds a triage workflow.
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("issue-tracker")
PROJECT_META = {
"name": "Acme Web App",
"owner": "platform-team",
"repo": "acme/acme-web",
}
@mcp.resource("project://meta")
def get_project_meta() -> dict:
"""Return high-level metadata about the tracked project."""
return PROJECT_META
@mcp.prompt()
def triage_open_issues(focus: str = "high priority") -> str:
"""Generate a prompt to triage open issues, optionally focused on a priority."""
return f"""You are a project manager reviewing open issues.
Focus on {focus} items. Use the list_issues tool to fetch open issues,
then summarize the top three that need immediate attention and propose
next steps for each."""
Resources are addressed by URI. A client can request project://meta at any time to inject that context into the conversation. Prompts appear in the client's prompt picker and, when invoked, expand into a full message that can itself reference tools — a powerful way to encode repeatable workflows.
Writing a Custom MCP Client
If you're building your own application rather than using Claude Desktop, you need a client that connects to an MCP server, lists its tools, and relays tool calls between Claude and the server. Here's a minimal example using the Python SDK's client API together with the Anthropic SDK:
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from anthropic import Anthropic
SERVER_SCRIPT = "issue_server.py"
async def main():
# 1. Spawn the MCP server as a subprocess over stdio.
params = StdioServerParameters(
command="python",
args=[SERVER_SCRIPT],
)
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
# 2. Discover available tools.
tools_result = await session.list_tools()
tool_specs = [
{
"name": t.name,
"description": t.description,
"input_schema": t.inputSchema,
}
for t in tools_result.tools
]
# 3. Send a user message to Claude with the tools attached.
anthropic = Anthropic()
messages = [{"role": "user", "content": "List all open issues."}]
response = anthropic.messages.create(
model="claude-sonnet-4-5-20250929",
max_tokens=1024,
tools=tool_specs,
messages=messages,
)
# 4. Handle any tool calls Claude makes.
while response.stop_reason == "tool_use":
tool_block = next(
b for b in response.content if b.type == "tool_use"
)
result = await session.call_tool(
tool_block.name, tool_block.input
)
tool_output = result.content[0].text
messages.append({"role": "assistant", "content": response.content})
messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": tool_block.id,
"content": tool_output,
}],
})
response = anthropic.messages.create(
model="claude-sonnet-4-5-20250929",
max_tokens=1024,
tools=tool_specs,
messages=messages,
)
# 5. Print the final answer.
print(response.content[0].text)
if __name__ == "__main__":
asyncio.run(main())
This loop — send message, check for tool calls, execute them via the MCP session, feed results back — is the heart of any agentic application built on MCP. The protocol handles serialization and transport; you focus on orchestration.
Best Practices
Write Exceptional Tool Descriptions
The tool description is the single most important factor in whether Claude calls your tool correctly. Describe what the tool does, when it should be used, and any constraints on its inputs. Avoid vague descriptions like "Gets data." Instead: "Retrieves a paginated list of GitHub pull requests for a given repository, filtered by state. Use this when the user asks about open PRs, review status, or merge readiness."
Keep Tools Focused and Composable
Prefer many small, single-purpose tools over one giant tool with a dozen parameters. Claude reasons more reliably about narrow tools and can chain them together. A search_issues tool plus a get_issue tool is better than a single query_issues_with_full_details monster.
Validate and Sanitize Inputs
Never trust that the model will send perfectly formed input. Validate types, ranges, and permissions inside every tool implementation. If a tool writes to a database or calls a mutating API, require explicit confirmation in the client layer before executing.
Return Structured, Information-Rich Results
Tool results should give Claude enough context to continue the conversation without a second round trip. Return JSON with meaningful field names, include error messages when something goes wrong, and avoid raw binary blobs — serialize them as base64 or a URL reference instead.
Scope Resources and Tools Appropriately
Only expose what the user actually needs. A filesystem MCP server that grants access to the entire home directory is a security risk. Use allowlists, restrict paths, and prefer read-only resources unless mutation is explicitly required.
Handle Long-Running Operations Gracefully
If a tool may take more than a few seconds, consider returning an intermediate status and exposing a separate "check status" tool. MCP supports progress notifications, so you can stream updates to the client while work proceeds.
Test Servers Independently
The MCP CLI includes an inspector that lets you list tools, call them with sample inputs, and inspect responses without involving Claude at all. Run mcp dev issue_server.py to launch a local web UI for interactive testing. Catching schema bugs here is far faster than debugging through a full LLM round trip.
Conclusion
The Model Context Protocol turns the messy problem of "how does Claude talk to my stuff" into a clean, standardized contract. By exposing tools, resources, and prompts through a small server process, you give any MCP-compatible client — Claude Desktop, your own application, or future agents — a uniform way to access your data and services. Start small with a single tool, validate it with the MCP inspector, wire it into Claude Desktop, and iterate. As your server grows, keep tools focused, descriptions precise, and security boundaries tight. With those foundations in place, MCP becomes a durable layer that lets your AI integrations scale alongside your real-world systems rather than becoming a tangle of one-off glue code.