Rebuild the docs around tested examples; shrink README.v2.md to a pitch (#2978)
This commit is contained in:
parent
4caa41f6d5
commit
411a6d3980
119 changed files with 9510 additions and 2639 deletions
7
docs_src/__init__.py
Normal file
7
docs_src/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
"""Complete, runnable source for every code example in `docs/`.
|
||||
|
||||
Each `docs/<page>.md` includes its examples from `docs_src/<chapter>/tutorialNNN.py`
|
||||
via `--8<--`, and `tests/docs_src/test_<chapter>.py` imports the same module and
|
||||
exercises it through the in-memory `mcp.Client`. The file you read in the docs is
|
||||
the file CI runs.
|
||||
"""
|
||||
0
docs_src/asgi/__init__.py
Normal file
0
docs_src/asgi/__init__.py
Normal file
12
docs_src/asgi/tutorial001.py
Normal file
12
docs_src/asgi/tutorial001.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
from mcp.server import MCPServer
|
||||
|
||||
mcp = MCPServer("Notes")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def add_note(text: str) -> str:
|
||||
"""Save a note."""
|
||||
return f"Saved: {text}"
|
||||
|
||||
|
||||
app = mcp.streamable_http_app()
|
||||
27
docs_src/asgi/tutorial002.py
Normal file
27
docs_src/asgi/tutorial002.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from starlette.applications import Starlette
|
||||
from starlette.routing import Mount
|
||||
|
||||
from mcp.server import MCPServer
|
||||
|
||||
mcp = MCPServer("Notes")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def add_note(text: str) -> str:
|
||||
"""Save a note."""
|
||||
return f"Saved: {text}"
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: Starlette) -> AsyncIterator[None]:
|
||||
async with mcp.session_manager.run():
|
||||
yield
|
||||
|
||||
|
||||
app = Starlette(
|
||||
routes=[Mount("/", app=mcp.streamable_http_app())],
|
||||
lifespan=lifespan,
|
||||
)
|
||||
39
docs_src/asgi/tutorial003.py
Normal file
39
docs_src/asgi/tutorial003.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
from collections.abc import AsyncIterator
|
||||
from contextlib import AsyncExitStack, asynccontextmanager
|
||||
|
||||
from starlette.applications import Starlette
|
||||
from starlette.routing import Mount
|
||||
|
||||
from mcp.server import MCPServer
|
||||
|
||||
notes = MCPServer("Notes")
|
||||
tasks = MCPServer("Tasks")
|
||||
|
||||
|
||||
@notes.tool()
|
||||
def add_note(text: str) -> str:
|
||||
"""Save a note."""
|
||||
return f"Saved: {text}"
|
||||
|
||||
|
||||
@tasks.tool()
|
||||
def add_task(title: str) -> str:
|
||||
"""Create a task."""
|
||||
return f"Created: {title}"
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: Starlette) -> AsyncIterator[None]:
|
||||
async with AsyncExitStack() as stack:
|
||||
await stack.enter_async_context(notes.session_manager.run())
|
||||
await stack.enter_async_context(tasks.session_manager.run())
|
||||
yield
|
||||
|
||||
|
||||
app = Starlette(
|
||||
routes=[
|
||||
Mount("/notes", app=notes.streamable_http_app()),
|
||||
Mount("/tasks", app=tasks.streamable_http_app()),
|
||||
],
|
||||
lifespan=lifespan,
|
||||
)
|
||||
27
docs_src/asgi/tutorial004.py
Normal file
27
docs_src/asgi/tutorial004.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from starlette.applications import Starlette
|
||||
from starlette.routing import Mount
|
||||
|
||||
from mcp.server import MCPServer
|
||||
|
||||
mcp = MCPServer("Notes")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def add_note(text: str) -> str:
|
||||
"""Save a note."""
|
||||
return f"Saved: {text}"
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: Starlette) -> AsyncIterator[None]:
|
||||
async with mcp.session_manager.run():
|
||||
yield
|
||||
|
||||
|
||||
app = Starlette(
|
||||
routes=[Mount("/notes", app=mcp.streamable_http_app(streamable_http_path="/"))],
|
||||
lifespan=lifespan,
|
||||
)
|
||||
52
docs_src/asgi/tutorial005.py
Normal file
52
docs_src/asgi/tutorial005.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from starlette.applications import Starlette
|
||||
from starlette.middleware import Middleware
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
from starlette.routing import Mount
|
||||
|
||||
from mcp.server import MCPServer
|
||||
from mcp.server.transport_security import TransportSecuritySettings
|
||||
|
||||
mcp = MCPServer("Notes")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def add_note(text: str) -> str:
|
||||
"""Save a note."""
|
||||
return f"Saved: {text}"
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: Starlette) -> AsyncIterator[None]:
|
||||
async with mcp.session_manager.run():
|
||||
yield
|
||||
|
||||
|
||||
security = TransportSecuritySettings(
|
||||
allowed_hosts=["mcp.example.com", "mcp.example.com:*"],
|
||||
allowed_origins=["https://app.example.com"],
|
||||
)
|
||||
|
||||
app = Starlette(
|
||||
routes=[Mount("/", app=mcp.streamable_http_app(transport_security=security))],
|
||||
middleware=[
|
||||
Middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["https://app.example.com"],
|
||||
allow_methods=["GET", "POST", "DELETE"],
|
||||
allow_headers=[
|
||||
"Authorization",
|
||||
"Content-Type",
|
||||
"Last-Event-ID",
|
||||
"Mcp-Method",
|
||||
"Mcp-Name",
|
||||
"Mcp-Protocol-Version",
|
||||
"Mcp-Session-Id",
|
||||
],
|
||||
expose_headers=["Mcp-Session-Id"],
|
||||
)
|
||||
],
|
||||
lifespan=lifespan,
|
||||
)
|
||||
20
docs_src/asgi/tutorial006.py
Normal file
20
docs_src/asgi/tutorial006.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
from starlette.requests import Request
|
||||
from starlette.responses import JSONResponse, Response
|
||||
|
||||
from mcp.server import MCPServer
|
||||
|
||||
mcp = MCPServer("Notes")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def add_note(text: str) -> str:
|
||||
"""Save a note."""
|
||||
return f"Saved: {text}"
|
||||
|
||||
|
||||
@mcp.custom_route("/health", methods=["GET"])
|
||||
async def health(request: Request) -> Response:
|
||||
return JSONResponse({"status": "ok"})
|
||||
|
||||
|
||||
app = mcp.streamable_http_app()
|
||||
0
docs_src/authorization/__init__.py
Normal file
0
docs_src/authorization/__init__.py
Normal file
31
docs_src/authorization/tutorial001.py
Normal file
31
docs_src/authorization/tutorial001.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
from pydantic import AnyHttpUrl
|
||||
|
||||
from mcp.server import MCPServer
|
||||
from mcp.server.auth.provider import AccessToken, TokenVerifier
|
||||
from mcp.server.auth.settings import AuthSettings
|
||||
|
||||
KNOWN_TOKENS = {
|
||||
"alice-token": AccessToken(token="alice-token", client_id="alice", scopes=["notes:read"]),
|
||||
}
|
||||
|
||||
|
||||
class StaticTokenVerifier(TokenVerifier):
|
||||
async def verify_token(self, token: str) -> AccessToken | None:
|
||||
return KNOWN_TOKENS.get(token)
|
||||
|
||||
|
||||
mcp = MCPServer(
|
||||
"Notes",
|
||||
token_verifier=StaticTokenVerifier(),
|
||||
auth=AuthSettings(
|
||||
issuer_url=AnyHttpUrl("https://auth.example.com"),
|
||||
resource_server_url=AnyHttpUrl("http://127.0.0.1:8000/mcp"),
|
||||
required_scopes=["notes:read"],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def list_notes() -> list[str]:
|
||||
"""List every note in the notebook."""
|
||||
return ["Buy milk", "Ship the release"]
|
||||
35
docs_src/authorization/tutorial002.py
Normal file
35
docs_src/authorization/tutorial002.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
from pydantic import AnyHttpUrl
|
||||
|
||||
from mcp.server import MCPServer
|
||||
from mcp.server.auth.middleware.auth_context import get_access_token
|
||||
from mcp.server.auth.provider import AccessToken, TokenVerifier
|
||||
from mcp.server.auth.settings import AuthSettings
|
||||
|
||||
KNOWN_TOKENS = {
|
||||
"alice-token": AccessToken(token="alice-token", client_id="alice", scopes=["notes:read"]),
|
||||
}
|
||||
|
||||
|
||||
class StaticTokenVerifier(TokenVerifier):
|
||||
async def verify_token(self, token: str) -> AccessToken | None:
|
||||
return KNOWN_TOKENS.get(token)
|
||||
|
||||
|
||||
mcp = MCPServer(
|
||||
"Notes",
|
||||
token_verifier=StaticTokenVerifier(),
|
||||
auth=AuthSettings(
|
||||
issuer_url=AnyHttpUrl("https://auth.example.com"),
|
||||
resource_server_url=AnyHttpUrl("http://127.0.0.1:8000/mcp"),
|
||||
required_scopes=["notes:read"],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def whoami() -> str:
|
||||
"""Report which OAuth client is calling."""
|
||||
token = get_access_token()
|
||||
if token is None:
|
||||
return "anonymous"
|
||||
return f"{token.client_id} (scopes: {', '.join(token.scopes)})"
|
||||
0
docs_src/client/__init__.py
Normal file
0
docs_src/client/__init__.py
Normal file
18
docs_src/client/tutorial001.py
Normal file
18
docs_src/client/tutorial001.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
from mcp import Client
|
||||
from mcp.server import MCPServer
|
||||
|
||||
mcp = MCPServer("Bookshop", instructions="Search the catalog before recommending a book.")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def search_books(query: str) -> str:
|
||||
"""Search the catalog by title or author."""
|
||||
return f"Found 3 books matching {query!r}."
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(mcp) as client:
|
||||
print(client.server_info)
|
||||
print(client.server_capabilities)
|
||||
print(client.protocol_version)
|
||||
print(client.instructions)
|
||||
20
docs_src/client/tutorial002.py
Normal file
20
docs_src/client/tutorial002.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
from mcp import Client
|
||||
from mcp.server import MCPServer
|
||||
|
||||
mcp = MCPServer("Bookshop")
|
||||
|
||||
|
||||
@mcp.tool(title="Search the catalog")
|
||||
def search_books(query: str, limit: int = 10) -> str:
|
||||
"""Search the catalog by title or author."""
|
||||
return f"Found 3 books matching {query!r} (showing up to {limit})."
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(mcp) as client:
|
||||
result = await client.list_tools()
|
||||
for tool in result.tools:
|
||||
print(tool.name)
|
||||
print(tool.title)
|
||||
print(tool.description)
|
||||
print(tool.input_schema)
|
||||
33
docs_src/client/tutorial003.py
Normal file
33
docs_src/client/tutorial003.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
from mcp_types import TextContent
|
||||
from pydantic import BaseModel
|
||||
|
||||
from mcp import Client
|
||||
from mcp.server import MCPServer
|
||||
|
||||
mcp = MCPServer("Bookshop")
|
||||
|
||||
|
||||
class Book(BaseModel):
|
||||
title: str
|
||||
author: str
|
||||
year: int
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def lookup_book(title: str) -> Book:
|
||||
"""Look up a book by its exact title."""
|
||||
if title != "Dune":
|
||||
raise ValueError(f"No book titled {title!r} in the catalog.")
|
||||
return Book(title="Dune", author="Frank Herbert", year=1965)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(mcp) as client:
|
||||
result = await client.call_tool("lookup_book", {"title": "Dune"})
|
||||
|
||||
for block in result.content:
|
||||
if isinstance(block, TextContent):
|
||||
print(block.text)
|
||||
|
||||
print(result.structured_content)
|
||||
print(result.is_error)
|
||||
32
docs_src/client/tutorial004.py
Normal file
32
docs_src/client/tutorial004.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
from mcp_types import TextResourceContents
|
||||
|
||||
from mcp import Client
|
||||
from mcp.server import MCPServer
|
||||
|
||||
mcp = MCPServer("Bookshop")
|
||||
|
||||
|
||||
@mcp.resource("catalog://genres")
|
||||
def genres() -> list[str]:
|
||||
"""The genres the catalog is organised by."""
|
||||
return ["fiction", "non-fiction", "poetry"]
|
||||
|
||||
|
||||
@mcp.resource("catalog://genres/{genre}")
|
||||
def books_in_genre(genre: str) -> str:
|
||||
"""Every title we stock in one genre."""
|
||||
return f"3 books filed under {genre}."
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(mcp) as client:
|
||||
listed = await client.list_resources()
|
||||
print([resource.uri for resource in listed.resources])
|
||||
|
||||
templates = await client.list_resource_templates()
|
||||
print([template.uri_template for template in templates.resource_templates])
|
||||
|
||||
result = await client.read_resource("catalog://genres/poetry")
|
||||
for contents in result.contents:
|
||||
if isinstance(contents, TextResourceContents):
|
||||
print(contents.text)
|
||||
20
docs_src/client/tutorial005.py
Normal file
20
docs_src/client/tutorial005.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
from mcp import Client
|
||||
from mcp.server import MCPServer
|
||||
|
||||
mcp = MCPServer("Bookshop")
|
||||
|
||||
|
||||
@mcp.prompt(title="Recommend a book")
|
||||
def recommend(genre: str) -> str:
|
||||
"""Ask for a recommendation in a genre."""
|
||||
return f"Recommend one {genre} book from the catalog and say why."
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(mcp) as client:
|
||||
listed = await client.list_prompts()
|
||||
print(listed.prompts)
|
||||
|
||||
result = await client.get_prompt("recommend", {"genre": "poetry"})
|
||||
for message in result.messages:
|
||||
print(message.role, message.content)
|
||||
32
docs_src/client/tutorial006.py
Normal file
32
docs_src/client/tutorial006.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
from mcp_types import Completion, CompletionArgument, CompletionContext, PromptReference, ResourceTemplateReference
|
||||
|
||||
from mcp import Client
|
||||
from mcp.server import MCPServer
|
||||
|
||||
mcp = MCPServer("Bookshop")
|
||||
|
||||
GENRES = ["fiction", "non-fiction", "poetry"]
|
||||
|
||||
|
||||
@mcp.prompt()
|
||||
def recommend(genre: str) -> str:
|
||||
"""Ask for a recommendation in a genre."""
|
||||
return f"Recommend one {genre} book from the catalog and say why."
|
||||
|
||||
|
||||
@mcp.completion()
|
||||
async def complete_genre(
|
||||
ref: PromptReference | ResourceTemplateReference,
|
||||
argument: CompletionArgument,
|
||||
context: CompletionContext | None,
|
||||
) -> Completion | None:
|
||||
return Completion(values=[genre for genre in GENRES if genre.startswith(argument.value)])
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(mcp) as client:
|
||||
result = await client.complete(
|
||||
ref=PromptReference(type="ref/prompt", name="recommend"),
|
||||
argument={"name": "genre", "value": "p"},
|
||||
)
|
||||
print(result.completion.values)
|
||||
31
docs_src/client/tutorial007.py
Normal file
31
docs_src/client/tutorial007.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
from mcp_types import Tool
|
||||
|
||||
from mcp import Client
|
||||
from mcp.server import MCPServer
|
||||
|
||||
mcp = MCPServer("Bookshop")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def search_books(query: str) -> str:
|
||||
"""Search the catalog by title or author."""
|
||||
return f"Found 3 books matching {query!r}."
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def reserve_book(title: str) -> str:
|
||||
"""Put a book on hold."""
|
||||
return f"Reserved {title!r}."
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(mcp) as client:
|
||||
tools: list[Tool] = []
|
||||
cursor: str | None = None
|
||||
while True:
|
||||
page = await client.list_tools(cursor=cursor)
|
||||
tools.extend(page.tools)
|
||||
if page.next_cursor is None:
|
||||
break
|
||||
cursor = page.next_cursor
|
||||
print([tool.name for tool in tools])
|
||||
0
docs_src/client_callbacks/__init__.py
Normal file
0
docs_src/client_callbacks/__init__.py
Normal file
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue