FRENES

Build your own MCP server in 30 minutes

By · · 6 min

Hands solder a new plug onto a cable beside a kitchen timer

You can build an MCP server in around thirty lines of Python with the official SDK: write functions with type annotations, and the SDK derives the parameter schemas and handles the protocol. Allow 30 minutes to create a first server with two tools and a resource, test it in the Inspector, and connect it to Claude, Cursor, or VS Code. This tutorial uses version 2 of the Python SDK, released on July 28, 2026 (v2.3.0 as of October 2, 2026).

What you will build

A “Notes” server that gives your AI assistant read-only access to a folder of Markdown notes:

  • a chercher_notes tool that returns notes containing a word;
  • a lire_note tool that returns a note’s contents;
  • a notes://liste resource that lists the files.

Read-only access is a deliberate choice: it is the ground rule for a first server. A tool that writes, deletes, or sends something could be triggered accidentally, or through a prompt injection hidden in a document the agent has read.

If these terms are new to you, MCP servers: what they are and which ones to know covers the basics. You can also find a definition of MCP server in the glossary.

Prerequisites

  • Python 3.11 or later, as required by SDK v2.
  • uv, the Python package manager recommended in the SDK documentation. pip also works.
  • An MCP host for the final test: Claude Desktop, Claude Code, Cursor, or VS Code with GitHub Copilot.
  • A folder containing a few .md files to use for testing.

The three building blocks of an MCP server

The Python SDK documentation distinguishes them by who decides to use them:

Building blockWho triggers itWeb equivalentExample
ToolThe modelPOST request: takes action, may change dataSearch a database, create a ticket
ResourceThe host applicationGET request: reads without changing anythingThe contents of a file
PromptThe userSaved commandA “/summarize” command

The server never talks directly to the model. The MCP client running inside the host passes calls to the server.

Step 1: create the project

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

The [cli] extra installs the mcp command, including mcp dev, mcp run, and mcp install, alongside the SDK. If you use pip, run pip install "mcp[cli]".

Step 2: write the server

Create server.py:

from pathlib import Path

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

# Folder exposed with read-only access. Nothing outside this folder is accessible.
NOTES = Path.home() / "notes"

mcp = MCPServer("Notes")


def _fichier(nom: str) -> Path:
    """Return a note's path, rejecting paths outside the NOTES folder."""
    chemin = (NOTES / nom).resolve()
    if not chemin.is_relative_to(NOTES.resolve()) or chemin.suffix != ".md":
        raise ToolError(f"Note refusée : {nom!r}")
    if not chemin.is_file():
        raise ToolError(f"Aucune note nommée {nom!r}")
    return chemin


@mcp.tool()
def chercher_notes(mot: str) -> list[str]:
    """List Markdown notes containing this word, ignoring case."""
    mot = mot.lower()
    return sorted(
        f.name for f in NOTES.glob("*.md")
        if mot in f.read_text(encoding="utf-8", errors="ignore").lower()
    )


@mcp.tool()
def lire_note(nom: str) -> str:
    """Return the full contents of a note, such as 'reunion-lundi.md'."""
    return _fichier(nom).read_text(encoding="utf-8", errors="ignore")


@mcp.resource("notes://liste")
def liste() -> str:
    """List all available notes, one per line."""
    return "\n".join(sorted(f.name for f in NOTES.glob("*.md")))


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

Three points deserve attention:

  1. Type annotations define the schema. mot: str is enough: the SDK generates the JSON schema the client presents to the model. You do not have to write parameter validation or request parsing yourself.
  2. The docstring becomes the description the model reads when choosing a tool. Write it carefully: it helps determine whether the model calls the tool at the right time.
  3. _fichier prevents access outside the folder. Without this check, a name such as ../.ssh/id_rsa could let the server read any file your account can access. This is the most common flaw in servers that access the filesystem.

If you are coming from version 1 of the SDK, FastMCP is now called MCPServer, and the mcp.server.fastmcp import no longer exists. The migration guide lists the other changes.

Step 3: test in the Inspector

uv run mcp dev server.py

This command starts the MCP Inspector and displays a local address to open in your browser. Check three things:

  • In the Tools tab, chercher_notes and lire_note appear with their descriptions and an input field.
  • Calling lire_note with ../secret.md returns the expected “Note refusée” error, meaning the note was rejected.
  • In the Resources tab, notes://liste returns your files.

Always test the server on its own before connecting it to a host. Errors are much easier to understand here than when they are passed through a host application.

Step 4: connect it to your assistant

All these hosts work the same way: they start your server as a process and communicate with it through standard input and standard output, using the stdio transport. You only need to provide the launch command, always with an absolute path:

uv run --with "mcp[cli]" mcp run /chemin/absolu/vers/server.py

Claude Desktop — the SDK writes the configuration for you:

uv run mcp install server.py

Quit Claude Desktop completely, rather than just closing its window, then reopen it.

Claude Code — run this command:

claude mcp add notes -- uv run --with "mcp[cli]" mcp run /chemin/absolu/vers/server.py

Type /mcp in a session to check that notes is connected.

Cursor — create .cursor/mcp.json at the root of your project:

{
  "mcpServers": {
    "notes": {
      "command": "uv",
      "args": ["run", "--with", "mcp[cli]", "mcp", "run", "/chemin/absolu/vers/server.py"]
    }
  }
}

VS Code — use .vscode/mcp.json. The configuration key is servers, and each entry specifies its transport type:

{
  "servers": {
    "notes": {
      "type": "stdio",
      "command": "uv",
      "args": ["run", "--with", "mcp[cli]", "mcp", "run", "/chemin/absolu/vers/server.py"]
    }
  }
}

These commands and configuration files come from the Connect to a real host page in the official documentation. If you want to install an existing server rather than build your own, follow Install an MCP server in Claude, ChatGPT, or Cursor.

Step 5: try it on a real task

Ask your assistant: “Find the decisions about the budget in my notes and summarize them.” It should call chercher_notes with “budget,” then call lire_note for the files it finds. If your host is configured to require approval, it should ask permission before each call. Keep that approval step enabled until you have observed the server’s behavior over several days.

Common problems

SymptomLikely causeSolution
The server does not appear in the hostThe path to server.py is relativeUse an absolute path, then restart the host
The host reports “command not found: uv”The host starts the server with a minimal PATHReplace uv with its absolute path, found with which uv or where uv
Configuration changes have no effectThe host reads its configuration at startupQuit the host completely and reopen it
The server crashes on the first callA print() writes to standard output, which is reserved for the protocol when using stdioSend logs to standard error with sys.stderr or use logging
ImportError: mcp.server.fastmcpThe code was written for SDK v1Switch to from mcp.server import MCPServer

Also run the launch command manually: a healthy stdio server prints nothing and waits. A traceback or an immediate exit points to the underlying problem.

Going further

  • Share it: uv run mcp run server.py --transport streamable-http serves the same server over HTTP at http://localhost:8000/mcp. Before exposing it to the internet, add authentication—the SDK supports OAuth—and restrict what each tool can do.
  • Test it automatically: the SDK provides a Client that connects directly to the mcp object in memory, without a separate process or network port. This is useful for pytest tests.
  • Publish it: the official MCP registry lists public servers. Our MCP server directory includes them alongside repository activity and a risk rating.
  • Choose an existing server instead of writing one: a server may already cover your needs. How to choose an MCP server explains how to check whether it is maintained and safe to use. Malicious MCP servers and skills: check before installing covers the pitfalls.

Checklist before using it every day

  • The server works on its own with mcp dev, and every tool has a clear docstring
  • Accepted paths, addresses, or queries are restricted to what is strictly necessary
  • No tool deletes, sends, or pays for anything without human approval
  • No secrets, such as API keys or passwords, are written in server.py: pass them through environment variables
  • The path to server.py in the host configuration is absolute
  • You have run the AI agent security assessment on your entire setup

Frequently asked questions

Do I need programming skills to build an MCP server?

You need basic Python or TypeScript skills: writing a function, installing a package, and running a script. The official SDK handles the protocol, parameter schemas, and validation, so a useful server can fit into around thirty lines.

Should I use Python or TypeScript for an MCP server?

Both SDKs are official and maintained by the Model Context Protocol project. Choose the language that matches the tool you want to expose: Python for data scripts or automation, TypeScript if your existing code runs on Node.js. The protocol is the same, so clients see no difference.

Can other people use my MCP server?

With the local stdio transport, only your computer runs it. To share it, serve the same code over Streamable HTTP at a web address with authentication. The SDK provides the transport, but hosting, access control, and security remain your responsibility.

What is the difference between an MCP tool and a resource?

A tool is a function the model chooses to call to take action or retrieve information, much like a POST request. A resource is data the application loads into the model's context without side effects, much like a GET request. A third type, a prompt, is a message template the user invokes by name.

In the same section

Neutrality. No affiliate links. No vendor pays to be reviewed, rated or ranked. Reviews are directed by François Chollet, neutral and factual, and every data point is dated and sourced. How we score