Build
MCP server Python example
Here is a minimal MCP server in Python that exposes one Tool (a clock function returning the current time) and runs over stdio. The full source is under 30 lines, including imports and the entry point.
The full source
Save this as clock_server.py. The Python SDK ships as the
mcp package on PyPI; install it with pip install mcp.
from datetime import datetime, timezone
from mcp.server import Server
from mcp.server.stdio import stdio_server
server = Server("clock-server")
@server.tool()
async def current_time(timezone_name: str = "UTC") -> str:
"""Return the current time in ISO 8601 format.
Args:
timezone_name: IANA timezone name. Defaults to UTC.
"""
now = datetime.now(timezone.utc)
return now.isoformat()
if __name__ == "__main__":
import asyncio
asyncio.run(stdio_server(server)) The example, broken down
- Imports and server initialization.
Server("clock-server")instantiates the protocol handler with a server name. The name surfaces to the agent client during theinitializeresponse. - Tool declaration via decorator.
@server.tool()registers the Python function as an MCP Tool. The SDK reads the function signature and docstring at registration time. - JSON Schema input from type hints. The
timezone_name: strparameter becomes a string property in the JSON Schema the SDK generates fortools/list. Default values become optional schema fields. For complex inputs, pass an explicitinput_schema=argument to the decorator. - Tool body returns structured output. The return value
(an ISO 8601 string here) goes back to the client as the
tools/callresult. Return any JSON-serializable value: string, dict, list, number. - Transport selection.
stdio_server(server)runs the JSON-RPC 2.0 loop over standard input and standard output. The agent client spawns the Python process and pipes both streams. - Entry point.
asyncio.run()drives the async event loop. The server reads requests, dispatches to registered handlers, writes responses, and exits when the client closes stdin.
What happens when the client connects
- The client spawns the Python script over stdio. Claude Desktop,
Cursor, or any other agent client launches
python clock_server.pyas a child process. Stdin and stdout become the JSON-RPC transport. - Initialize handshake. The client sends an
initializerequest with its protocol version and client capabilities. The server responds with its protocol version and advertises the Tools capability. - The client calls
tools/list. The server returns the schema forcurrent_time: name, description (the docstring), and the input JSON Schema generated from the type hints. - The client calls
tools/call. When the user's query triggers the tool, the client sends atools/callrequest with the tool name and arguments. The server runscurrent_time()and returns the ISO 8601 string.
Extending the example
Add a Resource: expose a config file as a URI the client can read.
@server.resource("config://app/settings")
async def read_settings() -> str:
"""Return the app's settings as JSON."""
with open("settings.json") as f:
return f.read()
The client lists this Resource via resources/list and reads it via
resources/read with the matching URI.
Add a Prompt: a parameterized template the client can offer to the user.
@server.prompt()
async def summarize(text: str, max_words: int = 100) -> str:
"""Summarize the given text in at most max_words words."""
return f"Summarize the following in {max_words} words or fewer:\n\n{text}"
The client discovers Prompts via prompts/list and materializes one
via prompts/get with the arguments filled in.
Trust posture for this example
Even this 30-line server runs with the user's full shell privileges. The clock function is harmless. A function that reads files, calls network APIs, or executes shell commands has the same shape but with much broader surface area.
Three checks every server author should run before publishing. First, audit
every dependency the server imports (including transitive); each one inherits
the same process privileges. Second, treat any input parameter that lands in
open(), subprocess.run(), or requests.get()
as untrusted; validate the path, command, or URL before passing it through.
Third, document what environment variables and filesystem paths the server
touches. Users installing the server need that contract to evaluate the risk.