# Building your own MCP server
A connector is just a server that speaks one protocol, and in the next thirty minutes you are going to write one that gives Claude a get_order_status tool it can call on your behalf. We will run it locally first, then expose it as a remote connector, then say a careful word about who is allowed to call it.
You already know MCP (the Model Context Protocol) as the open standard that lets Claude reachreachThe number of unique people exposed to your message in a given period. Unlike impressions, reach counts each person once, no matter how often they see it.Voir la définition complète → tools and data outside its context windowcontext windowThe context window is the maximum amount of text (measured in tokens) a language model can process at once, including both the input prompt and the generated output.Voir la définition complète →. Now you build the other side of that handshake: the server.
An MCP server is a small program that advertises a list of capabilities and waits for a client to call them. The three capability types are tools (functions the model can invoke), resources (read-only data the model can pull in), and prompts (reusable templates). For a connector that answers "where is my order," you want a tool.
The client is the host application: Claude Desktop, the Claude apps, Claude Code, or your own code using the Agent SDK. The client decides *when* to call your tool. Your server only decides *what the tool does*. That separation is the whole point. You never touch the model. You publish a clean function signature and a description, and Claude figures out when calling it helps.
Two transports matter:
You write the tool logic once. The transport is a few lines at the bottom. Start with stdio.
Install the official Python SDK first. The mcp package ships a FastMCP helper that handles the protocol plumbing so you write almost nothing but your own function.
pip install "mcp[cli]"Now the server. This is the whole thing.
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("order-tools")
# A tiny stand-in for your real database or internal API.
ORDERS = {
"A1001": {"status": "shipped", "carrier": "DHL", "eta": "2026-02-14"},
"A1002": {"status": "processing", "carrier": None, "eta": None},
}
@mcp.tool()
def get_order_status(order_id: str) -> dict:
"""Look up the current status of a customer order by its ID.
Args:
order_id: The order reference, e.g. 'A1001'.
"""
order = ORDERS.get(order_id.strip().upper())
if order is None:
return {"found": False, "order_id": order_id}
return {"found": True, "order_id": order_id, **order}
if __name__ == "__main__":
mcp.run()Read it top to bottom:
FastMCP("order-tools") creates the server and names it. That name shows up in the client UI.@mcp.tool() decorator registers the function as a callable tool. The SDK reads your type hints (order_id: str, return dict) to build the input and output schemaschemaA schema is the formal blueprint that defines how data is structured, named, typed, and related within a database, file, or message.Voir la définition complète → automatically. No JSON SchemaSchemaA schema is the formal blueprint that defines how data is structured, named, typed, and related within a database, file, or message.Voir la définition complète → by hand.Args: section documents each parameter. Write it like you are briefing a smart new colleague who cannot see your code.mcp.run() with no arguments defaults to stdio transport. That is all you need locally.In the real version, the body of get_order_status calls your order system: a SQLSQLSales Qualified Lead: a prospect the sales team has validated as ready for direct outreach and a proposal, having passed clear qualification criteria.Voir la définition complète → query, an internal REST endpoint, a Stripe lookup. The MCP layer never changes. You are wrapping an existing capability, not rebuilding it.
Claude Desktop reads a small config file that lists the local servers it should launch. On macOS it lives at ~/Library/Application Support/Claude/claude_desktop_config.json. Add your server:
{
"mcpServers": {
"order-tools": {
"command": "python",
"args": ["/absolute/path/to/server.py"]
}
}
}Restart Claude Desktop. The app launches your script as a subprocess, asks it for its tool list, and shows order-tools in the connector menu. Now type: *"What's the status of order A1001?"* Claude sees the tool, calls get_order_status("A1001"), gets the JSON back, and answers in plain language. You will be asked to approve the call the first time. That approval step is the client protecting you, and it is deliberate.
If nothing shows up, run python /path/to/server.py in a terminal first to catch import errors, then use the MCP Inspector (mcp dev server.py) to poke the tool directly before involving Claude at all. Debug the server in isolation; debug the connection second.
For the official walkthrough and the latest SDK details, keep the MCP server quickstart open in a tab. The protocol moves, and that page is the source of truth.
A local stdio server only helps the person running it on their own machine. To let a *team* use your connector, or to list it in the connector marketplace, it has to run as a remote HTTP service at a URL.
The code change is almost nothing. Swap the transport:
if __name__ == "__main__":
mcp.run(transport="streamable-http")Now your server listens on an HTTP endpoint instead of stdio. Deploy it like any web service: a container on your cloud of choice, behind HTTPS, on a stable hostname such as https://tools.yourco.com/mcp. Anthropic's docs cover the remote connector requirements, including the Streamable HTTP transport the Claude apps expect.
In the Claude apps, a user (or an admin, for an organization) adds your connector by URL under Settings, then Connectors. From that moment your tool appears alongside the built-in ones in Projects, in regular chats, and to managed agents. One server, many surfaces.
The hard part of going remote is not the transport. It is the question the stdio version let you ignore: who is calling, and what are they allowed to see?
Vérification des acquis
1. In the MCP architecture described in the lesson, which component decides WHEN a tool is actually called?
2. According to the lesson, which transport should you start with when first building an MCP server, and why?
3. How does MCP relate to Claude's context window, as framed in the lesson?
4. Select ALL correct answers about the capability types an MCP server can advertise.
Sélectionnez toutes les réponses correctes.
5. Select ALL correct answers about stdio and Streamable HTTP transports as described in the lesson.
Sélectionnez toutes les réponses correctes.
The moment your server is reachable over the internet, get_order_status("A1001") is a problem. Order A1001 belongs to *someone*. Without auth, anyone who finds your URL can enumerate every order. Local stdio servers inherit the trust of the machine they run on. Remote servers inherit nothing. You must add it.
MCP's remote transport supports OAuth 2.1 for exactly this. The flow, in plain terms:
1. A user adds your connector in the Claude app.
2. Before the connector works, the app sends the user to your authorization server to log in and consent.
3. Your server issues an access tokentokenA token is the basic unit of text that language models process, often a word fragment, whole word, or punctuation mark rather than a single character.Voir la définition complète → tied to *that specific user*.
4. Every tool call from Claude now arrives carrying that tokentokenA token is the basic unit of text that language models process, often a word fragment, whole word, or punctuation mark rather than a single character.Voir la définition complète →.
Inside your tool, you read the tokentokenA token is the basic unit of text that language models process, often a word fragment, whole word, or punctuation mark rather than a single character.Voir la définition complète →, resolve it to a user, and scope the query. The same get_order_status call returns different rows depending on who is asking:
@mcp.tool()
def get_order_status(order_id: str, ctx: Context) -> dict:
user = resolve_user(ctx.request_context) # from the bearer token
order = lookup_order(order_id, owner=user.id)
if order is None:
return {"found": False, "order_id": order_id}
return {"found": True, **order}The principle: never trust the `order_id` alone. Trust the authenticated identity, then check that this identity is allowed to see that order. The model is not your security boundary. Your server is. Claude will happily pass along whatever the user types, including an order ID that belongs to someone else, so the ownership check lives in your code and nowhere else.
Two practical notes for 2025-2026:
Your MCP server is a building block, not the whole app. The same server you just wrote plugs into multiple Anthropic surfaces with no changes:
get_order_status while it works in your repo.Compare this to Skills, which package instructions, scripts, and files that shape how Claude *behaves* on a task. A Skill teaches Claude a procedure. An MCP server gives Claude a *capability* it did not have: live access to your order system. You will often pair them. A "customer support" Skill that knows your tone and escalation rules, calling an order-tools MCP server for the live data. Knowing which problem each one solves is half of building well on Claude.
Build the tool once. Decide its transport by audience. Guard it by identity. That is the whole discipline.
FastMCP, then change one line (transport="streamable-http") to make it a remote connector. The tool logic never changes.mcp dev server.py and the MCP Inspector to verify the tool works before wiring it into Claude Desktop or the apps.