Tool use lets Claude call functions you define in your API request. MCP goes further — it lets Claude connect to external servers that provide tools, data, and prompts. MCP is the bridge between Claude and the rest of your infrastructure.

This is Article 13 in the Claude AI — From Zero to Power User series. You should have completed Article 8: Tool Use before this article.

By the end of this article, you will understand MCP architecture, use pre-built MCP servers, and build your own MCP server in both Python and TypeScript.


What is MCP?

MCP (Model Context Protocol) is an open protocol created by Anthropic for connecting AI models to external tools and data. Think of it as a USB standard for AI — any MCP-compatible model can connect to any MCP server.

Before MCP, every AI tool integration was custom. You had to write tool definitions, handle execution, and manage connections manually. MCP standardizes this.

Who Uses MCP

MCP is not limited to Claude. It has been adopted by:

  • VS Code — Copilot uses MCP servers
  • JetBrains IDEs — MCP support in AI Assistant
  • Cursor — Full MCP support
  • Zed — Built-in MCP integration
  • Many other AI tools and frameworks

An MCP server you build for Claude also works with these tools.


MCP Architecture

MCP has three layers:

Host (Claude)  →  Client  →  Server  →  External Service
  • Host — The AI application (Claude Desktop, Claude Code, your app)
  • Client — Manages the connection between host and server
  • Server — Provides tools, resources, and prompts to the AI

Three Primitives

MCP servers expose three types of capabilities:

PrimitiveWhat It DoesExample
ToolsFunctions the AI can call“Search the database”, “Create a ticket”
ResourcesData the AI can read“Contents of file X”, “Current config”
PromptsReusable prompt templates“Code review template”, “Bug report format”

Tools are the most commonly used. Resources and prompts are useful for providing context without requiring the AI to call a function.


Using Pre-Built MCP Servers

The MCP ecosystem has hundreds of pre-built servers. Here are some popular ones:

ServerWhat It Does
@modelcontextprotocol/server-githubGitHub issues, PRs, repos
@modelcontextprotocol/server-postgresQuery PostgreSQL databases
@modelcontextprotocol/server-slackRead and send Slack messages
@modelcontextprotocol/server-filesystemFile system access
@modelcontextprotocol/server-puppeteerBrowser automation
@modelcontextprotocol/server-google-driveGoogle Drive files

Installing in Claude Desktop

Add MCP servers to your Claude Desktop config file:

macOS: ~/Library/Application Support/Claude/claude_desktop_config.json

{
  "mcpServers": {
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": {
        "GITHUB_TOKEN": "ghp_your_token_here"
      }
    },
    "postgres": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-postgres"],
      "env": {
        "DATABASE_URL": "postgresql://user:pass@localhost:5432/mydb"
      }
    }
  }
}

Restart Claude Desktop after editing the config. The servers start automatically when Claude Desktop launches.

Installing in Claude Code

# Add an MCP server to Claude Code
claude mcp add github -- npx -y @modelcontextprotocol/server-github

# List installed servers
claude mcp list

# Remove a server
claude mcp remove github

Once installed, you can ask Claude to use the tools provided by the server:

"Create a GitHub issue in my project for the login bug we discussed"
"Query the database for all users who signed up this month"
"Search Slack for messages about the deployment"

Claude discovers the available tools from each MCP server and uses them as needed.


Transport Mechanisms

MCP servers communicate with clients via two transport methods:

stdio (Standard Input/Output)

The server runs as a subprocess. Communication happens through stdin/stdout.

  • Best for: Local servers, CLI tools, development
  • Used by: Claude Desktop, Claude Code

HTTP + SSE (Server-Sent Events)

The server runs as an HTTP server. Communication happens via HTTP requests and SSE streams.

  • Best for: Remote servers, production deployments, shared servers
  • Used by: Web applications, multi-user deployments

Most pre-built servers support stdio. For production deployments, HTTP+SSE is more practical.


Building Your First MCP Server: Python

Let us build a simple MCP server that manages a to-do list.

Python

# todo_server.py
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent

# In-memory storage
todos: list[dict] = []

server = Server("todo-server")

