Model Context Protocol MCP Guide: Build Custom Claude Tools in Python

Build an MCP Server in Python: Secure, Runnable Tutorial

Reviewed: August 12, 2026. This tutorial uses the stable Model Context Protocol (MCP) Python SDK v2 and the 2026-07-28 protocol revision. You will build a local stdio server, expose one useful tool, test it without an LLM, and connect it to a compatible host.

The project deliberately avoids an unrestricted shell, arbitrary file access, and fake “ping” results. A tool that can act on a computer needs a narrow contract, truthful output, validation, and a user-controlled approval boundary.

What Model Context Protocol does

MCP is an open protocol for connecting an AI application to external data and actions through a standard interface. The model does not connect directly to your database or file system. An MCP host manages the AI session, connects to one or more MCP servers, presents available capabilities to the model, and decides when user approval is required.

An MCP server can expose three familiar primitives:

  • Tools: callable operations with typed inputs, such as looking up an order or creating a ticket.
  • Resources: addressable data the client can read, such as a document or configuration record.
  • Prompts: reusable, parameterised message templates offered to the client.

MCP standardises discovery and invocation; it does not make a dangerous capability safe. If a server exposes “delete database,” the protocol faithfully describes a dangerous tool. The server and host still need authentication, authorisation, validation, logging, and human control.

Host, client and server

ComponentResponsibilityExample
HostRuns the AI experience, manages permissions and coordinates connectionsClaude Desktop, an IDE, or your application
MCP clientMaintains the protocol relationship with a server and calls its capabilitiesA client inside the host
MCP serverPublishes narrowly defined tools, resources, or promptsThe Python program built below

In the 2026-07-28 specification, modern requests are stateless and carry protocol/client information in metadata. The Python SDK v2 can also serve older clients, so application developers normally use the SDK rather than hand-writing JSON-RPC messages.

Use stdio locally and Streamable HTTP remotely

TransportUse it forOperational boundary
stdioA host launching a local server processMessages travel through standard input/output; logs must not corrupt stdout
Streamable HTTPA deployed service reached over a networkRequires real authentication, authorisation, TLS, origin/host controls, rate limits, and monitoring
SSECompatibility with older clients onlySuperseded by Streamable HTTP; do not choose it for a new server

The old advice “stdio versus SSE” is outdated. Official SDK documentation says SSE was superseded in the 2025-03-26 protocol revision.

Build a safe local MCP server in Python

1. Create the project

The stable SDK v2 requires Python 3.10 or later. With uv:

uv init safe-mcp-demo
cd safe-mcp-demo
uv add "mcp[cli]"

Or with a virtual environment and pip:

python -m venv .venv
# Windows PowerShell
.venv\Scripts\Activate.ps1
# macOS/Linux: source .venv/bin/activate
python -m pip install "mcp[cli]"

Record the installed version for reproducibility:

mcp version
python --version

2. Create a small workspace

mkdir workspace

Add a text file such as workspace/notes.txt. The server will inspect metadata inside this directory but will not read file contents or escape the directory.

3. Save this as server.py

from pathlib import Path
from typing import TypedDict

from mcp.server import MCPServer


ROOT = (Path(__file__).parent / "workspace").resolve()
mcp = MCPServer("Safe workspace inspector")


class FileInfo(TypedDict):
    name: str
    size_bytes: int
    extension: str


def resolve_inside_workspace(relative_path: str) -> Path:
    """Resolve a relative path and reject traversal outside ROOT."""
    if not relative_path or Path(relative_path).is_absolute():
        raise ValueError("Provide a non-empty relative path")

    target = (ROOT / relative_path).resolve()
    if target != ROOT and ROOT not in target.parents:
        raise ValueError("Path escapes the allowed workspace")
    return target


@mcp.tool()
def file_info(relative_path: str) -> FileInfo:
    """Return metadata for one file inside the allowed workspace."""
    target = resolve_inside_workspace(relative_path)
    if not target.is_file():
        raise ValueError("The requested path is not a file")

    stat = target.stat()
    return {
        "name": target.name,
        "size_bytes": stat.st_size,
        "extension": target.suffix.lower(),
    }


if __name__ == "__main__":
    mcp.run(transport="stdio")

The type hint creates the input schema, the docstring describes the tool, and the TypedDict gives the client structured output. The security decision is the path resolver: ../secret.txt and absolute paths are rejected before file access.

4. Check syntax

python -m py_compile server.py

Test the tool without trusting an LLM

A deterministic tool should be tested directly. Save this as test_server.py:

import asyncio

from mcp import Client
from server import mcp


async def main() -> None:
    async with Client(mcp) as client:
        tools = await client.list_tools()
        names = {tool.name for tool in tools.tools}
        assert "file_info" in names

        result = await client.call_tool(
            "file_info",
            {"relative_path": "notes.txt"},
        )
        assert result.is_error is False
        assert result.structured_content["name"] == "notes.txt"
        print(result.structured_content)


asyncio.run(main())

Run it:

python test_server.py

Then test the failure boundary. Change the argument to ../server.py; the call must return an error rather than reveal metadata outside the workspace. A passing happy-path test is not enough when the main risk is invalid input.

