Building Servers & Clients
A worked Python MCP server with tools and a resource, tested with the MCP Inspector, plus what building a client actually involves
Building Servers & Clients
TL;DR
Building an MCP server means registering tools and resources with the SDK and letting it handle the protocol plumbing — the official Python SDK's FastMCP interface generates JSON Schema from your function signatures and docstrings automatically. Building a client is rarely necessary: existing hosts (Claude Desktop, Claude Code, VS Code) already implement that side, so most developers only ever write servers.
| Property | Value |
|---|---|
| Level | Intermediate |
| Reading time | ~20 minutes |
| Prerequisites | Authorization & Security |
| You will understand | How to build a real server with tools and resources, test it, and what building a client would involve |
A Worked Server, Tools and a Resource
The official Python SDK's FastMCP class is the fastest path from "a Python function" to "a working MCP server." Here's a weather server exposing two tools and one resource:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather")
@mcp.tool()
async def get_alerts(state: str) -> str:
"""Get active weather alerts for a US state.
Args:
state: Two-letter US state code (e.g. CA, NY)
"""
alerts = await fetch_alerts(state)
if not alerts:
return f"No active alerts for {state}."
return "\n".join(format_alert(a) for a in alerts)
@mcp.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
"""Get the weather forecast for a specific location."""
forecast = await fetch_forecast(latitude, longitude)
return format_forecast(forecast)
@mcp.resource("weather://stations/{state}")
async def list_stations(state: str) -> str:
"""List weather stations for a US state, as a resource clients can read."""
stations = await fetch_stations(state)
return "\n".join(s.name for s in stations)
if __name__ == "__main__":
mcp.run(transport="stdio")What @mcp.tool() actually does
The decorator inspects the function's type-annotated parameters and builds the inputSchema a client sees via tools/list — state: str becomes a required "type": "string" property, latitude: float becomes "type": "number", and so on. The docstring becomes the tool's description, which is the entire basis the model has for deciding when to call it.
A vague docstring produces a vague tool. """Get weather.""" gives the model almost nothing to decide with. The get_alerts docstring above is deliberately specific about what the parameter is and what format it expects — the same lesson from Tools & Function Calling applies here at the SDK level, not just at the prompt level.
What @mcp.resource() does
resource() registers a URI template — here weather://stations/{state} — as a resource clients can read via resources/read (or discover via resources/templates/list, covered in Resources & Prompts). The function's return value becomes the resource's content. Unlike a tool, nothing calls this because the model decided to — the application fetches it to provide context, per the resources primitive's application-controlled nature.
Running It: stdio vs Remote
mcp.run(transport="stdio") starts the server reading JSON-RPC from stdin and writing responses to stdout — this is exactly what a host like Claude Desktop does when it spawns your server as a subprocess, per Transports.
For a server other clients reach over the network, the same SDK supports running over Streamable HTTP instead — the tool and resource registration code above doesn't change at all; only the transport argument and how you deploy the process differ. This is the shape the MCP project brief asks you to build toward.
Testing Before You Connect It to Anything
Don't skip this step
Wiring a half-tested server straight into Claude Desktop or Claude Code means debugging protocol issues and application behavior at the same time. Test the server on its own first.
The MCP Inspector (github.com/modelcontextprotocol/inspector) is the official tool for this. Point it at your server — stdio or HTTP — and it gives you:
- A live list of every tool, resource, and prompt the server currently reports
- The ability to call any tool manually, with arguments you type in, and see the exact result
- A raw view of the JSON-RPC traffic, useful when a schema or response shape isn't what you expected
npx @modelcontextprotocol/inspector python weather_server.pyWork through every tool and resource in the Inspector before pointing a real host at the server. A tool that works when the model calls it with well-formed arguments and a tool that works when a human tries slightly malformed ones are different tests, and the Inspector makes the second one easy to run.
Building a Client (You Probably Don't Need To)
Most developers never build an MCP client. A client is the host-side piece that connects to servers, and every mainstream AI application you'd want to use already has one — Claude Desktop, Claude Code, VS Code, Cursor. Building a client only makes sense if you're building a new AI application or host from scratch.
If you are in that position, a minimal client needs to:
- Connect over a transport (stdio: spawn the server process; Streamable HTTP: open an HTTP connection)
- Optionally call
server/discoverto learn the server's supported versions and capabilities up front - Call
tools/list(andresources/list,prompts/listas needed) to learn what's available - Call
tools/callwith a tool name and arguments when the model decides to use one, and feed the result back into the conversation
Every request needs the _meta fields covered in The MCP Architecture — protocol version, client capabilities, and client identity — since the protocol is stateless and carries that information per-request rather than negotiating it once.
Concept Checks
Check yourself
Why does a vague docstring on an `@mcp.tool()`-decorated function actually hurt the server, not just look unprofessional?
Because the docstring becomes the tool's description field, which is the entire basis a model has for deciding when and whether to call that tool. A vague description ("Get weather") gives the model nothing to distinguish this tool from a similar one or to know what input it expects, leading to wrong or missing tool calls — the same failure mode as a poorly-written tool description in any agent system.
What's the practical difference between how `@mcp.tool()` and `@mcp.resource()` get invoked?
A tool is called because the model, during a conversation, decides it needs that action and requests it — model-controlled. A resource is fetched by the application itself, deciding what context to provide, independent of any model decision in that moment — application-controlled. The code to register each looks similar, but who triggers the call is fundamentally different.
Why test a new server with the MCP Inspector before connecting it to Claude Desktop or Claude Code?
Because connecting an untested server directly to a real host means any problem could be either a protocol/server bug or an application-level behavior issue, and there's no easy way to tell which while debugging through the host's UI. The Inspector isolates the server, letting you call every tool and resource manually and inspect raw JSON-RPC traffic before a real model or host is involved at all.
Key Concepts Recap
| Concept | One-line summary |
|---|---|
FastMCP | The Python SDK's high-level interface for registering tools and resources |
@mcp.tool() | Generates inputSchema from type hints, description from the docstring |
@mcp.resource() | Registers a URI (or URI template) as application-fetchable content |
mcp.run(transport=...) | Starts the server over stdio or Streamable HTTP — registration code doesn't change |
| MCP Inspector | The official tool for testing a server manually before connecting a real host |
| Most developers only build servers | Existing hosts already implement the client side |
| A minimal client's four jobs | Connect, optionally discover, list primitives, call tools |
Next
With a working server, the next question is how to actually get it in front of Claude: Connecting Claude to MCP.
Authorization & Security
The full OAuth 2.1 flow MCP builds on — discovery, PKCE, the resource parameter, issuer validation, and step-up authorization
Connecting Claude to MCP
The full claude mcp add reference — transports, scopes, authentication, config files — plus Claude Desktop and the Claude API's MCP connector