Build an MCP Server in Python for Claude Code

Independently researched No sponsored picks Affiliate supported

Building an MCP server in Python for Claude Code is mostly a matter of defining a few safe tools, running them over stdio, and registering the server command correctly in Claude Code.

This walkthrough shows how to build a minimal MCP server step by step: project setup, tool definitions, transport, local testing, a concrete inspection flow, and Claude Code connection. If you want the big-picture background first, start with this overview of the Model Context Protocol (MCP).

An MCP server is a small process that exposes tools and context to Claude Code through a standard protocol.

An MCP server gives an AI client structured access to capabilities like file reading, test execution, API lookup, or internal automation. Instead of pasting context into chat manually, you let the client discover tools and call them with typed arguments.

In this tutorial, you will build two minimal tools:

  • ping — confirms the server is alive
  • read_file_summary — reads a text file from an allowed directory and returns a short structured summary

That is enough to learn the important MCP patterns without jumping straight into authentication, hosted deployments, or broad agent-style tools.

The core MCP building blocks

Most servers revolve around the same pieces:

  • Server runtime: receives MCP requests and returns MCP responses
  • Tools: functions the client can call
  • Resources: readable content the client can inspect
  • Transport: communication layer, commonly stdio or HTTP
  • Schemas: typed inputs and outputs that make tool calls predictable

The local architecture in one glance

Claude Code
   |
   | MCP over stdio
   v
Your MCP Server
   |
   +--> local files
   +--> scripts
   +--> APIs

If you are thinking in product terms, an MCP server is often the thin execution layer behind a reusable AI skill for developer workflows: a narrowly scoped set of tools that turns a model into something operationally useful.

The fastest way to start is to use Python, a current MCP SDK, and stdio transport.

For a first server, Python + stdio is usually the shortest path because it minimizes infrastructure and works well for local files, scripts, and development tooling.

The exact SDK helpers may evolve, but the workflow stays the same: install the SDK, define tools, run the server, and connect a client.

Python vs TypeScript and stdio vs HTTP

Choice Best for Advantages Tradeoffs
Python SDK Scripts, file tools, data workflows Fast prototyping, concise code Environment setup can trip up teams
TypeScript SDK Node tooling, editor extensions, JS stacks Familiar npm ecosystem, strong typing Slightly more setup for simple local tools
stdio transport Local development, Claude Code No web server, simple child-process model Not meant for shared remote access
HTTP transport Hosted internal services Standard networking and deployment Requires auth, routing, and more ops

Use:

  • Python
  • a maintained MCP Python SDK
  • stdio transport
  • a local virtual environment

That gives you a minimal but realistic setup that mirrors how many developers first connect custom tools into Claude Code.

A minimal MCP server project only needs a virtualenv, a server file, and two tool definitions.

The exact import paths can change across SDK iterations, so treat the code below as the core pattern and adapt imports to the current docs if needed.

1) Create the project

mkdir my-mcp-server
cd my-mcp-server
python -m venv .venv
source .venv/bin/activate

Install the SDK and a common validation dependency:

pip install mcp pydantic

If the current SDK recommends a different package or extra, follow its install instructions instead.

2) Create a safe test directory and sample file

This gives your file-reading tool a fixed root and a known test target:

mkdir -p docs
cat > docs/example.txt <<'EOF'
Hello from MCP.
This is a sample file for local tool testing.
EOF

3) Add server.py

Create server.py:

from pathlib import Path
from typing import Dict, Any
import sys

# Adjust imports if the current SDK exposes these differently.
from mcp.server.fastmcp import FastMCP

app = FastMCP("my-mcp-server")
ALLOWED_ROOT = Path("./docs").resolve()


@app.tool()
def ping() -> Dict[str, Any]:
    """Return a simple health response."""
    return {
        "ok": True,
        "message": "pong"
    }


@app.tool()
def read_file_summary(path: str) -> Dict[str, Any]:
    """
    Read a text file from the docs directory and return a preview.
    """
    requested = (ALLOWED_ROOT / path).resolve()

    if not str(requested).startswith(str(ALLOWED_ROOT)):
        return {
            "ok": False,
            "error": "Path escapes allowed root"
        }

    if not requested.exists():
        return {
            "ok": False,
            "error": f"File not found: {requested}"
        }

    if not requested.is_file():
        return {
            "ok": False,
            "error": f"Not a file: {requested}"
        }

    text = requested.read_text(encoding="utf-8", errors="replace")

    return {
        "ok": True,
        "path": str(requested),
        "chars": len(text),
        "preview": text[:500]
    }


