Skip to content

fig. Bmcp

Build Your First MCP Server in Python (and Connect It to Claude Code)

A hands-on guide to the Model Context Protocol: build a small MCP server in Python with tools and resources, then wire it into an AI coding agent.

Written by
Mayank Panchal
Published
Reading time
3 min
Filed under
mcp
  • mcp
  • python
  • ai-agents
On this page · 8
  1. The three things a server offers
  2. Setup
  3. A small server
  4. Try it without a client
  5. Connect it to Claude Code
  6. Making tools the model uses well
  7. Security: treat the model as an untrusted caller
  8. Where to go next

The Model Context Protocol (MCP) is an open protocol that lets AI applications discover and call tools you write, without a custom connector for each app. One server, many clients: a coding agent, a chat app, or your own script can all use it.

This is the shortest path from nothing to a working server in Python.

The three things a server offers#

Concept Who controls it Think of it as
Tools The model Functions it can call to do something
Resources The application Read-only data it can load as context
Prompts The user Reusable templates the user picks explicitly

Most servers start with tools. Add resources when there is data worth reading rather than computing.

Setup#

Bash
python -m venv .venv && source .venv/bin/activate
pip install "mcp[cli]"

A small server#

This one manages a notes list in memory:

Python
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("notes")

NOTES: dict[int, str] = {}


@mcp.tool()
def add_note(text: str) -> str:
    """Save a note and return its id."""
    note_id = len(NOTES) + 1
    NOTES[note_id] = text
    return f"Saved note {note_id}"


@mcp.tool()
def search_notes(query: str) -> list[str]:
    """Return every note that contains the query, case-insensitively."""
    q = query.lower()
    return [t for t in NOTES.values() if q in t.lower()]


@mcp.resource("notes://{note_id}")
def get_note(note_id: int) -> str:
    """Read one note by id."""
    return NOTES.get(note_id, "No such note")


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

Three details carry the whole experience:

  • Type hints become the input schema. The SDK builds the JSON schema the model sees from your annotations, so query: str means the model is told to pass a string.
  • The docstring is the tool description. The model decides when to call your tool from this text. Write it for the model: what it does, what it returns, when not to use it.
  • mcp.run() defaults to stdio. The client launches your script as a subprocess and talks over standard input and output.

Try it without a client#

The mcp[cli] extra includes an inspector:

Bash
mcp dev server.py

It opens a UI where you can list tools and call them by hand. Debug here first. It is much faster than guessing from a chat transcript.

Connect it to Claude Code#

Register the server by command:

Bash
claude mcp add notes -- python /absolute/path/to/server.py

Then ask it to "save a note that the deploy is on Friday" and watch it call add_note. Use an absolute path to the script, and make sure python resolves to the environment where you installed mcp.

Making tools the model uses well#

  • Keep tools narrow. search_notes beats do_anything(command: str). Small, well-named tools get chosen correctly.
  • Return something readable. Errors should say what went wrong and what to try, not a bare stack trace.
  • Cap the output. A tool that returns 50,000 lines wastes context and money. Paginate or summarise.

Security: treat the model as an untrusted caller#

An MCP server is an API whose caller is a language model that may have read hostile text. So:

  1. Validate every argument exactly as you would for a public endpoint. Never pass a model-supplied string to a shell, a SQL query, or a file path unchecked.
  2. Grant least privilege. A read-only database role for a reporting tool; a single directory for a file tool.
  3. Gate the dangerous tools. Anything that deletes, spends money, or sends messages should need explicit approval from the user in the client.
  4. Don't expose a server to the network by accident. Stdio is local by design. If you move to an HTTP transport, add authentication first.

Where to go next#

Wrap an API you already use, such as your own FastAPI backend, and expose three or four of its most useful operations as tools. That is a better learning project than any toy example, because you will immediately feel how much the tool descriptions matter.

Written by

Mayank Panchal

Software Developer & Full-Time Learner. Coding on the clock, learning around the clock. Software developer in Bengaluru - Next.js, FastAPI, PostgreSQL, AI-native tooling.

fig. B.1keep reading

fig. B.2contact

Let’s build something sharp.