← Back to DevBytes

Model Context Protocol (MCP): Integrating External Tools with Claude

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

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:

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:

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.

— Ad —

Google AdSense will appear here after approval

← Back to all articles