Skip to main content
Glama

MCP Server Python Starter

A clean starting point for building your own MCP server in Python. Clone it, swap the example tools for yours, and you've got something you can actually ship.

CI

What this is

MCP (the Model Context Protocol) is an open standard that lets AI apps like Claude call your code, so the model can look up your data or take actions in your systems. An MCP server is the small program that exposes those capabilities, and this repo is a template for writing one well.

If you're new to the idea, I wrote a plain-English intro: What is an MCP server?

I put this together because most MCP examples stop at "hello world." That's fine for a demo. But the first time you put one in front of a real team, you need config, logging that doesn't break stdio, input validation, tests, and a way to deploy it. That's what's here.

Related MCP server: mcp-docs-server

What's included

  • The official mcp Python SDK (v2), using MCPServer. That's the decorator-based API that used to be called FastMCP.

  • Two example tools. search_docs is a read-only lookup with pagination over a small bundled dataset. draft_support_ticket takes a pydantic model as input, so bad input gets a clear error before your code even runs.

  • A resource template, docs://{slug}, that returns a doc as Markdown.

  • A prompt template, explain_doc, that shows up as a reusable prompt in clients that support them.

  • Config from environment variables with pydantic-settings (MCP_STARTER_*, or a .env file).

  • JSON logs to stderr. With the stdio transport, stdout is the protocol channel, so one stray print() can break the connection. Ruff's T20 rule is turned on to catch those.

  • Two transports: stdio (the default, for local clients) and streamable HTTP (for running it as a service).

  • Tests that talk to the server through the SDK's in-memory client, plus a smoke test that launches it over real stdio.

  • Ruff, Pyright in strict mode, GitHub Actions CI, and a slim Dockerfile that runs as a non-root user.

The sample data is a made-up help center for a made-up product. It's only there so the tools have something to search. There are no network calls and no secrets.

Quick start

You'll need uv and Python 3.11 or newer.

git clone https://github.com/BonnieDooneConsulting/mcp-server-python-starter.git
cd mcp-server-python-starter
uv sync

Run it over stdio (this is what Claude Code and Claude Desktop use):

uv run mcp-starter

It'll sit there waiting for a client on stdin. That's expected. Press Ctrl+C to stop it.

Or run it over HTTP:

uv run mcp-starter --transport streamable-http --port 8000
# the MCP endpoint is http://127.0.0.1:8000/mcp

Want to poke at it in a browser first? The MCP Inspector is handy for that:

npx @modelcontextprotocol/inspector uv run mcp-starter

Configuration

Every setting can come from a flag, an environment variable, or a .env file. Flags win.

Variable

Flag

Default

MCP_STARTER_TRANSPORT

--transport

stdio

MCP_STARTER_HOST

--host

127.0.0.1

MCP_STARTER_PORT

--port

8000

MCP_STARTER_HTTP_PATH

/mcp

MCP_STARTER_LOG_LEVEL

--log-level

INFO

MCP_STARTER_LOG_FORMAT

json (or text)

MCP_STARTER_SERVER_NAME

mcp-starter

Connecting to Claude Code

Point Claude Code at your checkout:

claude mcp add mcp-starter -- uv --directory /absolute/path/to/mcp-server-python-starter run mcp-starter

If you're running the HTTP transport instead:

claude mcp add --transport http mcp-starter http://127.0.0.1:8000/mcp

Run claude mcp list to check that it connected, then ask Claude something like "search the help docs for webhooks."

Connecting to Claude Desktop

Open Settings, go to Developer, and click Edit Config. That opens claude_desktop_config.json. Add this:

{
  "mcpServers": {
    "mcp-starter": {
      "command": "uv",
      "args": [
        "--directory",
        "/absolute/path/to/mcp-server-python-starter",
        "run",
        "mcp-starter"
      ]
    }
  }
}

Restart Claude Desktop. If it can't find uv, use the full path from which uv as the command. The desktop app doesn't always see the same PATH your terminal does, and it's a common reason a server shows up as failed.

Adding your own tool

Tools live in src/mcp_starter/tools.py, inside register(). Type hints become the input schema, the docstring becomes the description the model reads, and a pydantic return type becomes structured output. Here's a small one:

