Understanding the Model Context Protocol — how it standardizes tool communication between AI models and external services.
In November 2024, Anthropic released the Model Context Protocol (MCP) as an open standard, publishing a full specification and reference implementations in Python and TypeScript. Within weeks, companies including Block (formerly Square), Apollo, and Zed had announced integrations. The core problem MCP solved was one that every enterprise AI team had hit independently: every tool integration required custom glue code, and none of it was reusable across models. A filesystem connector built for Claude couldn't be used with GPT-4 without a complete rewrite. MCP changed that by defining a single protocol that any client and any server could implement once.
By Q1 2025, over 1,000 open-source MCP servers had been published on GitHub. The Anthropic-maintained model-context-protocol organization hosted reference servers for filesystem access, Git, databases, web search, and cloud APIs — all interoperable with any compliant MCP host.
The Model Context Protocol is a client-server protocol that standardizes how AI models request and receive context from external data sources and tools. Before MCP, every AI application that needed to call a tool — search a database, read a file, call an API — had to implement a bespoke integration. This created what Anthropic called the "M×N problem": M different models times N different tools meant M×N unique integrations to build and maintain.
MCP collapses this to M+N. Any MCP-compliant client (a model host like Claude Desktop, or a custom application) can connect to any MCP-compliant server (a tool provider) without custom code. The protocol defines three core primitives that servers can expose: Tools (callable functions the model can invoke), Resources (data the model can read), and Prompts (pre-defined prompt templates). This separation of concerns is fundamental — a server decides what it exposes; the client decides how to present those capabilities to the model.
MCP is architecturally similar to the Language Server Protocol (LSP) that standardized IDE integrations for programming languages in 2016. LSP eliminated the "every editor needs a plugin for every language" problem. MCP eliminates the equivalent problem for AI tool use. Both protocols succeeded for the same reason: they defined a minimal, transport-agnostic interface that was easy to implement on both sides.
The protocol operates over a transport layer — currently either stdio (standard input/output, for local processes) or HTTP with Server-Sent Events (SSE, for remote services). The transport choice is an implementation detail; the message format and capability negotiation are the same regardless. This design means an MCP server written as a local Python script can be deployed as a remote HTTPS service with only transport-layer changes.
MCP defines three distinct roles in every interaction. Understanding these roles precisely is essential before building anything.
The lifecycle of an MCP connection follows a defined sequence: the client initiates a connection and sends an initialize request containing its protocol version and capabilities. The server responds with its own version and the capabilities it supports. After this handshake, the client sends an initialized notification and the connection is live. From this point, the model can request a list of available tools, call those tools with arguments, and receive structured results.
MCP uses JSON-RPC 2.0 as its message format. Every request has a method name, an optional params object, and an id for correlation. Responses carry either a result or an error. Notifications (messages without an id) are fire-and-forget. If you've worked with JSON-RPC before, the message format will be immediately familiar; MCP's contribution is the specific methods and capability negotiation on top of it.
Each of MCP's three server-side primitives serves a distinct purpose in how the model interacts with the external world.
Tools are the most action-oriented primitive. A tool has a name, a description (which the model reads to decide whether to call it), and a JSON Schema defining its input parameters. When the model calls a tool, the server executes the corresponding logic — running a shell command, querying a database, calling an external API — and returns a result. Tools are the mechanism behind statements like "I'll search the web for that" or "Let me check the current stock price."
Resources represent data that the model can read but not necessarily modify. A resource has a URI (like file:///home/user/notes.txt or postgres://localhost/mydb/tables/users), a MIME type, and content. Resources can be static (a fixed file) or dynamic (a live query result). The host decides which resources to inject into the model's context and when.
Prompts are pre-defined prompt templates that servers can expose. This is less commonly discussed but powerful: a server can define a structured workflow — "analyze this codebase for security issues" or "summarize this document in three bullet points" — that users can invoke by name. Prompts can accept arguments that get interpolated into the template.
3 questions — free, untracked, retake anytime.
Explore MCP's three-layer architecture and primitive types with a live AI tutor.
In this lab you'll work through concrete architectural questions about MCP. The AI tutor will open with a design scenario — respond with your analysis, then continue the conversation to deepen your understanding.
initialize request and response look like in practice.Installing, configuring, and connecting MCP servers to Claude Desktop and custom hosts — from first install to verified tool call.
In early 2025, the engineering team at Sourcegraph documented their MCP integration publicly. They connected their Cody AI assistant to an MCP server exposing code search and repository navigation tools. The setup took one afternoon: they wrote a server in TypeScript using the official MCP SDK, registered it in Claude Desktop's claude_desktop_config.json, and immediately had a working integration. Their blog post noted that the same server configuration worked without modification when they later wired it into their own custom host application — exactly the portability MCP was designed to deliver.
The contrast with their previous approach was stark: their prior Cody tool integration required about 800 lines of bespoke adapter code per model provider. The MCP server was 200 lines total and connected to four different host applications.
Anthropic maintains official MCP SDKs for Python and TypeScript. Both are available on their respective package registries and follow the same API conventions. For Python: pip install mcp. For TypeScript/Node: npm install @modelcontextprotocol/sdk. The minimum viable development environment is a Python 3.10+ or Node 18+ installation, the SDK, and Claude Desktop (for testing via stdio transport).
The MCP Inspector — available via npx @modelcontextprotocol/inspector — is the fastest way to debug a server without involving a full host application. It provides a web UI where you can directly call tools, list resources, and inspect the raw JSON-RPC messages in both directions. Use it before you ever open Claude Desktop.
The Python SDK uses a decorator-based API that makes tool registration concise. A minimal server that exposes one tool looks like this conceptually: import FastMCP, instantiate it with a name, decorate a function with @mcp.tool(), and call mcp.run(). FastMCP (now part of the official SDK) infers the tool's JSON Schema from the function's type annotations — you don't write schemas by hand for basic cases.
Claude Desktop reads MCP server configurations from a JSON file at a platform-specific location. On macOS this is ~/Library/Application Support/Claude/claude_desktop_config.json; on Windows it is %APPDATA%\Claude\claude_desktop_config.json. The configuration structure is straightforward: a top-level mcpServers object where each key is the server's display name and each value contains the command to launch it.
A typical entry for a Python server named "my-tools" running via uv (the fast Python package manager) looks like: {"command": "uv", "args": ["run", "my_server.py"]}. For a Node server: {"command": "node", "args": ["/absolute/path/to/server.js"]}. Restart Claude Desktop after editing the config file. In the Claude Desktop UI, click the hammer icon (🔨) to verify the server connected and its tools are listed.
Claude Desktop launches servers using the system PATH at the time it starts — not the PATH in your terminal session. If your server depends on a virtual environment or a locally-installed tool, use absolute paths in the config and specify the full path to the Python or Node executable. This is the most common cause of "server failed to start" errors in first-time setups.
Environment variables can be passed to server processes via an env object in the server config. This is the standard way to pass API keys and database credentials. Never hardcode secrets in your server source code — put them in the Claude Desktop config's env section for local development, and use proper secrets management for production deployments.
After connecting a server, there are three verification steps practitioners use. First, check the Claude Desktop logs — on macOS, these are at ~/Library/Logs/Claude/mcp*.log. Each server gets its own log file. Look for the initialized notification in the log to confirm the handshake completed.
Second, open a conversation in Claude Desktop and ask Claude what tools it has available. Claude will list the tools from all connected servers by name and description. If your tool appears, the connection is working. Third, ask Claude to call the tool with a simple input and observe the result. This end-to-end test confirms the tool's execution logic is reachable from the model.
~/Library/Logs/Claude/mcp-server-{name}.log%APPDATA%\Claude\logs\mcp-server-{name}.lognpx @modelcontextprotocol/inspector python my_server.py for real-time message inspectionping tool that returns "pong" — the fastest way to confirm end-to-end connectivity during development3 questions — free, untracked, retake anytime.
%APPDATA%\Claude\.~/Library/Application Support/Claude/claude_desktop_config.json.Walk through a real MCP server setup with a tutor that knows every config detail.
In this lab the tutor will immediately present a realistic setup scenario — a partially broken MCP configuration — and guide you through diagnosing it. Use this to internalize the debugging workflow.
Writing production-quality MCP tools — schema design, error handling, input validation, and building tools the model can actually use reliably.
In March 2025, the team behind the open-source MCP server mcp-server-kubernetes published a retrospective on their tool design decisions. Their initial implementation returned raw Kubernetes API responses — complete JSON objects with dozens of fields. Claude would reliably use the tools but produce responses that were too verbose because the model was processing unnecessary data. They rewrote the tool responses to return only the fields relevant to the operation (pod name, status, restart count, node) and documented that token-efficient tool responses directly improved the quality of the model's reasoning about Kubernetes state. The lesson they highlighted: tool output is part of the model's context window; every byte matters.
A tool's JSON Schema definition does double duty: it tells the MCP client what parameters are valid (for validation), and it tells the model what parameters are available and what they mean (for decision-making). Writing good schemas is the single highest-leverage activity in MCP tool development, because a well-described tool gets called correctly and a poorly-described one gets called wrong.
The description field on the tool itself should answer: when should I call this? Be specific about preconditions and side effects. "Reads the contents of a file" is good. "Use this tool when you need to read a file" is worse (circular). "Reads the text content of a local file at the given absolute path. Returns an error if the file does not exist or is binary." is better — it specifies the input type, the failure modes, and the output type.
Each parameter's description field in your JSON Schema is read by the model. Include the expected format, units, valid ranges, and any constraints. For a date parameter, write "ISO 8601 date string, e.g. 2024-03-15" not just "a date". The model will produce correctly-formatted arguments more reliably when the description is precise.
Use enum constraints wherever possible. If a parameter only accepts "read", "write", or "append", define it as an enum — not a free-form string with examples. Enums constrain the model's output space and eliminate an entire class of invalid tool calls. Similarly, use minimum/maximum for numeric ranges and pattern for string formats like UUIDs or file extensions.
MCP tools can return two types of results: a successful content array, or a tool error. The distinction matters for how the model handles the response. A successful result with an error message embedded in the text is ambiguous — the model may not recognize it as an error. A proper tool error (setting isError: true in the response) signals clearly that the tool call failed and the model should reason about why and potentially retry with different arguments or take a different approach.
There are three categories of errors to handle explicitly. First, expected errors: a file doesn't exist, a query returns no results, an API rate limit is hit. These should be returned as tool errors with a specific, actionable message. Second, validation errors: the model passed an argument with the wrong type or an out-of-range value. Return these as tool errors immediately, before any side effects occur. Third, unexpected errors: crashes, network failures, unhandled exceptions. Catch these at the top level of your tool handler and return a sanitized error message — never let the raw exception propagate to the protocol layer.
text, image, or resourceisError: true at the response level, not just in the text contentIn Python with the official MCP SDK, wrap your tool handler body in a try/except. On success, return [TextContent(type="text", text=result)]. On a known error, raise McpError with an appropriate error code. On an unexpected exception, log it server-side and return a generic error to the model — don't expose internal stack traces or sensitive configuration details in tool error messages.
MCP servers are trusted components that execute with real system privileges — they can write files, query databases, call external APIs. This means input validation is a security requirement, not just good practice. Treat every tool call argument as untrusted input, even though it comes from the model. The model's arguments can be influenced by adversarial content in the data the model was given — a technique called prompt injection. A malicious document could instruct the model to call a tool with arguments like ../../../etc/passwd or ; rm -rf /.
The standard defensive patterns apply: validate and sanitize all path arguments (resolve and confirm they're within allowed directories), use parameterized queries for all database operations (never string-interpolate model-provided values into SQL), and apply allowlists rather than blocklists for permitted operations. The Anthropic documentation explicitly warns about prompt injection in MCP server design — it's a first-class concern, not an edge case.
For filesystem tools specifically: resolve the absolute path of any user-provided path argument and confirm it is a child of an explicitly permitted base directory before any file operation. For shell execution tools: avoid them entirely if possible; if you must have them, use a subprocess with a fixed command template and only allow model-controlled values for a small set of pre-validated arguments.
Use the AI below to explore Lesson 3 concepts in depth. Challenge assumptions and work through scenarios.
This lesson explores l4: mcp-compatible agents — examining the key principles, real-world applications, and implications for practitioners working in this domain.
Understanding this topic requires both theoretical grounding and practical awareness of how these concepts manifest in deployed systems. The frameworks covered in earlier lessons provide the foundation; this lesson connects them to implementation reality.
The transition from theory to practice reveals challenges that pure conceptual frameworks don't capture. Real-world deployment introduces constraints, trade-offs, and edge cases that demand nuanced judgment rather than rigid rule-following.
Effective practitioners in this space develop the ability to reason across multiple frameworks simultaneously, recognizing when different perspectives apply and how to resolve conflicts between competing priorities.
As this field continues to evolve, the principles covered in this module will remain foundational even as specific technologies and implementations change. The ability to think critically about these topics — rather than simply memorizing current best practices — is what separates effective practitioners from those who merely follow checklists.
Use the AI below to explore the concepts from Lesson 4 in depth. Ask questions, challenge assumptions, and work through practical scenarios related to l4: mcp-compatible agents.