AI-assisted draft. I wrote and verified the code against the current MCP docs and Python SDK.
MCP (Model Context Protocol) is an open standard for connecting AI apps to external systems. Think of it like USB-C for AI: one shape of plug for tools, data, and workflows, instead of a custom integration for every host.
If you have used Claude Desktop, Cursor, VS Code, or ChatGPT with connectors, you have already seen the client side. This post focuses on the server side: a tiny remote tool you can run locally, then call from a real MCP client.
Three building blocks
An MCP server can expose:
- Tools - functions the model can call (search, create a ticket, generate an image).
- Resources - read-only data the app can pull in as context (files, schemas, docs).
- Prompts - reusable templates the user picks on purpose.
Most first servers start with tools. That is what we will build.
Local vs remote transport
How the bytes move is the transport:
| Transport | What it is | When to use it |
|---|---|---|
stdio |
Host launches your process and talks over stdin/stdout | Local servers on one machine |
streamable-http |
A normal HTTP endpoint (default path /mcp) |
Anything you deploy or call over the network |
sse (old HTTP+SSE) |
Deprecated remote transport | Only for older clients. Do not build new servers on it |
Streamable HTTP replaced the older HTTP+SSE transport in the 2025-03-26 protocol revision. New remote servers should expose one MCP endpoint (for example https://your-host/mcp) that accepts client POSTs.
What you need
- Python 3.11+
-
uv(or pip) - About 15 minutes
1. Scaffold a tiny server
uv init greeter
cd greeter
uv venv
source .venv/bin/activate
uv add "mcp[cli]"
Create server.py:
from mcp.server import MCPServer
mcp = MCPServer("greeter")
@mcp.tool()
def greet(name: str = "World") -> str:
"""Return a short greeting for the given name."""
return f"Hello, {name}!"
if __name__ == "__main__":
# Streamable HTTP: clients hit http://127.0.0.1:3001/mcp
mcp.run(
transport="streamable-http",
host="127.0.0.1",
port=3001,
# Stateless + JSON is a simple default for a first remote server
stateless_http=True,
json_response=True,
)
Notes that trip people up:
- Transport options (
host,port,stateless_http, …) go onrun(), not onMCPServer(...). - For
stdioservers, never write to stdout (printis unsafe). Useloggingto stderr. For HTTP servers, normal stdout logging is fine. - The default MCP path is
/mcp.
Start it:
uv run server.py
You should now have http://127.0.0.1:3001/mcp.
2. Call the tool from a Python client
In another terminal (same venv, or another project with mcp installed):
import asyncio
from mcp import Client
async def main() -> None:
async with Client("http://127.0.0.1:3001/mcp") as client:
listed = await client.list_tools()
print("tools:", [t.name for t in listed.tools])
result = await client.call_tool("greet", {"name": "Dev.to"})
# Simple string tools often expose structured_content;
# otherwise inspect result.content for text blocks.
print(result.structured_content or result.content)
asyncio.run(main())
A URL string is treated as Streamable HTTP by the Python client. That is the remote path. For local stdio servers you would pass launch parameters instead of a URL.
Expected shape of the happy path: list_tools shows greet, and call_tool returns something like Hello, Dev.to!.
3. Point a real host at it
Hosts differ in UI, but the idea is the same: give them the MCP URL.
Claude (Custom Connector): Settings → Connectors → Add custom connector → paste http://127.0.0.1:3001/mcp (for a public server, use your HTTPS URL). Complete any auth the server asks for.
Cursor / VS Code-style config often looks like:
{
"mcpServers": {
"greeter": {
"url": "http://127.0.0.1:3001/mcp"
}
}
}
Exact keys vary by client version. Prefer the client's current docs over screenshots from early 2025 tutorials, which still show /sse endpoints.
For a local stdio server instead, the config uses command + args so the host can spawn your process. Remotes use a URL.
4. Smoke-test with the Inspector
The MCP Inspector is the fastest way to see tools without wiring a full chat UI:
uv run mcp dev server.py
That path is aimed at stdio servers. For an already-running Streamable HTTP server, open the Inspector UI and connect to http://127.0.0.1:3001/mcp as an HTTP / Streamable HTTP target. List tools, call greet, confirm the text result.
Common mistakes
-
Building on SSE-only
/sseURLs. That is the deprecated remote transport. Prefer/mcpStreamable HTTP. -
Putting
port=onMCPServer(...). It belongs onrun(...). - Printing to stdout on a stdio server. Corrupts JSON-RPC.
- Copying 2024 client snippets. Many still assume initialize/session headers that newer protocol revisions changed. Check the current MCP docs and the Python SDK run guide.
What to build next
Swap greet for a real tool: wrap an internal API, return a resource URI for docs, or add a prompt template for a workflow your team repeats. Keep tools small and typed. Prefer one clear job per tool over a mega-tool with a dozen modes.
If you ship the server beyond localhost, put it behind HTTPS, require auth (API key or OAuth), and turn on the SDK's transport security options before you expose it.
Sources
- MCP intro
- Build an MCP server
- Connect to remote MCP servers
- Python SDK: Running your server
- Python SDK: Client transports
Questions or corrections welcome in the comments. Protocol details move; if something here diverges from the live docs, trust the docs.
Top comments (0)