from typing import Annotated

from pydantic import BaseModel, Field


class WordCount(BaseModel):
    words: int
    characters: int


def register(server: MCPServer, store: DocStore) -> None:
    ...

    @server.tool(annotations=ToolAnnotations(read_only_hint=True))
    def count_words(
        text: Annotated[str, Field(min_length=1, max_length=10_000)],
    ) -> WordCount:
        """Count the words and characters in a piece of text."""
        return WordCount(words=len(text.split()), characters=len(text))

A few habits that'll save you pain later:

  • Write the docstring for the model, not for you. It's how the model decides when to call your tool, so say what it does and what it returns.

  • Raise ToolError for failures you expect, like "that customer doesn't exist." The message goes straight back to the model, so make it something it can act on. Any other exception gets hidden behind a generic error and logged with a traceback, which is what you want for real bugs.

  • Keep tools small and specific. A few focused tools are easier for the model to pick between than one big tool with a mode argument.

  • Add a test in tests/test_server.py. The client fixture hands you a connected client, so most tests are a few lines.

If you outgrow one file, split tools into a package and call each module's register() from build_server() in server.py.

Running tests

uv run pytest                          # tests, all in-memory
uv run ruff check .                    # lint
uv run ruff format --check .           # formatting
uv run pyright                         # type check (strict)
uv run python scripts/smoke_stdio.py   # launch over real stdio and list tools

CI runs all of these on every push to main and every pull request, on Python 3.11 through 3.14.

Not using uv? A plain virtualenv works too:

python3 -m venv .venv
.venv/bin/pip install -e '.[dev]'
.venv/bin/pytest

Running in Docker

The image runs the HTTP transport on port 8000 as a non-root user.

docker build -t mcp-starter .
docker run --rm -p 8000:8000 mcp-starter

One heads-up before you put it on the internet: this template doesn't include auth. Inside the container it binds to 0.0.0.0, and the SDK only turns on DNS rebinding protection by default when it's bound to localhost. Put it behind something that handles auth, or wire up the SDK's auth support, before you expose it.

Project layout

.
├── src/mcp_starter/
│   ├── cli.py            # entry point: parses flags, sets up logging, picks a transport
│   ├── server.py         # build_server(): creates the MCPServer and registers everything
│   ├── config.py         # Settings, loaded from MCP_STARTER_* env vars
│   ├── logging_setup.py  # JSON logs to stderr
│   ├── store.py          # tiny read-only data layer over the bundled JSON
│   ├── tools.py          # search_docs, draft_support_ticket
│   ├── resources.py      # docs://{slug}
│   ├── prompts.py        # explain_doc
│   └── data/docs.json    # sample data
├── tests/                # pytest, using the SDK's in-memory client
├── scripts/smoke_stdio.py
├── Dockerfile
├── .github/workflows/ci.yml
├── pyproject.toml        # deps plus ruff, pyright, and pytest config
└── uv.lock

Contributing

Issues and pull requests are welcome. Please run the checks in Running tests before opening a PR. If you're planning something big, open an issue first so we can talk it through.

License

MIT. See LICENSE.


Built by Bonnie Doone Consulting. Need a custom MCP server for your team? Let's talk.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Generic MCP server that exposes Markdown documentation to LLMs, enabling them to search and answer questions about any software documentation.
    MIT
  • F
    license
    Not graded
    quality
    F
    maintenance
    Serves Markdown documentation as an MCP server, enabling AI agents to answer questions from your content. Supports local, npm, and Cloudflare deployment with reusable prompts and resources.
    106 npm
    3
    -
  • F
    license
    Not graded
    quality
    B
    maintenance
    Local documentation search server for AI models using hybrid retrieval (phrase, keyword, vector). Provides MCP tools to search and fetch documentation from bundled or custom doc sets without any external API keys.
    -
  • F
    license
    Not graded
    quality
    C
    maintenance
    A local MCP server providing read-only search and retrieval over the Claude documentation corpus via SQLite FTS5, with tools for searching, fetching documents, outlines, and listing sources without network I/O.
    -