Python Server

This chapter exposes a tool, a resource, and a prompt with the official SDK. The First steps guide is authoritative: the class is MCPServer (v1 used FastMCP) and the import is from mcp.server import MCPServer.


Full example

from pathlib import Path

from mcp.server import MCPServer

mcp = MCPServer(
    "workshop",
    instructions="Local demo: add numbers, read a greeting, summarize text.",
    version="0.1.0",
)

NOTES = Path(__file__).with_name("notes")


@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two integers and return the sum."""
    return a + b


@mcp.resource("greeting://{name}")
def greeting(name: str) -> str:
    """Return a short greeting for the given name."""
    return f"Hello, {name}!"


@mcp.resource("notes://inbox")
def notes_inbox() -> str:
    """Read the local notes/inbox.md file if it exists."""
    path = NOTES / "inbox.md"
    if not path.is_file():
        return "(missing notes/inbox.md — create it next to server.py)"
    return path.read_text(encoding="utf-8")


@mcp.prompt()
def summarize(text: str) -> str:
    """Ask the model to summarize the given text in one sentence."""
    return f"Summarize the following text in one sentence:\n\n{text}"


if __name__ == "__main__":
    mcp.run()

Create notes/inbox.md next to the file and write a few lines. Inspector can then read the concrete URI notes://inbox. greeting://{name} is a template (Resource Templates tab); fill name=Ada and read it.


What each decorator does

DecoratorWho triggers itWhat you return
@mcp.tool()The model, via the host’s tools/callAn action result (number, text, structured data)
@mcp.resource("uri")The host, when it loads contextRead-only text or bytes
@mcp.prompt()The user, from a menu or slash commandOne user message (or a list of messages)

A {param} in the URI makes a template; the name must match the function parameter. A URI without braces is concrete and appears in resources/list.

The docstring is the description; type hints are the schema. For richer field text, use Annotated and pydantic.Field (same pattern as the official Tools / Prompts docs).


How to run it

Stdio (Cursor / Claude Code / mcp dev):

python .\server.py
python ./server.py

Or let the CLI find the module-level mcp object:

mcp run .\server.py

HTTP:

if __name__ == "__main__":
    mcp.run(transport="streamable-http", host="127.0.0.1", port=8000)
mcp run .\server.py --transport streamable-http

Transport options belong on run(). Passing them to the constructor yields: TypeError: MCPServer.__init__() got an unexpected keyword argument 'port'.


Verify in Inspector

mcp dev .\server.py

Walk the tabs:

  1. Toolsadd(2, 5)7
  2. Resources → read notes://inbox
  3. Resource Templatesgreeting / AdaHello, Ada!
  4. Promptssummarize with a paragraph → a rendered role: user message

Hosts often surface resources and prompts less completely than Inspector. Prove the server here first, then debug the client UI.


Logging and secrets

  • Use logging, not print to stdout
  • Read tokens from the environment; never put them in a resource body
  • Keep file reads inside a directory you own (this example uses only notes/)
import logging
import os

log = logging.getLogger("workshop")

@mcp.tool()
def masked_env_name() -> str:
    """Return whether NOTES_TOKEN is set, never the raw value."""
    present = bool(os.environ.get("NOTES_TOKEN"))
    log.info("NOTES_TOKEN set=%s", present)
    return "configured" if present else "missing"

v1 FastMCP vs this course

v1v2 (this course)
from mcp.server.fastmcp import FastMCPfrom mcp.server import MCPServer
FastMCP("demo")MCPServer("demo")
@mcp.tool() / resource / promptThe same
mcp.run()The same (HTTP options now live on run())

Rename the class and import on old files; decorator code usually stays as-is.


Next

评论