Skip to content

Commit 3da0ebc

Browse files
committed
docs: make all code examples self-contained and runnable
Every Python code block in docs pages is now both linted and executed by pytest-examples. - Make all concepts.md examples self-contained with proper imports - Add test_docs_examples_run that executes every doc code block - Use __name__ override to prevent `if __name__ == "__main__"` blocks from starting servers during tests - Support skip-run/skip-lint prefix tags for future use - Drop snippet-source from quickstart.md so the example is self-contained
1 parent 9206abd commit 3da0ebc

File tree

3 files changed

+85
-32
lines changed

3 files changed

+85
-32
lines changed

docs/concepts.md

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,41 +21,51 @@ Host (e.g. Claude Desktop)
2121

2222
## Primitives
2323

24-
MCP servers expose three core primitives:
24+
MCP servers expose three core primitives: **resources**, **tools**, and **prompts**.
2525

2626
### Resources
2727

2828
Resources provide data to LLMs — similar to GET endpoints in a REST API. They load information into the
2929
LLM's context without performing computation or causing side effects.
3030

31+
Resources can be static (fixed URI) or use URI templates for dynamic content:
32+
3133
```python
34+
import json
35+
36+
from mcp.server.mcpserver import MCPServer
37+
38+
mcp = MCPServer("Demo")
39+
40+
3241
@mcp.resource("config://app")
3342
def get_config() -> str:
3443
"""Expose application configuration."""
3544
return json.dumps({"theme": "dark", "version": "2.0"})
36-
```
3745

38-
Resources can be static (fixed URI) or use URI templates for dynamic content:
3946

40-
```python
4147
@mcp.resource("users://{user_id}/profile")
4248
def get_profile(user_id: str) -> str:
4349
"""Get a user profile by ID."""
44-
return json.dumps(load_profile(user_id))
50+
return json.dumps({"user_id": user_id, "name": "Alice"})
4551
```
4652

4753
<!-- TODO: See [Resources](server/resources.md) for full documentation. -->
4854

4955
### Tools
5056

5157
Tools let LLMs take actions — similar to POST endpoints. They perform computation, call external APIs,
52-
or produce side effects.
58+
or produce side effects:
5359

5460
```python
61+
from mcp.server.mcpserver import MCPServer
62+
63+
mcp = MCPServer("Demo")
64+
65+
5566
@mcp.tool()
5667
def send_email(to: str, subject: str, body: str) -> str:
5768
"""Send an email to the given recipient."""
58-
# ... send email logic ...
5969
return f"Email sent to {to}"
6070
```
6171

@@ -67,6 +77,11 @@ Tools support structured output, progress reporting, and more.
6777
Prompts are reusable templates for LLM interactions. They help standardize common workflows:
6878

6979
```python
80+
from mcp.server.mcpserver import MCPServer
81+
82+
mcp = MCPServer("Demo")
83+
84+
7085
@mcp.prompt()
7186
def review_code(code: str, language: str = "python") -> str:
7287
"""Generate a code review prompt."""
@@ -93,7 +108,10 @@ When handling requests, your functions can access a **context object** that prov
93108
like logging, progress reporting, and access to the current session:
94109

95110
```python
96-
from mcp.server.mcpserver import Context
111+
from mcp.server.mcpserver import Context, MCPServer
112+
113+
mcp = MCPServer("Demo")
114+
97115

98116
@mcp.tool()
99117
async def long_task(ctx: Context) -> str:
@@ -113,15 +131,28 @@ Servers support a **lifespan** pattern for managing startup and shutdown logic
113131
initializing a database connection pool on startup and closing it on shutdown:
114132

115133
```python
134+
from collections.abc import AsyncIterator
116135
from contextlib import asynccontextmanager
136+
from dataclasses import dataclass
137+
138+
from mcp.server.mcpserver import MCPServer
139+
140+
141+
@dataclass
142+
class AppContext:
143+
db_url: str
144+
117145

118146
@asynccontextmanager
119-
async def app_lifespan(server):
120-
db = await Database.connect()
147+
async def app_lifespan(server: MCPServer) -> AsyncIterator[AppContext]:
148+
# Initialize on startup
149+
ctx = AppContext(db_url="postgresql://localhost/mydb")
121150
try:
122-
yield {"db": db}
151+
yield ctx
123152
finally:
124-
await db.disconnect()
153+
# Cleanup on shutdown
154+
pass
155+
125156

126157
mcp = MCPServer("My App", lifespan=app_lifespan)
127158
```

docs/quickstart.md

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,7 @@ You'll need Python 3.10+ and [uv](https://docs.astral.sh/uv/) (recommended) or p
1010

1111
Create a file called `server.py` with a tool, a resource, and a prompt:
1212

