Step-by-step builds

Build an MCP Server in Python: A Notes Search Server, Step by Step

Satvik ParamkushamUpdated October 5, 202612 min read

Quick answer

To build an MCP server in Python, install the official SDK with pip install "mcp[cli]", create an MCPServer object, turn ordinary functions into tools with the @mcp.tool() decorator, and call mcp.run(). Test it in the MCP Inspector with mcp dev server.py, then add it to Claude Desktop's config file so the assistant can call your tools.

MCP (Model Context Protocol) is the open standard that lets AI apps such as Claude Desktop, Claude Code and many IDEs plug into outside tools and data. Once you build one MCP server, every MCP-compatible app can use it, which is why "build an MCP server" has become one of the most searched project ideas for developers and final-year students.

This guide builds a small but useful server in about 40 lines of Python: it lets an AI assistant list, search and read the notes in a folder on your laptop. You will test it in the MCP Inspector, check it with a short Python test, and connect it to Claude Desktop through its config file. Every command and import below was checked against the official MCP Python SDK documentation for its current v2 release.

If MCP is new to you, read what MCP is first. The five minutes will make every step below easier.

What is an MCP server?

An MCP server is a program that exposes tools, resources and prompts to AI applications through the Model Context Protocol, so any MCP-compatible app can use them without custom integration code.

The protocol defines three roles:

  • Host: the AI application the user talks to, such as Claude Desktop or an IDE.
  • Client: the connector inside the host that speaks MCP to one server.
  • Server: your program, which offers capabilities.

A server can offer three kinds of capabilities. Tools are functions the model can call, like search_notes. Resources are read-only data the app can load, like a file. Prompts are reusable templates. This project focuses on tools, the most common starting point.

The idea is close to building tools for an AI agent, with one difference: instead of wiring tools into your own agent loop, you publish them in a standard format that other people's agents can call.

What do you need to build an MCP server in Python?

You need Python 3.10 or newer, the official mcp package with its CLI extra, and an MCP host to test with. No API key is needed, because the host brings the model.

PartWhat we useWhy
LanguagePython 3.10+Minimum version for the SDK
SDKmcp (official Python SDK, v2) with the cli extraGives you MCPServer and the mcp command
Package manageruv (recommended) or pipThe official docs use uv
Testing UIMCP InspectorCalls your tools from a browser form
HostClaude Desktop (or Claude Code, Cursor, VS Code)Lets a real model use your tools
Node.jsRecent versionThe Inspector is a Node.js app

One warning before you copy older tutorials: in v1 of the SDK the main class was FastMCP, imported from mcp.server.fastmcp. Version 2 renamed it to MCPServer, imported from mcp.server. If you see ModuleNotFoundError for mcp.server.fastmcp, you are mixing a v1 tutorial with the v2 package.

How does an MCP server talk to an AI app? (architecture)

For a local server, the host starts your Python file as a child process and exchanges JSON-RPC messages with it over standard input and standard output (the stdio transport).

  1. Claude Desktop reads its config file and launches your server with the command you gave it.
  2. It asks the server which tools it has. The SDK answers using your function names, type hints and docstrings.
  3. When a user's request needs a tool, the model chooses it and the host sends a call with arguments.
  4. Your function runs on your machine and returns a result, which goes back to the model.

Because stdout carries protocol messages, a stray print() in a stdio server corrupts the stream. Log to stderr instead. For servers other people reach over a network, the SDK also supports the Streamable HTTP transport, but stdio is the right choice for a personal tool on your own laptop.

How to build an MCP server in Python, step by step

  1. Step 1. Set up the project

    Create a project with uv and add the SDK with its CLI extra. The cli extra installs the mcp command used for testing and installing.

    uv init notes-mcp
    cd notes-mcp
    uv add "mcp[cli]"
    mkdir notes

    Using pip instead? Create and activate a virtual environment, then run pip install "mcp[cli]". Put a few .md or .txt files in the notes folder: lecture notes, a to-do list, interview prep. Small files are perfect for a first test.

  2. Step 2. Create the server and your first tool

    Create server.py. The MCPServer object is your server, and each function decorated with @mcp.tool() becomes a tool. The type hints become the input schema and the docstring becomes the description the model reads.

    # server.py
    from pathlib import Path
    from mcp.server import MCPServer
    from mcp.server.mcpserver.exceptions import ToolError
    
    mcp = MCPServer("Notes")
    NOTES = Path(__file__).resolve().parent / "notes"  # absolute, so any host finds it
    
    def note_files():
        return sorted(p for p in NOTES.iterdir() if p.suffix in {".md", ".txt"})
    
    @mcp.tool()
    def list_notes() -> list[str]:
        """List the file names of all notes (.md and .txt)."""
        return [p.name for p in note_files()]

    The NOTES path is built from the file's own location, not the current folder. Hosts launch servers from their own working directory, so a relative path like "notes" would point somewhere unexpected.

  3. Step 3. Add search and read tools

    Add two more tools below the first. search_notes finds matching lines across every note; read_note returns one whole file but refuses anything outside the notes folder.

    @mcp.tool()
    def search_notes(query: str, max_results: int = 10) -> list[str]:
        """Find lines in the notes that contain the query (case-insensitive)."""
        hits = []
        for p in note_files():
            for no, line in enumerate(p.read_text(encoding="utf-8").splitlines(), 1):
                if query.lower() in line.lower():
                    hits.append(f"{p.name}:{no}: {line.strip()}")
        return hits[:max_results] or ["No matches."]
    
    @mcp.tool()
    def read_note(name: str) -> str:
        """Return the full text of one note, e.g. dbms.md."""
        path = (NOTES / name).resolve()
        if path.parent != NOTES or not path.is_file():
            raise ToolError(f"No note called {name!r}. Call list_notes first.")
        return path.read_text(encoding="utf-8")[:8000]
    
    if __name__ == "__main__":
        mcp.run()  # stdio by default: the host talks to it over stdin/stdout

    Raising ToolError sends a clear error message back to the model instead of crashing, so the model can recover by calling list_notes. The path check blocks requests such as ../../secrets.txt.

  4. Step 4. Test it in the MCP Inspector

    The MCP Inspector is the official browser-based tester for MCP servers. Start your server inside it with one command:

    uv run mcp dev server.py

    Open the URL it prints, connect, and go to the Tools tab. You should see list_notes, search_notes and read_note, each with a form built from your type hints. Call search_notes with a word from your notes, then try read_note with a file that does not exist to see your ToolError message.

    The Inspector runs on Node.js, so mcp dev needs npx on your PATH. Its README lists the minimum Node version (22.19 or newer at the time of writing). You can also start the Inspector directly with npx @modelcontextprotocol/inspector and point it at your server's launch command.

  5. Step 5. Write a quick automated test

    The SDK's Client can connect to your server object in memory, with no subprocess or port, which makes testing fast.

    # test_client.py
    import anyio
    from mcp import Client
    from server import mcp
    
    async def main():
        async with Client(mcp) as client:
            tools = await client.list_tools()
            print([t.name for t in tools.tools])
            result = await client.call_tool("search_notes", {"query": "exam"})
            print(result.structured_content)
            bad = await client.call_tool("read_note", {"name": "../secrets.txt"})
            print(bad.is_error, bad.content[0].text)
    
    anyio.run(main)

    Run it with uv run test_client.py. You should see the three tool names, a dictionary whose result key holds the matching lines, and True followed by your error message for the blocked path. A test you can rerun after every change is what separates a project from a demo.

  6. Step 6. Connect the server to Claude Desktop

    The quickest way is the SDK's install command, which writes the config entry for you with absolute paths:

    uv run mcp install server.py

    To do it by hand, open claude_desktop_config.json. On macOS it lives at ~/Library/Application Support/Claude/claude_desktop_config.json and on Windows at %APPDATA%\Claude\claude_desktop_config.json. Add your server under the mcpServers key:

    {
      "mcpServers": {
        "notes": {
          "command": "uv",
          "args": ["--directory", "C:/Users/you/notes-mcp", "run", "server.py"]
        }
      }
    }

    Use the absolute path to your project folder. If Claude Desktop cannot find uv, put the full path to the uv executable in command (find it with where uv on Windows or which uv on macOS). Then fully quit Claude Desktop, not just its window, and reopen it.

  7. Step 7. Try it with real questions

    Ask Claude Desktop questions that need your tools:

    • "List my notes."
    • "Search my notes for normalisation and explain what I wrote."
    • "Read dbms.md and make five quiz questions from it."

    The host asks your permission before it runs a tool, and you can see which tool it called with which arguments. Note how the model chains tools on its own: it may call search_notes first, then read_note on the best match. If it picks the wrong tool, improve that tool's docstring, restart the host and try again.

What are good MCP server project ideas?

Good MCP server projects wrap one data source or service you actually use, with two to four focused tools. Ideas that work well for students and freshers:

  • College timetable server: reads a timetable CSV and answers "what is my next class?" and "when is the DBMS lab?"
  • Expense tracker server: tools to add an expense to a CSV, total by category and show the month's summary.
  • GitHub issues helper: lists open issues in your repo and drafts a summary, using a token kept in an environment variable.
  • SQLite query server: read-only SQL over a sample database, a great way to show safe tool design.
  • Placement tracker: stores companies, rounds and dates, and answers "which interviews are this week?"
  • Notes server with semantic search: replace keyword search with embeddings, the same retrieval idea as a RAG chatbot project.

For a final-year version, add resources and prompts alongside tools, run it over Streamable HTTP so teammates can connect, and write tests with the in-memory Client for every tool.

How do I explain my MCP server project in a viva or interview?

Lead with one sentence: "I built an MCP server in Python that exposes three tools for searching and reading my notes, so any MCP-compatible assistant, such as Claude Desktop, can use my data without custom integration code."

Then be ready for:

  • What problem does MCP solve? (One standard interface instead of a separate integration for every app and every tool.)
  • What are hosts, clients and servers?
  • What is the difference between a tool, a resource and a prompt?
  • Why can a stdio server not print to stdout?
  • How do you stop the model from reading files outside the notes folder?
  • How is an MCP server different from an AI agent? (The server offers tools; the agent decides when to use them.)

Our agentic AI interview questions page covers MCP and tool calling in more depth.

Common mistakes when building an MCP server

Most first MCP servers break for the same few reasons:

  1. Printing to stdout. In a stdio server, print() corrupts the protocol stream. Use the logging module, which writes to stderr by default.
  2. Relative paths. The host starts your server from its own folder, so build paths from Path(__file__) and use absolute paths in the config.
  3. Mixing SDK versions. v1 tutorials import FastMCP from mcp.server.fastmcp; the v2 package uses MCPServer from mcp.server.
  4. Not restarting the host. Claude Desktop reads its config at startup. Fully quit it after every config change.
  5. Exposing too much. A tool that reads any path on disk is a security hole. Restrict tools to one folder and make write tools ask for confirmation.
  6. Vague docstrings. The model chooses tools from their descriptions. "Search notes" is weaker than "Find lines in the notes that contain the query."

Fix these and your server will behave the same in the Inspector, in tests and in a real host.

Frequently asked questions

How do I build an MCP server from scratch in Python?

Install the official SDK with pip install "mcp[cli]" or uv add "mcp[cli]", create an MCPServer object, decorate functions with @mcp.tool(), and call mcp.run() under an if __name__ check. Test it with mcp dev server.py in the MCP Inspector, then add it to an MCP host such as Claude Desktop.

What is the difference between FastMCP and MCPServer?

They are the same idea at different versions. Version 1 of the official MCP Python SDK called the high-level server class FastMCP, imported from mcp.server.fastmcp. Version 2 renamed it to MCPServer, imported from mcp.server. A separate community project also uses the FastMCP name, so check which package a tutorial installs.

Which language is best to build an MCP server?

Python and TypeScript have the most mature official SDKs and the most examples, so they are the easiest places to start. Official SDKs also exist for other languages such as Java, Kotlin, C# and Go. Pick the language your data source or team already uses.

Do I need an API key to build an MCP server?

No. The server only exposes tools; the host application, such as Claude Desktop, brings the model and its account. You only need API keys if your tools themselves call an external service, and those keys should be passed through environment variables, not written into code.

Can I use my MCP server with apps other than Claude?

Yes. Any MCP-compatible host can launch the same server with the same command, including Claude Code, Cursor and VS Code, each with its own config location. That portability is the main reason to build an MCP server instead of a custom plugin for one app.

References