Skip to content

Build an MCP server in 10 minutes

Beginner · 10 min · tested with mcp 2.3

MCP (Model Context Protocol) lets you give AI apps like Claude, Cursor and VS Code new abilities by writing a small server once. This tutorial builds a notes server: the AI can save notes, search them and read them. The same pattern works for your database, your company API or any script you already have.

For the full story (the protocol, remote servers, OAuth, security), see the MCP notes.


1. Install

pip install "mcp>=2"

Version 2 of the Python SDK calls the server class MCPServer. Older tutorials use from mcp.server.fastmcp import FastMCP, which is the same idea under its old name.


2. Write the server

notes_server.py
from datetime import date
from pathlib import Path

from mcp.server.mcpserver import MCPServer
from mcp.server.mcpserver.exceptions import ToolError

NOTES = (Path(__file__).parent / "notes").resolve()
NOTES.mkdir(exist_ok=True)

mcp = MCPServer("notes", instructions="Read and write the user's plain-text notes.")

@mcp.tool()
def add_note(title: str, text: str) -> str:
    """Save a new note. Use when the user asks to remember or write something down."""
    path = NOTES / f"{date.today()}-{title.lower().replace(' ', '-')}.md"
    path.write_text(text, encoding="utf-8")
    return f"saved {path.name}"

@mcp.tool()
def search_notes(query: str) -> list[str]:
    """Find notes whose text contains the query (case-insensitive). Returns matching file names."""
    return [p.name for p in sorted(NOTES.glob("*.md"))
            if query.lower() in p.read_text(encoding="utf-8").lower()]

@mcp.tool()
def read_note(name: str) -> str:
    """Read one note by its file name, as returned by search_notes."""
    path = (NOTES / name).resolve()
    if path.parent != NOTES or not path.exists():      # blocks "../secrets.txt" tricks
        raise ToolError(f"no note called {name!r}; use search_notes first")
    return path.read_text(encoding="utf-8")

if __name__ == "__main__":
    mcp.run()                                          # talks over stdin/stdout

That's a complete server. The SDK turns each function into a tool:

  • the function name is the tool name,
  • the docstring is the description the AI reads to decide when to use it (so say when, not just what),
  • the type hints become the input schema.

ToolError sends a helpful message back to the AI so it can correct itself. Never print() in a stdio server: stdout is the protocol channel. Log to stderr instead.


3. Test it before connecting anything

A tiny client script starts the server and calls its tools, exactly like an AI app would:

test_client.py
import asyncio
import sys
from mcp import Client, StdioServerParameters

async def main():
    server = StdioServerParameters(command=sys.executable, args=["notes_server.py"])
    async with Client(server) as client:
        print([t.name for t in (await client.list_tools()).tools])
        r = await client.call_tool("add_note", {"title": "RAG ideas", "text": "Try hybrid search with BM25."})
        print(r.content[0].text)
        r = await client.call_tool("search_notes", {"query": "bm25"})
        print(r.structured_content)
        r = await client.call_tool("read_note", {"name": "../secrets.txt"})
        print(r.is_error, r.content[0].text)

asyncio.run(main())
['add_note', 'search_notes', 'read_note']
saved 2026-10-26-rag-ideas.md
{'result': ['2026-10-26-rag-ideas.md']}
True Error executing tool read_note: no note called '../secrets.txt'; use search_notes first

For clicking around instead, the MCP Inspector gives you a web UI to list and call tools (needs Node.js):

npx @modelcontextprotocol/inspector python notes_server.py

4. Connect it to your AI app

Use the absolute path to the file. AI apps don't start in your project folder or your virtual environment, so point command at the Python that has mcp installed (on Windows, something like C:/projects/notes/.venv/Scripts/python.exe).

claude mcp add notes -- /abs/path/.venv/bin/python /abs/path/notes_server.py
claude mcp list

Then ask: "Save a note titled 'standup' saying the eval suite is green."

Settings → Developer → Edit Config, then restart Claude Desktop:

claude_desktop_config.json
{
  "mcpServers": {
    "notes": {
      "command": "/abs/path/.venv/bin/python",
      "args": ["/abs/path/notes_server.py"]
    }
  }
}
.cursor/mcp.json
{
  "mcpServers": {
    "notes": {"command": "/abs/path/.venv/bin/python", "args": ["/abs/path/notes_server.py"]}
  }
}
.vscode/mcp.json
{
  "servers": {
    "notes": {"type": "stdio", "command": "/abs/path/.venv/bin/python", "args": ["/abs/path/notes_server.py"]}
  }
}

The app asks your permission before running a tool the first time. Try "What did I note about BM25?" and watch it call search_notes, then read_note.


5. If it doesn't show up

Symptom Fix
Server not listed / fails to start Absolute paths; the right Python; restart the app after editing config
Starts, then disconnects Something printed to stdout: remove print() calls, log to stderr
ModuleNotFoundError: mcp command points at a Python without mcp: use your venv's Python
AI never uses the tool Improve the docstring: say exactly when to use it
Works in the Inspector, not in the app Check the app's MCP logs (Claude Code: /mcp)

Next steps

  • Add a resource (@mcp.resource("notes://index")) or a prompt (@mcp.prompt()): see Building servers.
  • Serve it over HTTP for a team: mcp.run(transport="streamable-http"), then add auth: see Remote servers and auth.
  • Before connecting anyone else's server, read MCP security.

Next: Write your first tests for an LLM app →