if __name__ == "__main__":
    # Never write debugging logs to stdout under stdio transport.
    print("Starting MCP server...", file=sys.stderr)
    app.run()

4) Why this is a good minimal server

This example covers the core mechanics you will reuse in larger projects:

  • tool registration
  • structured return payloads
  • input and path validation
  • directory scoping
  • stdio startup behavior

It is minimal, but not toy-like: the same design patterns apply when you later add test runners, API calls, repo inspection, or internal documentation tools.

Good MCP tools should be narrow, typed, and safe by default.

The best MCP tools do one thing clearly, accept limited inputs, and return structured outputs that the client can reason about reliably.

Tool design rules that improve model behavior

Prefer tools that are:

  • single-purpose
  • read-only when possible
  • clearly named
  • strictly validated
  • small in scope
  • predictable in output shape

Good examples:

  • list_repo_files
  • get_ticket_by_id
  • run_unit_tests
  • read_file_summary

Risky examples:

  • do_task
  • filesystem_agent
  • system_tool
  • run_anything

Better vs worse tool definitions

Better Worse
read_file_summary(path) filesystem_agent(action, payload)
run_pytest(test_path) run_any_command(command)
get_customer_by_id(id) query_database(sql)

Why path restriction is non-negotiable

A file tool without a root restriction can expose sensitive files by accident. Since AI clients can chain and retry tool calls, you should assume unexpected inputs will happen.

The key guard is:

requested = (ALLOWED_ROOT / path).resolve()
if not str(requested).startswith(str(ALLOWED_ROOT)):
    return {"ok": False, "error": "Path escapes allowed root"}

That blocks common traversal attempts like ../secret.txt.

Tool descriptions also matter

Models often choose tools based on name and description, so descriptions should explain:

  • what the tool does
  • when to use it
  • what input it expects
  • what limits apply
  • what it returns

Good description:

Read a text file from the docs directory and return a preview and character count.

Weak description:

Read file.

Local testing should prove tool discovery, invocation, and failure handling before Claude Code enters the loop.

Before you wire up Claude Code, verify that your server starts cleanly, exposes tools, and returns useful errors when inputs are wrong.

1) Start the server manually

python server.py

With stdio, an apparently idle process is normal. It is waiting for an MCP client to connect.

2) Validate the file and environment first

These checks catch a lot of setup issues early:

python -m py_compile server.py
python -c "import mcp; print('mcp import ok')"
which python

If which python does not point into your .venv, activate the environment again.

3) Use an MCP inspector or test client for a real invocation flow

The most useful next step is to attach an MCP inspector or SDK-provided test client if your current SDK includes one. The exact command can vary by SDK release, but the flow is usually the same:

  1. Launch the inspector or test client.
  2. Point it at your server command.
  3. Confirm the tool list includes ping and read_file_summary.
  4. Invoke a valid tool call.
  5. Invoke an invalid call and verify the error is structured.

A typical command pattern looks like this:

/path/to/python server.py

Then inside the inspector or client UI/flow:

List tools

  • Expected result: ping, read_file_summary

Invoke ping

{}

Expected response:

{
  "ok": true,
  "message": "pong"
}

Invoke read_file_summary

{
  "path": "example.txt"
}

Expected response shape:

{
  "ok": true,
  "path": "/absolute/path/to/docs/example.txt",
  "chars": 59,
  "preview": "Hello from MCP.\nThis is a sample file for local tool testing.\n"
}

Invoke an invalid path

{
  "path": "../secret.txt"
}

Expected response:

{
  "ok": false,
  "error": "Path escapes allowed root"
}

The exact UI labels differ between inspector tools, but that sequence proves the three things that matter most: discovery, successful invocation, and safe rejection.

4) Avoid the most common stdio bug

Do not print logs to stdout. MCP messages usually travel over stdin/stdout, so stray output can break parsing.

Bad:

print("debugging")

Good:

import sys
print("debugging", file=sys.stderr)

5) What should pass before you continue

Verify all of the following:

  • the server starts with no traceback
  • the process stays alive
  • the client can discover both tools
  • ping returns structured JSON
  • read_file_summary("example.txt") succeeds
  • read_file_summary("../oops.txt") fails cleanly
  • logs go to stderr, not stdout
  • no interactive prompt blocks startup

