Level : Advanced

How to build an MCP server: principles and steps with the official SDK

By The Info Agent IA editorial team ·

A hand assembles geometric blocks on a glowing workbench, linked by cables of light to a central socket.

Building an MCP server means writing a small program that exposes your functions, data or prompt templates in a standard form that any compatible AI application can discover and use. With the official SDK, a useful first server fits in about thirty lines of Python: this tutorial takes you from installation to plugging it into an assistant, then through the security rules.

What you will build

We will create a “notes” server that gives an AI read access to a folder of Markdown notes on your computer. It will offer all three building blocks of the protocol: a search tool, a resource to read a note and a ready-made prompt to summarize it.

Prerequisites:

  • working knowledge of Python and the command line;
  • Python 3.11 or later, as required by the SDK installation page;
  • the uv tool, which manages the environment and dependencies;
  • an MCP-compatible host application, here Claude Desktop, for the final test.

If the Model Context Protocol is still fuzzy, start with our explainer on MCP, the Model Context Protocol: host, client and server roles, transports, security.

The three building blocks in code

An MCP server invents nothing: it describes functions and data so the model knows when and how to use them. In the Python SDK, each building block is a decorator placed on an ordinary function.

Building blockPython decoratorWho triggers itIn our server
Tool@mcp.tool()The model, with user approvalFind notes containing a word
Resource@mcp.resource(...)The applicationRead the content of a note
Prompt@mcp.prompt()The userAsk for a structured summary

Tools are the block agents use most: they are the standardized version of the tool calling that large models already perform. The function name becomes the tool name, the docstring becomes its description and the type hints become the parameter schema.

Choosing an SDK

The official site ranks SDKs in tiers based on completeness, protocol support and maintenance commitment. Here is the picture as of 22 October 2026:

TierLanguages
Tier 1TypeScript, Python, C#, Go, Rust, Ruby
Tier 2Java
Tier 3Swift, PHP, Kotlin

All of them let you build servers exposing tools, resources and prompts, locally or remotely. This tutorial uses Python; a TypeScript variant follows further down.

Step 1: set up the project

Install uv if you have not already (the official tutorial gives the command for each operating system), then create the project and add the SDK with its command-line extras:

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

The cli extra adds the mcp command, used among other things to launch the Inspector. Then create a server.py file and a notes folder in your home directory, with two or three .md files to experiment with.

Step 2: write the server

Here is the complete server. It follows the structure of the official tutorial: an MCPServer instance, decorated functions, then a launch in stdio mode.

import logging
from pathlib import Path

from mcp.server import MCPServer

logger = logging.getLogger(__name__)
mcp = MCPServer("notes")
FOLDER = (Path.home() / "notes").resolve()


@mcp.tool()
def search_notes(keyword: str) -> list[str]:
    """List the Markdown notes whose text contains the keyword."""
    logger.info("Search: %s", keyword)
    return [
        f.name
        for f in FOLDER.glob("*.md")
        if keyword.lower() in f.read_text(encoding="utf-8").lower()
    ]


@mcp.resource("notes://{name}")
def read_note(name: str) -> str:
    """Return the content of a note from the folder."""
    path = (FOLDER / name).resolve()
    if path.parent != FOLDER:
        raise ValueError("Access denied outside the notes folder")
    return path.read_text(encoding="utf-8")


@mcp.prompt()
def summarize_note(name: str) -> str:
    """Prepare a request to summarize a note."""
    return f"Summarize the note {name} in five bullets, decisions first."


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

Three details matter. First, the docstring is not decoration: it is what the model reads to decide whether to call the tool, so write it like a precise instruction. Second, the resource checks that the requested file stays inside the allowed folder, which blocks a request such as ../../.ssh/config. Third, mcp.run() with no argument uses the stdio transport, the one desktop applications expect.

Step 3: never write to standard output

This is the most common mistake. In stdio mode, the host and the server exchange JSON-RPC messages over standard input and output. The official documentation is explicit: writing to standard output corrupts those messages and breaks the server. A single debugging print() is enough to do it.

Use the logging module, which writes to standard error, as our code does. In TypeScript, the equivalent rule is to replace console.log() with console.error(). With an HTTP server, the constraint goes away.

Step 4: test with the Inspector

Before plugging the server into an assistant, test it on its own. The SDK ships a command that starts it and opens the Inspector, an official testing interface:

uv run mcp dev server.py

In the Inspector, check that the tool, the resource and the prompt are listed, call search_notes with a word that appears in your files, then try to read a note that does not exist to see how the error comes back. For automated tests, the SDK documentation shows how to open an in-memory Client on your server with pytest, with no network involved.

Step 5: connect the server to Claude Desktop

Open Claude Desktop’s claude_desktop_config.json configuration file (the official tutorial gives its location on macOS, Windows and Linux) and declare your server under the mcpServers key:

{
  "mcpServers": {
    "notes": {
      "command": "uv",
      "args": ["--directory", "/ABSOLUTE/PATH/TO/notes-mcp", "run", "server.py"]
    }
  }
}

The path must be absolute; on Windows, double the backslashes or use forward slashes. If the app cannot find uv, put its full path in command. Quit the application completely (closing the window is not enough, the documentation notes), restart it, then check that “notes” shows up in the connectors menu. Finally, ask: “Which notes mention the budget?”

If something goes wrong, read the application logs: each server gets a log file that collects its standard error, and therefore your logging messages.

Check yourself Your server works in the Inspector but crashes as soon as Claude Desktop calls it. You had added a print() to trace searches. What is the connection?

In stdio mode, print() writes to standard output, the very channel that carries protocol messages. The debugging text gets mixed into the JSON-RPC messages and the host can no longer parse them. Replace it with logger.info(), which writes to standard error, and find those lines in the server log.

Variant: a remote server over HTTP

To share a server across several applications or colleagues, switch to the Streamable HTTP transport. According to the Python SDK documentation consulted on 22 October 2026, you only need to change the last line:

if __name__ == "__main__":
    mcp.run(transport="streamable-http")

By default the server then listens on the local address 127.0.0.1, port 8000, at the /mcp path. Putting it online then requires authentication (the specification recommends OAuth), HTTPS and some thought about each user’s permissions. The older SSE transport is deprecated.

What about TypeScript?

The official tutorial follows the same approach with the @modelcontextprotocol/server package and the zod validation library. One key point in the current version: you pass serveStdio a function that builds a fresh server instance on every call.

import { McpServer } from "@modelcontextprotocol/server";
import { serveStdio } from "@modelcontextprotocol/server/stdio";
import { z } from "zod";

function createServer() {
  const server = new McpServer({ name: "notes", version: "1.0.0" });
  server.registerTool(
    "search_notes",
    {
      description: "List the notes containing a keyword",
      inputSchema: z.object({ keyword: z.string() }),
    },
    async ({ keyword }) => ({
      content: [{ type: "text", text: "Searching for " + keyword }],
    }),
  );
  return server;
}

serveStdio(createServer);

The documentation says a server wired the old way with server.connect(new StdioServerTransport()) still compiles, but only serves 2025-era clients.

Security rules from your very first server

An MCP server gives an AI hands. The specification points out that tools amount to code execution and that the host must obtain user consent before calling them. On the server side, keep four habits:

  1. Least privilege. Expose one folder, not the whole disk; read access, not write access, until you actually need it.
  2. Input validation. Parameters come from a model, and so indirectly from text you do not control: check paths, identifiers and sizes.
  3. Distrust of returned content. A note or email read by your tool may contain a prompt injection, that is, hidden instructions trying to hijack the agent.
  4. Human confirmation for sensitive actions. For any tool that sends, pays or deletes, keep a human in the loop.

Our guide to securing an AI agent covers these protections in detail.

Using your server from an agent

An MCP server is not only for desktop assistants: most agent frameworks can consume one. The Claude Agent SDK, the OpenAI Agents SDK or Mastra can load your “notes” server without any custom integration code.

ToolTypeLicenseMCPMulti-agentLevel
Claude Agent SDK CodeproprietaryYesYesIntermediate
OpenAI Agents SDK CodeMITYesYesIntermediate
LangGraph CodeMITYesYesAdvanced
Mastra CodeApache-2.0YesYesIntermediate
goose Low-codeApache-2.0YesYesBeginner
Source: editorial survey (official docs and repositories), data as of October 22, 2026 · method.

To choose, see our comparison of AI agent frameworks or the framework comparator, which you can filter on MCP. The server then slots into the architecture described in our guide to building an AI agent, from no-code to code. Once it is running, measure whether the agent calls the right tools at the right moment: that is the subject of our article on evaluating an AI agent.

Frequently asked questions

Do I need to be a developer to build an MCP server?

You need basic programming skills, in Python or TypeScript for example. The official SDK handles the protocol work: you write ordinary functions and it generates their description for the AI. If you do not code, use existing servers or an MCP-compatible no-code tool instead.

Which language should I pick for my first MCP server?

The one you know best among the official SDKs. Python and TypeScript have the most detailed official tutorial and sit in the top tier, alongside C#, Go, Rust and Ruby. Because the protocol is the same everywhere, a Python server works with a client written in any other language.

Does an MCP server have to run on the internet?

No. For personal use, the simplest option is a local server, launched by the AI app on your computer and talking over standard input and output. The HTTP transport is for cases where several people or applications need to reach the same server remotely.

Does my old server built with FastMCP still work?

Version 2 of the Python SDK renamed the FastMCP class to MCPServer and follows the specification dated 28 July 2026. The official documentation offers a migration guide. Test your server in the Inspector after upgrading and before redeploying it.

Go further