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 MCPMCPAn open standard that lets AI assistants connect to your company's tools and data in a consistent, governed way, instead of one custom integration at a time.View full definition → (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.View full definition → 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.View full definition →. Now you build the other side of that handshake: the server.
What an MCP server actually is
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:
- stdio: the server runs as a local subprocess and talks over standard input/output. This is how Claude Desktop launches a local connector. Zero network, zero auth, fastest to build.
- Streamable HTTP: the server runs as a web service at a URL. This is how a *remote* connector works, and it is what you submit to the connector marketplace or share with a team.
You write the tool logic once. The transport is a few lines at the bottom. Start with stdio.
The minimal server
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.- The
@mcp.tool()decorator registers the function as a callable tool. The SDK reads your type hints (order_id: str, returndict) 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.View full definition → automatically. No JSON Schema by hand. - The docstring is not decoration. Claude reads it to decide when and how to call the tool. The first line describes the tool; the
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.View full definition → query, an internal REST endpoint, a Stripe lookup. The MCP layer never changes. You are wrapping an existing capability, not rebuilding it.
Running it locally in Claude desktop
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.
Build an MCP Server in Python
Going remote: from subprocess to connector
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?
Knowledge check
1. In the MCP architecture described, what is the division of responsibility between the client and your server?
2. For answering a question like 'where is my order,' which MCP capability type is the right choice, and why?
3. Why does the lesson recommend starting with the stdio transport before moving to Streamable HTTP?
4. Select ALL statements that correctly describe the stdio and Streamable HTTP transports.
Select all the correct answers.
5. Select ALL correct statements about MCP servers and the FastMCP helper.
Select all the correct answers.
A word on authentication
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:
- A user adds your connector in the Claude app.
- Before the connector works, the app sends the user to your authorization server to log in and consent.
- 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.View full definition → tied to *that specific user*.
- Every tool call from Claude now arrives carrying that token.
Inside your tool, you read the token, 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:
- For internal-only tools, a simpler bearer token or APIAPIApplication Programming Interface: a standardised interface that lets applications communicate and exchange data without knowing each other's internal workings.View full definition → key passed as a header is acceptable, as long as the transport is HTTPS and the token maps to a real principal you can scope against. Full OAuth is for connectors real users add themselves.
- If you list a connector in the marketplace or share it across an organization, follow Anthropic's connector and security requirements. Org admins control which connectors are enabled, and that governance layer assumes your server authenticates properly underneath. See the Anthropic connector documentation for the current bar.
Where this fits in the ecosystem
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:
- Claude Code can load it so the coding agent calls
get_order_statuswhile it works in your repo. - The Claude Agent SDK lets you build a managed agent that uses your connector as one of several tools, alongside file access and the GitHub integration.
- The connector marketplace lets other people discover and add it.
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.
Key Takeaways
- Start with stdio, ship with HTTP. Write and debug your tool as a local stdio server with
FastMCP, then change one line (transport="streamable-http") to make it a remote connector. The tool logic never changes. - The docstring and type hints are the interface. Claude decides when to call your tool from its name, description, and parameters. Write them as carefully as the code.
- Authenticate the moment you go remote. Use OAuth 2.1 for user-facing connectors, scope every query to the authenticated identity, and never let the model's input be your security boundary.
- Test in isolation first. Use
mcp dev server.pyand the MCP Inspector to verify the tool works before wiring it into Claude Desktop or the apps. - Pick the right primitive. MCP servers add *capabilities* (live data, actions); Skills shape *behavior* (procedures, tone). Real connectors usually combine both.
What to do, from this lesson
These actions are compiled in the role's Playbook.
- Write tool docstrings and type hints for the model's routing
- Build MCP servers as local stdio, then flip transport to remote
- Scope every remote MCP query to the authenticated identity
Related articles
Recent articles from the blog that build on this lesson.
- AIThe quiet handoff that changed what AI agents can actually doIn December 2025, Anthropic gave away one of its most consequential pieces of infrastructure. The Model Context Protocol is now an open standard, and the ripple effects on what AI agents can actually do in the real world are only beginning to show up at work.
- AIThe Model Context Protocol: how AI actually connects to the world outside its context windowMost AI assistants are islands. The Model Context Protocol is the specification that turns them into networked systems, and understanding how it works changes what you can realistically build or demand from AI in your organisation.