Claude Code connection works best when you register absolute paths for the virtualenv Python and server file.

Claude Code usually launches your MCP server as a subprocess, so the most reliable configuration uses an absolute interpreter path and absolute script path.

The exact config location may change over time, but the shape is typically similar to this.

Minimal Claude Code MCP config

{
  "mcpServers": {
    "my-mcp-server": {
      "command": "/absolute/path/to/my-mcp-server/.venv/bin/python",
      "args": ["/absolute/path/to/my-mcp-server/server.py"]
    }
  }
}

This is safer than using python from PATH, because it guarantees Claude Code runs the interpreter where you installed the MCP SDK.

Config with environment variables

{
  "mcpServers": {
    "my-mcp-server": {
      "command": "/absolute/path/to/my-mcp-server/.venv/bin/python",
      "args": ["/absolute/path/to/my-mcp-server/server.py"],
      "env": {
        "MY_APP_MODE": "development",
        "LOG_LEVEL": "debug"
      }
    }
  }
}

A practical Claude Code smoke test

After saving the config, restart or reload Claude Code and try prompts like:

  • “What MCP tools are available?”
  • “Call the ping tool.”
  • “Use read_file_summary with example.txt.”
  • “Try read_file_summary with ../oops.txt and show the returned error.”

If Claude Code cannot see the tools, the issue is usually startup-related, not tool-definition-related.

The most common Claude Code setup failures

These are the usual culprits:

  • using relative paths in command or args
  • pointing to the wrong Python interpreter
  • relying on shell aliases or shell startup files
  • missing SDK installation in the selected .venv
  • printing to stdout during startup
  • assuming the working directory is your project root
  • requiring secrets only available in your interactive shell

Resolve your paths explicitly first:

pwd
realpath .venv/bin/python
realpath server.py

Then paste those exact values into config.

A useful MCP server becomes production-ready when you add stronger guardrails, logging, and deployment choices.

A local toy server is easy; a dependable server for daily work needs tighter boundaries, better observability, and a cleaner rollout model.

Security upgrades to make early

Add these before exposing sensitive systems:

  • keep tools narrow
  • restrict filesystem roots
  • avoid raw shell execution
  • validate every parameter
  • redact sensitive errors
  • prefer read-only operations first
  • add authentication if you move to networked transports

If you are considering a tool like run_command(command: str), stop and replace it with explicit tools such as git_status(), run_pytest(test_path), or read_ci_log(job_id).

Reliability upgrades that pay off fast

Add these next:

  • request timeouts
  • retries for flaky upstream APIs
  • structured stderr logging
  • correlation IDs
  • unit tests for tool functions
  • integration tests for server startup
  • predictable error contracts

A good MCP server should fail in a way the client can reason about, not with silent exits or free-form stack traces.

When to keep stdio and when to move to HTTP

Keep stdio if you need:

  • personal local workflows
  • fast Claude Code integration
  • minimal infrastructure
  • tight edit-test loops

Move toward HTTP if you need:

  • shared access across machines
  • centralized deployment
  • auth and policy layers
  • standard service monitoring

For many developer use cases, stdio is not just the beginner option; it remains the right option.

Frequently Asked Questions

How do I debug tool discovery if Claude Code says no MCP tools are available?

First, confirm the server process actually starts by running the same command and args manually in your terminal. Then check for three common issues: wrong .venv interpreter, import failures, and accidental stdout logging that corrupts the MCP session.

Why does my server work in the terminal but fail inside Claude Code?

The usual reason is environment mismatch: Claude Code may launch a different Python binary, working directory, or environment than your shell. Fix this by using absolute paths for both .venv/bin/python and server.py, and by passing required environment variables explicitly in the MCP config.

What should I do if file paths behave inconsistently?

Assume the current working directory is not reliable and resolve paths from a known base. In practice, use Path(...).resolve(), log Path.cwd() to stderr during debugging, and prefer absolute config paths so your server starts from a predictable context.

Can I test my MCP tools before connecting Claude Code?

Yes: use an SDK-provided MCP inspector or test client to list tools and invoke them directly before Claude Code is involved. A good preflight test is to call ping, then read_file_summary with example.txt, then retry with ../secret.txt to confirm your success and error paths are both working.

Why Trust FindPicked?

Our recommendations are based on extensive research, real user reviews, and spec-by-spec analysis. We never accept payment for placement. When you buy through our links, we may earn a commission — this supports our work at no extra cost to you.

Learn how we pick →