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¶
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¶
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:
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):
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).
Then ask: "Save a note titled 'standup' saying the eval suite is green."
Settings → Developer → Edit Config, then restart Claude Desktop:
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.