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 the initialize response.
  • 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: str parameter becomes a string property in the JSON Schema the SDK generates for tools/list. Default values become optional schema fields. For complex inputs, pass an explicit input_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/call result. 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

  1. The client spawns the Python script over stdio. Claude Desktop, Cursor, or any other agent client launches python clock_server.py as a child process. Stdin and stdout become the JSON-RPC transport.
  2. Initialize handshake. The client sends an initialize request with its protocol version and client capabilities. The server responds with its protocol version and advertises the Tools capability.
  3. The client calls tools/list. The server returns the schema for current_time: name, description (the docstring), and the input JSON Schema generated from the type hints.
  4. The client calls tools/call. When the user's query triggers the tool, the client sends a tools/call request with the tool name and arguments. The server runs current_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.

Related on MCPowered