@server.list_tools()
async def list_tools() -> list[Tool]:
    return [
        Tool(
            name="add_todo",
            description="Add a new to-do item",
            inputSchema={
                "type": "object",
                "properties": {
                    "title": {
                        "type": "string",
                        "description": "The to-do item title"
                    },
                    "priority": {
                        "type": "string",
                        "enum": ["low", "medium", "high"],
                        "description": "Priority level"
                    }
                },
                "required": ["title"]
            }
        ),
        Tool(
            name="list_todos",
            description="List all to-do items",
            inputSchema={
                "type": "object",
                "properties": {}
            }
        ),
        Tool(
            name="complete_todo",
            description="Mark a to-do item as complete by index",
            inputSchema={
                "type": "object",
                "properties": {
                    "index": {
                        "type": "integer",
                        "description": "The to-do item index (0-based)"
                    }
                },
                "required": ["index"]
            }
        )
    ]

@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
    if name == "add_todo":
        todo = {
            "title": arguments["title"],
            "priority": arguments.get("priority", "medium"),
            "completed": False
        }
        todos.append(todo)
        return [TextContent(type="text", text=f"Added: {todo['title']} ({todo['priority']} priority)")]

    elif name == "list_todos":
        if not todos:
            return [TextContent(type="text", text="No to-do items yet.")]

        lines = []
        for i, todo in enumerate(todos):
            status = "done" if todo["completed"] else "pending"
            lines.append(f"{i}. [{status}] {todo['title']} ({todo['priority']})")
        return [TextContent(type="text", text="\n".join(lines))]

    elif name == "complete_todo":
        index = arguments["index"]
        if 0 <= index < len(todos):
            todos[index]["completed"] = True
            return [TextContent(type="text", text=f"Completed: {todos[index]['title']}")]
        return [TextContent(type="text", text=f"Invalid index: {index}")]

    return [TextContent(type="text", text=f"Unknown tool: {name}")]

async def main():
    async with stdio_server() as (read_stream, write_stream):
        await server.run(read_stream, write_stream)

if __name__ == "__main__":
    import asyncio
    asyncio.run(main())

Install the MCP SDK:

pip install mcp

Register it in Claude Desktop:

{
  "mcpServers": {
    "todo": {
      "command": "python",
      "args": ["path/to/todo_server.py"]
    }
  }
}

Building Your First MCP Server: TypeScript

TypeScript

// todo-server.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";

interface Todo {
  title: string;
  priority: string;
  completed: boolean;
}

const todos: Todo[] = [];

const server = new Server(
  { name: "todo-server", version: "1.0.0" },
  { capabilities: { tools: {} } }
);

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: "add_todo",
      description: "Add a new to-do item",
      inputSchema: {
        type: "object" as const,
        properties: {
          title: { type: "string", description: "The to-do item title" },
          priority: {
            type: "string",
            enum: ["low", "medium", "high"],
            description: "Priority level",
          },
        },
        required: ["title"],
      },
    },
    {
      name: "list_todos",
      description: "List all to-do items",
      inputSchema: {
        type: "object" as const,
        properties: {},
      },
    },
    {
      name: "complete_todo",
      description: "Mark a to-do item as complete by index",
      inputSchema: {
        type: "object" as const,
        properties: {
          index: {
            type: "integer",
            description: "The to-do item index (0-based)",
          },
        },
        required: ["index"],
      },
    },
  ],
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  if (name === "add_todo") {
    const todo: Todo = {
      title: (args as { title: string }).title,
      priority: (args as { priority?: string }).priority ?? "medium",
      completed: false,
    };
    todos.push(todo);
    return {
      content: [
        {
          type: "text" as const,
          text: `Added: ${todo.title} (${todo.priority} priority)`,
        },
      ],
    };
  }

  if (name === "list_todos") {
    if (todos.length === 0) {
      return {
        content: [{ type: "text" as const, text: "No to-do items yet." }],
      };
    }
    const lines = todos.map(
      (t, i) =>
        `${i}. [${t.completed ? "done" : "pending"}] ${t.title} (${t.priority})`
    );
    return {
      content: [{ type: "text" as const, text: lines.join("\n") }],
    };
  }

  if (name === "complete_todo") {
    const index = (args as { index: number }).index;
    if (index >= 0 && index < todos.length) {
      todos[index].completed = true;
      return {
        content: [
          {
            type: "text" as const,
            text: `Completed: ${todos[index].title}`,
          },
        ],
      };
    }
    return {
      content: [
        { type: "text" as const, text: `Invalid index: ${index}` },
      ],
    };
  }

  return {
    content: [{ type: "text" as const, text: `Unknown tool: ${name}` }],
  };
});

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Todo MCP server running on stdio");
}

main();

Install the SDK:

npm install @modelcontextprotocol/sdk

Register in Claude Desktop:

{
  "mcpServers": {
    "todo": {
      "command": "npx",
      "args": ["tsx", "path/to/todo-server.ts"]
    }
  }
}

MCP Resources

Resources expose read-only data to Claude. Unlike tools (which Claude calls), resources are available as context that Claude can request.

Python

from mcp.server import Server
from mcp.types import Resource, TextContent

server = Server("config-server")

@server.list_resources()
async def list_resources() -> list[Resource]:
    return [
        Resource(
            uri="config://app/settings",
            name="App Settings",
            description="Current application configuration",
            mimeType="application/json"
        )
    ]

@server.read_resource()
async def read_resource(uri: str) -> str:
    if uri == "config://app/settings":
        return '{"debug": false, "max_connections": 100, "log_level": "info"}'
    raise ValueError(f"Unknown resource: {uri}")

Resources are useful for providing context without giving Claude the ability to modify data.


MCP Prompts

Prompts are reusable templates that Claude can use. They are like saved prompt snippets.

Python

from mcp.server import Server
from mcp.types import Prompt, PromptMessage, TextContent

server = Server("prompt-server")

@server.list_prompts()
async def list_prompts() -> list[Prompt]:
    return [
        Prompt(
            name="code_review",
            description="Review code for bugs and improvements",
            arguments=[
                {"name": "code", "description": "The code to review", "required": True},
                {"name": "language", "description": "Programming language", "required": True}
            ]
        )
    ]

@server.get_prompt()
async def get_prompt(name: str, arguments: dict) -> list[PromptMessage]:
    if name == "code_review":
        return [
            PromptMessage(
                role="user",
                content=TextContent(
                    type="text",
                    text=f"""Review this {arguments['language']} code for:
1. Bugs and logic errors
2. Security vulnerabilities
3. Performance issues
4. Error handling gaps

Code:
```{arguments['language']}
{arguments['code']}

Rate each issue as CRITICAL, WARNING, or INFO.""" ) ) ] raise ValueError(f"Unknown prompt: {name}")


---

## Testing with MCP Inspector

The MCP Inspector is a debugging tool that lets you test your server without connecting it to Claude.

```bash
# Install and run the inspector
npx @modelcontextprotocol/inspector

# Or test a specific server
npx @modelcontextprotocol/inspector python path/to/todo_server.py

The inspector shows:

  • Available tools, resources, and prompts
  • Let you call tools with test inputs
  • View the full request/response cycle
  • Debug transport layer issues

Always test with the inspector before connecting to Claude Desktop or Claude Code.


MCP in Production

Security Considerations

  1. Validate all inputs — Never trust data from the AI model
  2. Limit permissions — Give servers the minimum access they need
  3. Use authentication — Require tokens for sensitive operations
  4. Audit logging — Log all tool calls for debugging and compliance
  5. Rate limiting — Prevent excessive tool calls

Example: Database Server with Read-Only Access

@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
    if name == "query_database":
        query = arguments["query"].strip().upper()

        # Only allow SELECT queries
        if not query.startswith("SELECT"):
            return [TextContent(type="text", text="Error: Only SELECT queries are allowed")]

        # Block dangerous patterns
        dangerous = ["DROP", "DELETE", "INSERT", "UPDATE", "ALTER", "TRUNCATE"]
        if any(word in query for word in dangerous):
            return [TextContent(type="text", text="Error: Query contains forbidden operations")]

        # Execute the query (with timeout)
        result = await db.execute(query, timeout=5.0)
        return [TextContent(type="text", text=json.dumps(result))]

Where to Find MCP Servers

The ecosystem is growing fast. Check the official registry first for maintained, well-tested servers.


Summary

ConceptDetails
MCPOpen protocol for connecting AI to external tools
Three primitivesTools (actions), Resources (data), Prompts (templates)
Transportstdio (local) or HTTP+SSE (remote)
Pre-built serversGitHub, PostgreSQL, Slack, filesystem, and hundreds more
SDKmcp (Python), @modelcontextprotocol/sdk (TypeScript)
TestingMCP Inspector for debugging
SecurityValidate inputs, limit permissions, audit logs

MCP is the standard way to extend Claude’s capabilities. Build once, use in Claude Desktop, Claude Code, VS Code, and any other MCP-compatible tool.


What’s Next?

In the next article, we will cover Building AI Agents with Claude — using the Agent SDK to create autonomous agents that plan, act, and observe.

Next: Building AI Agents with Claude