Skip to content
AI Engineering
Building with the API

Building an MCP server

Wrapping your own tool or data source as an MCP server lets any compatible app use it. The pieces are small.

Chapter 7: Building with the APILesson 9 of 11Level: CoreAbout 15 min
Words on this page (9)
tool
: A function the model can ask your program to run, described by a name, description and argument schema.
MCP
: Model Context Protocol: a standard for plugging tools and data into AI apps.
agent
: A model run in a loop that chooses its own tools and steps until the task is done.
prompt
: The input text you give a model, including instructions, examples and data.
stdio
: Standard input and output: how a local program talks to the program that started it, used by local MCP servers.
SDK
: Software development kit: a library that wraps an API for a programming language.
schema
: A description of the shape of data: which fields exist and what types they have.
logarithm
: The power you raise a base to in order to get a number. log base 2 of 8 is 3 because 2 to the power 3 is 8.
OAuth
: A standard way to let an app act for you without giving it your password.

The architecture

A host is the app the user runs, such as a coding or a chat client. Inside it, an client keeps one connection to each server. A server exposes three kinds of things: (functions the model can call), resources (data it can read, such as files) and (reusable templates). The transport carries the messages. runs the server as a local subprocess and is the simplest. Streamable HTTP serves remote servers and adds authentication.

Building one

Pick an in Python or TypeScript. Define each tool with a name, a clear description and an input . Return concise text the model can use. Validate arguments and handle errors with messages that say what to try next. Test with the MCP Inspector, then connect it to your client. Keep the server narrow: a few well-described tools beat dozens of thin ones, since descriptions load into context.

Safety

A server runs with whatever access you give it. Scope credentials to the minimum, calls, and treat every argument as untrusted input. For remote servers, use rather than static keys.

A worked example: the shape of a small server

Name: notes. Tool 1: search_notes(query) returns up to five matching snippets with file names. Tool 2: add_note(title, text) writes a new file (needs approval). Resource: note://{id} returns one note. The server runs locally. You test it in the Inspector, then in your agent.

Design tips

Few tools, clear names, short descriptions that say when to use them. Return small answers: five snippets, not the whole folder. Return helpful errors: "No notes match 'taxes'. Try fewer words."

How a call reaches your code

The SDK reads each function when the server starts. It takes the name, the docstring (the text in quotes under the function line) and the type hints, and builds the description and argument schema the model sees.

  1. A client connects and asks for the tool list.
  2. The model picks search_notes and sends the query.
  3. The SDK checks the arguments against the schema.
  4. Your function runs and returns text.
  5. The SDK wraps that text in a protocol message and sends it back.

So a vague docstring means a vague tool. The model never sees your code, only that description.

A second example: adding a tool that writes

Add add_note(title, text). A title like "../../.bashrc" could point outside your notes folder. Build the path, resolve it, and refuse the call unless the result still sits inside the notes folder. Also require the host to ask for approval before any write. Reading a few lines is low risk. Writing files is not.

What goes wrong

The tool does not show up in the client. The server crashed at startup. Run it directly in a terminal and read the error before blaming the client.

The client connects but replies are garbled. A stray print statement went to standard output, which carries the protocol. Send debug text to standard error.

The model never uses your tool. The description does not say when to use it. Try five different prompts in the Inspector and rewrite the description until it triggers.

The reply floods the context. Cap results, as the example does with five lines.

Try it yourself

Add a second tool, list_notes, that returns up to 20 file names. Open the Inspector and confirm it lists two tools. Call each with an empty notes folder. Expect friendly messages, not crashes. Then ask your agent "what notes do I have?" and check which tool it chooses.

Common mistake

Giving the server broad access "just in case". Point it at one folder and nothing else.

The code

With the official Python SDK (pip install mcp, version 2 or later), a small server is a few lines. Each function with a docstring becomes a tool whose description the model reads. Older tutorials import FastMCP from mcp.server.fastmcp. Version 2 renamed it MCPServer, so use the import below or pin mcp<2 for the old code.

from mcp.server.mcpserver import MCPServer
from pathlib import Path

mcp = MCPServer("notes")
NOTES = Path("~/notes").expanduser()  # the one folder this server may read

@mcp.tool()
def search_notes(query: str) -> str:
    """Search my notes for a word or phrase. Returns up to five matching lines with the file name."""
    hits = []
    for path in NOTES.glob("*.md"):
        for line in path.read_text().splitlines():
            if query.lower() in line.lower():
                hits.append(f"{path.name}: {line.strip()}")
    return "\n".join(hits[:5]) or f"No notes match '{query}'. Try fewer words."

if __name__ == "__main__":
    mcp.run()  # talks over stdio, so a local app can start it

Test it with npx @modelcontextprotocol/inspector python server.py, then add it to your coding agent's MCP settings.

Check yourself

What are the three things an MCP server can expose?

Which transport is simplest for a local server?

Your notes server returns every matching line from 400 files. What is wrong?

Go deeper

Spotted a mistake or something unclear? Report it on GitHub.

On this page