Inspect the server interactively

uv run mcp dev server.py

The MCP Inspector lets you review the generated schema and invoke the tool with controlled inputs. Test an existing file, a missing file, an absolute path, and traversal attempts before connecting an AI host.

Connect the server to Claude Desktop or another host

The Python SDK provides an installation command for local development:

uv run mcp install server.py

Restart the host if required, open its MCP or Extensions settings, and verify that the server and file_info tool appear. Current Claude Desktop versions also support packaged desktop extensions, which are easier to distribute and manage than asking every user to edit JSON manually.

If you configure a stdio server manually, use an absolute executable and script path. A minimal host configuration follows this shape:

{
  "mcpServers": {
    "safe-workspace": {
      "command": "C:/path/to/safe-mcp-demo/.venv/Scripts/python.exe",
      "args": ["C:/path/to/safe-mcp-demo/server.py"]
    }
  }
}

The exact configuration location and UI are host-specific and change over time. Use the current documentation for the host rather than copying an old path from a random tutorial.

Troubleshoot a local stdio server

Server does not appear

  • Run the configured Python executable and script from a terminal.
  • Use absolute paths and confirm the host account can access them.
  • Check that the environment contains the same MCP SDK version used during development.
  • Open the host’s MCP/extension logs instead of repeatedly reinstalling.

JSON parse or connection error

For stdio, stdout belongs to protocol traffic. Do not use print() for server diagnostics or configure a library to log there. Send logs to stderr:

import logging
import sys

logging.basicConfig(stream=sys.stderr, level=logging.INFO)

Tool is listed but the call fails

Read the structured error, then reproduce the same input with the in-process client. Check schema types, file permissions, working directory assumptions, and exception handling. Do not “fix” a failure by returning a success sentence when no operation happened.

MCP security checklist

  • Expose the smallest action. Prefer file_info to run_command.
  • Validate inputs at the boundary. Allow expected values; reject everything else.
  • Resolve paths safely. Check traversal, symlinks, ownership, file type, and size before reading or writing.
  • Keep secrets outside tool results. Use a secret store or environment injection and redact logs.
  • Separate read from write. Give destructive operations distinct tools and require confirmation.
  • Return truthful outcomes. Never claim a service is reachable, a file was saved, or a ticket was created without verifying it.
  • Assume tool output is untrusted. A webpage, document, or database field can contain instructions intended to manipulate the model.
  • Pin and update dependencies. Review SDK releases and security advisories before deployment.
  • Audit important actions. Record the authenticated actor, tool, approved arguments, result, and time without storing unnecessary sensitive data.

MCP’s specification explicitly treats tools as potentially arbitrary code execution. The host should show users which server supplies a tool and let them review sensitive calls; the server must still enforce its own rules because model instructions are not authorisation.

When to use Streamable HTTP

Change to Streamable HTTP only when a networked deployment is required:

if __name__ == "__main__":
    mcp.run(
        transport="streamable-http",
        host="127.0.0.1",
        port=8000,
    )

Binding to localhost is suitable for local testing, not production. A remote server needs TLS, an authentication and authorisation design, request-size limits, rate limiting, timeouts, safe CORS/origin and host validation, dependency patching, and logs. Do not expose the tutorial server publicly merely by changing 127.0.0.1 to 0.0.0.0.

The official Python SDK has had security advisories affecting HTTP transports. Use a supported release, read the advisory list, and test identity isolation before handling customer or company data.

Practice project

Extend the server with a read-only list_text_files tool that:

  1. accepts no directory from the caller;
  2. returns only .txt files directly inside ROOT;
  3. sorts results by filename;
  4. caps output at 100 entries;
  5. does not follow symlinks;
  6. has tests for the cap, extension filter, and symlink rule.

Document the threat you prevented beside each test. That turns a demo into portfolio evidence of API design and security reasoning.

If Python functions, paths, and testing are still unfamiliar, complete the free Python automation course first. Then use the AI prompt-writing guide to design clear tool-use instructions, and compare host capabilities in Claude vs ChatGPT vs Gemini.

Frequently asked questions

Is MCP limited to Claude?

No. Anthropic introduced MCP, but the protocol and SDKs are open. Any compatible host can connect to a conforming server.

Should a new MCP server use SSE?

No. Use stdio for a local subprocess or Streamable HTTP for a network service. SSE remains only for older-client compatibility.

Does MCP replace a REST API?

Not automatically. A server may wrap an existing API and present model-friendly tools or resources. The underlying service still needs its normal access controls and business rules.

Can an MCP tool run shell commands?

It can, but an unrestricted shell gives model-controlled input enormous authority. Build specific allowlisted operations instead, isolate the process, and require approval for material changes.

Why test without an LLM?

It separates protocol and tool defects from model behaviour. A deterministic operation should produce the same validated result for the same input regardless of which model requested it.

Official sources and refresh rule

Refresh rule: review this tutorial whenever the MCP specification or Python SDK stable major version changes, and at least every three months. Re-run the code and security tests before changing the “Reviewed” date.

Similar Posts

Leave a Reply