13-
<!-- snippet-source examples/snippets/servers/mcpserver_quickstart.py -->
1413
```python
15-
"""MCPServer quickstart example.
16-
17-
Run from the repository root:
18-
uv run examples/snippets/servers/mcpserver_quickstart.py
19-
"""
20-
2114
from mcp.server.mcpserver import MCPServer
2215

2316
# Create an MCP server
@@ -27,21 +20,21 @@ mcp = MCPServer("Demo")
2720
# Add an addition tool
2821
@mcp.tool()
2922
def add(a: int, b: int) -> int:
30-
"""Add two numbers"""
23+
"""Add two numbers."""
3124
return a + b
3225

3326

3427
# Add a dynamic greeting resource
3528
@mcp.resource("greeting://{name}")
3629
def get_greeting(name: str) -> str:
37-
"""Get a personalized greeting"""
30+
"""Get a personalized greeting."""
3831
return f"Hello, {name}!"
3932

4033

4134
# Add a prompt
4235
@mcp.prompt()
4336
def greet_user(name: str, style: str = "friendly") -> str:
44-
"""Generate a greeting prompt"""
37+
"""Generate a greeting prompt."""
4538
styles = {
4639
"friendly": "Please write a warm, friendly greeting",
4740
"formal": "Please write a formal, professional greeting",
@@ -51,14 +44,10 @@ def greet_user(name: str, style: str = "friendly") -> str:
5144
return f"{styles.get(style, styles['friendly'])} for someone named {name}."
5245

5346

54-
# Run with streamable HTTP transport
5547
if __name__ == "__main__":
56-
mcp.run(transport="streamable-http", json_response=True)
48+
mcp.run(transport="streamable-http")
5749
```
5850

59-
_Full example: [examples/snippets/servers/mcpserver_quickstart.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/mcpserver_quickstart.py)_
60-
<!-- /snippet-source -->
61-
6251
## Run the server
6352

6453
```bash

tests/test_examples.py

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import sys
99
from pathlib import Path
10+
from typing import Any
1011

1112
import pytest
1213
from inline_snapshot import snapshot
@@ -96,20 +97,52 @@ async def test_desktop(monkeypatch: pytest.MonkeyPatch):
9697
assert "/fake/path/file2.txt" in content.text
9798

9899

100+
SKIP_RUN_TAGS = ["skip", "skip-run"]
101+
SKIP_LINT_TAGS = ["skip", "skip-lint"]
102+
103+
# Files with code examples that are both linted and run
104+
DOCS_FILES = ["docs/quickstart.md", "docs/concepts.md"]
105+
106+
107+
def _set_eval_config(eval_example: EvalExample) -> None:
108+
eval_example.set_config(
109+
ruff_ignore=["F841", "I001", "F821"],
110+
target_version="py310",
111+
line_length=120,
112+
)
113+
114+
99115
# TODO(v2): Change back to README.md when v2 is released
100116
@pytest.mark.parametrize(
101117
"example",
102-
find_examples("README.v2.md", "docs/quickstart.md", "docs/concepts.md"),
118+
find_examples("README.v2.md", *DOCS_FILES),
103119
ids=str,
104120
)
105121
def test_docs_examples(example: CodeExample, eval_example: EvalExample):
106-
ruff_ignore: list[str] = ["F841", "I001", "F821"] # F821: undefined names (snippets lack imports)
122+
if any(example.prefix_settings().get(key) == "true" for key in SKIP_LINT_TAGS):
123+
pytest.skip("skip-lint")
107124

108-
# Use project's actual line length of 120
109-
eval_example.set_config(ruff_ignore=ruff_ignore, target_version="py310", line_length=120)
125+
_set_eval_config(eval_example)
110126

111-
# Use Ruff for both formatting and linting (skip Black)
112127
if eval_example.update_examples: # pragma: no cover
113128
eval_example.format_ruff(example)
114129
else:
115130
eval_example.lint_ruff(example)
131+
132+
133+
def _get_runnable_docs_examples() -> list[CodeExample]:
134+
examples = find_examples(*DOCS_FILES)
135+
return [ex for ex in examples if not any(ex.prefix_settings().get(key) == "true" for key in SKIP_RUN_TAGS)]
136+
137+
138+
@pytest.mark.parametrize("example", _get_runnable_docs_examples(), ids=str)
139+
def test_docs_examples_run(example: CodeExample, eval_example: EvalExample):
140+
_set_eval_config(eval_example)
141+
142+
# Prevent `if __name__ == "__main__"` blocks from starting servers
143+
globals: dict[str, Any] = {"__name__": "__docs_test__"}
144+
145+
if eval_example.update_examples: # pragma: no cover
146+
eval_example.run_print_update(example, module_globals=globals)
147+
else:
148+
eval_example.run_print_check(example, module_globals=globals)

0 commit comments

Comments
 (0)