chore: import upstream snapshot with attribution
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Waiting to run
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Waiting to run
This commit is contained in:
@@ -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,0 +1,3 @@
|
||||
<!doctype html>
|
||||
<title>Report</title>
|
||||
<p>Quarterly numbers render here.</p>
|
||||
@@ -0,0 +1,39 @@
|
||||
from mcp import Client
|
||||
from mcp.client import advertise
|
||||
from mcp.server.apps import APP_MIME_TYPE, EXTENSION_ID, Apps, client_supports_apps
|
||||
from mcp.server.mcpserver import MCPServer
|
||||
from mcp.server.mcpserver.context import Context
|
||||
|
||||
CLOCK_HTML = """\
|
||||
<!doctype html>
|
||||
<title>Clock</title>
|
||||
<h1 id="now">...</h1>
|
||||
<script>
|
||||
window.addEventListener("message", (event) => {
|
||||
const text = event.data?.result?.content?.[0]?.text;
|
||||
if (text) document.getElementById("now").textContent = text;
|
||||
});
|
||||
</script>
|
||||
"""
|
||||
|
||||
apps = Apps()
|
||||
|
||||
|
||||
@apps.tool(resource_uri="ui://clock/app.html", description="The current time.")
|
||||
def get_time(ctx: Context) -> str:
|
||||
now = "2026-06-26T12:00:00Z"
|
||||
if not client_supports_apps(ctx):
|
||||
return f"The time is {now}."
|
||||
return now
|
||||
|
||||
|
||||
apps.add_html_resource("ui://clock/app.html", CLOCK_HTML, title="Clock")
|
||||
|
||||
mcp = MCPServer("clock", extensions=[apps])
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(mcp, extensions=[advertise(EXTENSION_ID, {"mimeTypes": [APP_MIME_TYPE]})]) as client:
|
||||
result = await client.call_tool("get_time", {})
|
||||
print(result.content)
|
||||
# [TextContent(text='2026-06-26T12:00:00Z')]
|
||||
@@ -0,0 +1,25 @@
|
||||
from mcp.server.apps import Apps, ResourceCsp, ResourcePermissions
|
||||
from mcp.server.mcpserver import MCPServer
|
||||
|
||||
DASHBOARD_HTML = "<!doctype html><title>Dashboard</title><canvas id='chart'></canvas>"
|
||||
|
||||
apps = Apps()
|
||||
|
||||
|
||||
@apps.tool(resource_uri="ui://dashboard/app.html", visibility=["app"])
|
||||
def refresh_dashboard() -> str:
|
||||
"""Refresh the dashboard data."""
|
||||
return "refreshed"
|
||||
|
||||
|
||||
apps.add_html_resource(
|
||||
"ui://dashboard/app.html",
|
||||
DASHBOARD_HTML,
|
||||
title="Dashboard",
|
||||
csp=ResourceCsp(connect_domains=["https://api.example.com"]),
|
||||
permissions=ResourcePermissions(clipboard_write={}),
|
||||
domain="dashboard.example.com",
|
||||
prefers_border=True,
|
||||
)
|
||||
|
||||
mcp = MCPServer("dashboard", extensions=[apps])
|
||||
@@ -0,0 +1,20 @@
|
||||
from pathlib import Path
|
||||
|
||||
from mcp.server.apps import Apps
|
||||
from mcp.server.mcpserver import MCPServer
|
||||
from mcp.server.mcpserver.resources import FileResource
|
||||
|
||||
REPORT_HTML = Path(__file__).parent / "report.html"
|
||||
|
||||
apps = Apps()
|
||||
|
||||
|
||||
@apps.tool(resource_uri="ui://report/app.html")
|
||||
def refresh_report() -> str:
|
||||
"""Refresh the report data."""
|
||||
return "report refreshed"
|
||||
|
||||
|
||||
apps.add_resource(FileResource(uri="ui://report/app.html", name="report", path=REPORT_HTML))
|
||||
|
||||
mcp = MCPServer("report", extensions=[apps])
|
||||
@@ -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()
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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,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"]
|
||||
@@ -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,0 +1,19 @@
|
||||
from mcp.server import CacheHint, MCPServer
|
||||
|
||||
mcp = MCPServer(
|
||||
"Weather",
|
||||
cache_hints={
|
||||
"tools/list": CacheHint(ttl_ms=60_000, scope="public"),
|
||||
"resources/read": CacheHint(ttl_ms=5_000),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def forecast(city: str) -> str:
|
||||
return f"Sunny in {city}"
|
||||
|
||||
|
||||
@mcp.resource("config://units")
|
||||
def units() -> str:
|
||||
return "metric"
|
||||
@@ -0,0 +1,18 @@
|
||||
from typing import Any
|
||||
|
||||
from mcp_types import ListToolsResult, PaginatedRequestParams, Tool
|
||||
|
||||
from mcp.server import CacheHint, Server, ServerRequestContext
|
||||
|
||||
TOOLS = [Tool(name="forecast", input_schema={"type": "object"})]
|
||||
|
||||
|
||||
async def list_tools(ctx: ServerRequestContext[Any], params: PaginatedRequestParams | None) -> ListToolsResult:
|
||||
return ListToolsResult(tools=TOOLS, ttl_ms=1_000)
|
||||
|
||||
|
||||
server = Server(
|
||||
"Weather",
|
||||
on_list_tools=list_tools,
|
||||
cache_hints={"tools/list": CacheHint(ttl_ms=60_000, scope="public")},
|
||||
)
|
||||
@@ -0,0 +1,40 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from mcp_types import ListToolsResult, PaginatedRequestParams, Tool
|
||||
|
||||
from mcp import Client
|
||||
from mcp.client import CacheConfig
|
||||
from mcp.server import CacheHint, Server, ServerRequestContext
|
||||
|
||||
|
||||
@dataclass
|
||||
class DemoState:
|
||||
fetches: int = 0
|
||||
now: float = 1_000_000.0
|
||||
|
||||
|
||||
state = DemoState()
|
||||
|
||||
|
||||
async def list_tools(ctx: ServerRequestContext[Any], params: PaginatedRequestParams | None) -> ListToolsResult:
|
||||
state.fetches += 1
|
||||
return ListToolsResult(tools=[Tool(name="forecast", input_schema={"type": "object"})])
|
||||
|
||||
|
||||
server = Server(
|
||||
"Weather",
|
||||
on_list_tools=list_tools,
|
||||
cache_hints={"tools/list": CacheHint(ttl_ms=60_000, scope="public")},
|
||||
)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
start = state.fetches
|
||||
async with Client(server, cache=CacheConfig(clock=lambda: state.now)) as client:
|
||||
await client.list_tools() # fetch 1
|
||||
await client.list_tools() # fresh for 60s: served from the cache
|
||||
state.now += 60.0
|
||||
await client.list_tools() # the TTL ran out: fetch 2
|
||||
await client.list_tools(cache_mode="refresh") # skip the cache read: fetch 3
|
||||
print(f"4 calls, {state.fetches - start} fetches")
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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,0 +1,19 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
from mcp.server import MCPServer
|
||||
from mcp.server.mcpserver import Context
|
||||
|
||||
mcp = MCPServer("Library")
|
||||
|
||||
|
||||
class CardHolder(BaseModel):
|
||||
name: str
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def issue_card(ctx: Context) -> str:
|
||||
"""Issue a new library card."""
|
||||
answer = await ctx.elicit("What name should go on the card?", schema=CardHolder)
|
||||
if answer.action == "accept":
|
||||
return f"Card issued to {answer.data.name}."
|
||||
return "No card issued."
|
||||
@@ -0,0 +1,21 @@
|
||||
from mcp_types import ElicitRequestParams, ElicitResult
|
||||
|
||||
from mcp import Client
|
||||
from mcp.client import ClientRequestContext
|
||||
|
||||
|
||||
async def handle_elicitation(
|
||||
context: ClientRequestContext,
|
||||
params: ElicitRequestParams,
|
||||
) -> ElicitResult:
|
||||
return ElicitResult(action="accept", content={"name": "Ada Lovelace"})
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(
|
||||
"http://127.0.0.1:8000/mcp",
|
||||
mode="legacy",
|
||||
elicitation_callback=handle_elicitation,
|
||||
) as client:
|
||||
result = await client.call_tool("issue_card")
|
||||
print(result.content)
|
||||
@@ -0,0 +1,31 @@
|
||||
from mcp_types import ClientCapabilities, ElicitationCapability, RootsCapability, SamplingCapability
|
||||
from pydantic import BaseModel
|
||||
|
||||
from mcp.server import MCPServer
|
||||
from mcp.server.mcpserver import Context
|
||||
|
||||
mcp = MCPServer("Library")
|
||||
|
||||
|
||||
class CardHolder(BaseModel):
|
||||
name: str
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def issue_card(ctx: Context) -> str:
|
||||
"""Issue a new library card."""
|
||||
answer = await ctx.elicit("What name should go on the card?", schema=CardHolder)
|
||||
if answer.action == "accept":
|
||||
return f"Card issued to {answer.data.name}."
|
||||
return "No card issued."
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def client_features(ctx: Context) -> list[str]:
|
||||
"""Which optional features the connected client declared."""
|
||||
declared = {
|
||||
"elicitation": ClientCapabilities(elicitation=ElicitationCapability()),
|
||||
"sampling": ClientCapabilities(sampling=SamplingCapability()),
|
||||
"roots": ClientCapabilities(roots=RootsCapability()),
|
||||
}
|
||||
return [name for name, capability in declared.items() if ctx.session.check_client_capability(capability)]
|
||||
@@ -0,0 +1,19 @@
|
||||
from mcp_types import CreateMessageRequestParams, CreateMessageResult, ListRootsResult, Root, TextContent
|
||||
from pydantic import FileUrl
|
||||
|
||||
from mcp.client import ClientRequestContext
|
||||
|
||||
|
||||
async def handle_sampling(
|
||||
context: ClientRequestContext,
|
||||
params: CreateMessageRequestParams,
|
||||
) -> CreateMessageResult:
|
||||
return CreateMessageResult(
|
||||
role="assistant",
|
||||
content=TextContent(type="text", text="The answer is 42."),
|
||||
model="my-llm",
|
||||
)
|
||||
|
||||
|
||||
async def handle_list_roots(context: ClientRequestContext) -> ListRootsResult:
|
||||
return ListRootsResult(roots=[Root(uri=FileUrl("file:///home/ada/notebooks"), name="notebooks")])
|
||||
@@ -0,0 +1,16 @@
|
||||
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}."
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(mcp) as client:
|
||||
result = await client.call_tool("search_books", {"query": "dune"})
|
||||
print(result.structured_content)
|
||||
@@ -0,0 +1,7 @@
|
||||
from mcp import Client
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client("http://localhost:8000/mcp") as client:
|
||||
result = await client.list_tools()
|
||||
print([tool.name for tool in result.tools])
|
||||
@@ -0,0 +1,16 @@
|
||||
import httpx
|
||||
|
||||
from mcp import Client
|
||||
from mcp.client.streamable_http import streamable_http_client
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with httpx.AsyncClient(
|
||||
headers={"Authorization": "Bearer ..."},
|
||||
timeout=httpx.Timeout(30.0, read=300.0),
|
||||
follow_redirects=True,
|
||||
) as http_client:
|
||||
transport = streamable_http_client("http://localhost:8000/mcp", http_client=http_client)
|
||||
async with Client(transport) as client:
|
||||
result = await client.list_tools()
|
||||
print([tool.name for tool in result.tools])
|
||||
@@ -0,0 +1,14 @@
|
||||
from mcp import Client, StdioServerParameters
|
||||
from mcp.client.stdio import stdio_client
|
||||
|
||||
server = StdioServerParameters(
|
||||
command="uv",
|
||||
args=["run", "server.py"],
|
||||
env={"BOOKSHOP_API_KEY": "secret"},
|
||||
)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(stdio_client(server)) as client:
|
||||
result = await client.list_tools()
|
||||
print([tool.name for tool in result.tools])
|
||||
@@ -0,0 +1,15 @@
|
||||
from mcp.server import MCPServer
|
||||
|
||||
mcp = MCPServer("GitHub Explorer")
|
||||
|
||||
|
||||
@mcp.resource("github://repos/{owner}/{repo}")
|
||||
def github_repo(owner: str, repo: str) -> str:
|
||||
"""A GitHub repository."""
|
||||
return f"Repository: {owner}/{repo}"
|
||||
|
||||
|
||||
@mcp.prompt()
|
||||
def review_code(language: str, code: str) -> str:
|
||||
"""Review a snippet of code."""
|
||||
return f"Review this {language} code:\n{code}"
|
||||
@@ -0,0 +1,30 @@
|
||||
from mcp_types import Completion, CompletionArgument, CompletionContext, PromptReference, ResourceTemplateReference
|
||||
|
||||
from mcp.server import MCPServer
|
||||
|
||||
mcp = MCPServer("GitHub Explorer")
|
||||
|
||||
LANGUAGES = ["go", "javascript", "python", "rust", "typescript"]
|
||||
|
||||
|
||||
@mcp.resource("github://repos/{owner}/{repo}")
|
||||
def github_repo(owner: str, repo: str) -> str:
|
||||
"""A GitHub repository."""
|
||||
return f"Repository: {owner}/{repo}"
|
||||
|
||||
|
||||
@mcp.prompt()
|
||||
def review_code(language: str, code: str) -> str:
|
||||
"""Review a snippet of code."""
|
||||
return f"Review this {language} code:\n{code}"
|
||||
|
||||
|
||||
@mcp.completion()
|
||||
async def handle_completion(
|
||||
ref: PromptReference | ResourceTemplateReference,
|
||||
argument: CompletionArgument,
|
||||
context: CompletionContext | None,
|
||||
) -> Completion | None:
|
||||
if isinstance(ref, PromptReference) and argument.name == "language":
|
||||
return Completion(values=[lang for lang in LANGUAGES if lang.startswith(argument.value)])
|
||||
return None
|
||||
@@ -0,0 +1,40 @@
|
||||
from mcp_types import Completion, CompletionArgument, CompletionContext, PromptReference, ResourceTemplateReference
|
||||
|
||||
from mcp.server import MCPServer
|
||||
|
||||
mcp = MCPServer("GitHub Explorer")
|
||||
|
||||
LANGUAGES = ["go", "javascript", "python", "rust", "typescript"]
|
||||
|
||||
REPOS_BY_OWNER = {
|
||||
"modelcontextprotocol": ["python-sdk", "typescript-sdk", "inspector"],
|
||||
"pydantic": ["pydantic", "pydantic-ai", "logfire"],
|
||||
}
|
||||
|
||||
|
||||
@mcp.resource("github://repos/{owner}/{repo}")
|
||||
def github_repo(owner: str, repo: str) -> str:
|
||||
"""A GitHub repository."""
|
||||
return f"Repository: {owner}/{repo}"
|
||||
|
||||
|
||||
@mcp.prompt()
|
||||
def review_code(language: str, code: str) -> str:
|
||||
"""Review a snippet of code."""
|
||||
return f"Review this {language} code:\n{code}"
|
||||
|
||||
|
||||
@mcp.completion()
|
||||
async def handle_completion(
|
||||
ref: PromptReference | ResourceTemplateReference,
|
||||
argument: CompletionArgument,
|
||||
context: CompletionContext | None,
|
||||
) -> Completion | None:
|
||||
if isinstance(ref, PromptReference) and argument.name == "language":
|
||||
return Completion(values=[lang for lang in LANGUAGES if lang.startswith(argument.value)])
|
||||
if isinstance(ref, ResourceTemplateReference) and argument.name == "repo":
|
||||
if context is None or context.arguments is None:
|
||||
return None
|
||||
repos = REPOS_BY_OWNER.get(context.arguments.get("owner", ""), [])
|
||||
return Completion(values=[repo for repo in repos if repo.startswith(argument.value)])
|
||||
return None
|
||||
@@ -0,0 +1,10 @@
|
||||
from mcp.server import MCPServer
|
||||
from mcp.server.mcpserver import Context
|
||||
|
||||
mcp = MCPServer("Bookshop")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def search_books(query: str, ctx: Context) -> str:
|
||||
"""Search the catalog by title or author."""
|
||||
return f"[request {ctx.request_id}] Found 3 books matching {query!r}."
|
||||
@@ -0,0 +1,17 @@
|
||||
from mcp.server import MCPServer
|
||||
from mcp.server.mcpserver import Context
|
||||
|
||||
mcp = MCPServer("Bookshop")
|
||||
|
||||
|
||||
@mcp.resource("catalog://genres")
|
||||
def genres() -> str:
|
||||
"""The genres the catalog is organised into."""
|
||||
return "fiction, non-fiction, poetry"
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def describe_catalog(ctx: Context) -> str:
|
||||
"""Describe how the catalog is organised."""
|
||||
[contents] = await ctx.read_resource("catalog://genres")
|
||||
return f"The catalog is organised into: {contents.content}"
|
||||
@@ -0,0 +1,17 @@
|
||||
from mcp.server import MCPServer
|
||||
from mcp.server.mcpserver import Context
|
||||
|
||||
mcp = MCPServer("Bookshop")
|
||||
|
||||
|
||||
def recommend_book(genre: str) -> str:
|
||||
"""Recommend a book in the given genre."""
|
||||
return f"In {genre}, try 'Dune'."
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def enable_recommendations(ctx: Context) -> str:
|
||||
"""Switch on the recommendation tool."""
|
||||
mcp.add_tool(recommend_book)
|
||||
await ctx.session.send_tool_list_changed()
|
||||
return "Recommendations are now available."
|
||||
@@ -0,0 +1,27 @@
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from mcp.server import MCPServer
|
||||
from mcp.server.mcpserver import Resolve
|
||||
|
||||
mcp = MCPServer("Bookshop")
|
||||
|
||||
INVENTORY = {"Dune": 7, "Neuromancer": 0}
|
||||
|
||||
|
||||
class Stock(BaseModel):
|
||||
title: str
|
||||
copies: int
|
||||
|
||||
|
||||
async def check_stock(title: str) -> Stock:
|
||||
return Stock(title=title, copies=INVENTORY.get(title, 0))
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def reserve_book(title: str, stock: Annotated[Stock, Resolve(check_stock)]) -> str:
|
||||
"""Reserve a copy of a book."""
|
||||
if stock.copies == 0:
|
||||
return f"{title!r} is out of stock."
|
||||
return f"Reserved {title!r} ({stock.copies - 1} copies left)."
|
||||
@@ -0,0 +1,35 @@
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from mcp.server import MCPServer
|
||||
from mcp.server.mcpserver import Resolve
|
||||
|
||||
mcp = MCPServer("Bookshop")
|
||||
|
||||
INVENTORY = {"Dune": 7, "Neuromancer": 0}
|
||||
|
||||
|
||||
class Stock(BaseModel):
|
||||
title: str
|
||||
copies: int
|
||||
|
||||
|
||||
async def check_stock(title: str) -> Stock:
|
||||
return Stock(title=title, copies=INVENTORY.get(title, 0))
|
||||
|
||||
|
||||
async def estimate_delivery(stock: Annotated[Stock, Resolve(check_stock)]) -> str:
|
||||
return "tomorrow" if stock.copies > 0 else "in 2-3 weeks"
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def order_book(
|
||||
title: str,
|
||||
stock: Annotated[Stock, Resolve(check_stock)],
|
||||
delivery: Annotated[str, Resolve(estimate_delivery)],
|
||||
) -> str:
|
||||
"""Order a book from the shop."""
|
||||
if stock.copies == 0:
|
||||
return f"{title!r} is on backorder; it would arrive {delivery}."
|
||||
return f"Ordered {title!r}; it arrives {delivery}."
|
||||
@@ -0,0 +1,46 @@
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from mcp.server import MCPServer
|
||||
from mcp.server.mcpserver import Elicit, Resolve
|
||||
|
||||
mcp = MCPServer("Bookshop")
|
||||
|
||||
INVENTORY = {"Dune": 7, "Neuromancer": 0}
|
||||
|
||||
|
||||
class Stock(BaseModel):
|
||||
title: str
|
||||
copies: int
|
||||
|
||||
|
||||
class Backorder(BaseModel):
|
||||
confirm: bool = Field(description="Order anyway and wait?")
|
||||
|
||||
|
||||
async def check_stock(title: str) -> Stock:
|
||||
return Stock(title=title, copies=INVENTORY.get(title, 0))
|
||||
|
||||
|
||||
async def confirm_backorder(
|
||||
title: str,
|
||||
stock: Annotated[Stock, Resolve(check_stock)],
|
||||
) -> Backorder | Elicit[Backorder]:
|
||||
if stock.copies > 0:
|
||||
return Backorder(confirm=True) # in stock: nothing to ask
|
||||
return Elicit(f"{title!r} is out of stock (2-3 weeks). Order anyway?", Backorder)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def order_book(
|
||||
title: str,
|
||||
stock: Annotated[Stock, Resolve(check_stock)],
|
||||
backorder: Annotated[Backorder, Resolve(confirm_backorder)],
|
||||
) -> str:
|
||||
"""Order a book from the shop."""
|
||||
if not backorder.confirm:
|
||||
return "No order placed."
|
||||
if stock.copies == 0:
|
||||
return f"Backordered {title!r}; it ships in 2-3 weeks."
|
||||
return f"Ordered {title!r}."
|
||||
@@ -0,0 +1,26 @@
|
||||
from typing import Annotated
|
||||
|
||||
from mcp_types import CreateMessageResult, SamplingMessage, TextContent
|
||||
|
||||
from mcp.server import MCPServer
|
||||
from mcp.server.mcpserver import Resolve, Sample
|
||||
|
||||
mcp = MCPServer("Bookshop")
|
||||
|
||||
|
||||
def suggest_title(genre: str) -> Sample:
|
||||
prompt = f"Suggest one {genre} book title. Answer with the title only."
|
||||
return Sample(
|
||||
[SamplingMessage(role="user", content=TextContent(type="text", text=prompt))],
|
||||
max_tokens=50,
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def recommend_book(
|
||||
genre: str,
|
||||
suggestion: Annotated[CreateMessageResult, Resolve(suggest_title)],
|
||||
) -> str:
|
||||
"""Recommend a book in the given genre."""
|
||||
title = suggestion.content.text if suggestion.content.type == "text" else "the classics"
|
||||
return f"Today's {genre} pick: {title}"
|
||||
@@ -0,0 +1,17 @@
|
||||
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}"
|
||||
|
||||
|
||||
security = TransportSecuritySettings(
|
||||
allowed_hosts=["mcp.example.com", "mcp.example.com:*"],
|
||||
allowed_origins=["https://app.example.com"],
|
||||
)
|
||||
app = mcp.streamable_http_app(transport_security=security)
|
||||
@@ -0,0 +1,27 @@
|
||||
from mcp_types import ElicitRequest, ElicitRequestFormParams, ElicitResult, InputRequiredResult
|
||||
|
||||
from mcp.server.mcpserver import Context, MCPServer
|
||||
|
||||
CONFIRM = ElicitRequest(
|
||||
params=ElicitRequestFormParams(
|
||||
message="Issue this refund?",
|
||||
requested_schema={"type": "object", "properties": {"ok": {"type": "boolean"}}, "required": ["ok"]},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def make_server() -> MCPServer:
|
||||
"""Every worker process builds one of these, once, at import."""
|
||||
mcp = MCPServer("billing")
|
||||
|
||||
@mcp.tool()
|
||||
async def refund(amount: int, ctx: Context) -> str | InputRequiredResult:
|
||||
"""Refund an amount, once a human has confirmed it."""
|
||||
if ctx.input_responses is None:
|
||||
return InputRequiredResult(input_requests={"ok": CONFIRM}, request_state=f"refund:{amount}")
|
||||
answer = (ctx.input_responses or {}).get("ok")
|
||||
if not isinstance(answer, ElicitResult) or answer.action != "accept" or not (answer.content or {}).get("ok"):
|
||||
return "refund cancelled"
|
||||
return f"refunded ${amount}"
|
||||
|
||||
return mcp
|
||||
@@ -0,0 +1,27 @@
|
||||
from mcp_types import ElicitRequest, ElicitRequestFormParams, ElicitResult, InputRequiredResult
|
||||
|
||||
from mcp.server.mcpserver import Context, MCPServer, RequestStateSecurity
|
||||
|
||||
CONFIRM = ElicitRequest(
|
||||
params=ElicitRequestFormParams(
|
||||
message="Issue this refund?",
|
||||
requested_schema={"type": "object", "properties": {"ok": {"type": "boolean"}}, "required": ["ok"]},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def make_server(key: str) -> MCPServer:
|
||||
"""Every worker process: the same key, and the same name."""
|
||||
mcp = MCPServer("billing", request_state_security=RequestStateSecurity(keys=[key]))
|
||||
|
||||
@mcp.tool()
|
||||
async def refund(amount: int, ctx: Context) -> str | InputRequiredResult:
|
||||
"""Refund an amount, once a human has confirmed it."""
|
||||
if ctx.input_responses is None:
|
||||
return InputRequiredResult(input_requests={"ok": CONFIRM}, request_state=f"refund:{amount}")
|
||||
answer = (ctx.input_responses or {}).get("ok")
|
||||
if not isinstance(answer, ElicitResult) or answer.action != "accept" or not (answer.content or {}).get("ok"):
|
||||
return "refund cancelled"
|
||||
return f"refunded ${amount}"
|
||||
|
||||
return mcp
|
||||
@@ -0,0 +1,23 @@
|
||||
from mcp.server.mcpserver import Context, MCPServer
|
||||
from mcp.server.subscriptions import SubscriptionBus
|
||||
|
||||
NOTES = {"todo": "buy milk"}
|
||||
|
||||
|
||||
def make_server(bus: SubscriptionBus) -> MCPServer:
|
||||
"""Every replica gets its own server object; all of them hold the same bus."""
|
||||
mcp = MCPServer("Notebook", subscriptions=bus)
|
||||
|
||||
@mcp.resource("note://{name}")
|
||||
def note(name: str) -> str:
|
||||
"""One note, by name."""
|
||||
return NOTES[name]
|
||||
|
||||
@mcp.tool()
|
||||
async def edit_note(name: str, text: str, ctx: Context) -> str:
|
||||
"""Replace a note's text."""
|
||||
NOTES[name] = text
|
||||
await ctx.notify_resource_updated(f"note://{name}")
|
||||
return "saved"
|
||||
|
||||
return mcp
|
||||
@@ -0,0 +1,26 @@
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from mcp.server import MCPServer
|
||||
from mcp.server.mcpserver import Context
|
||||
|
||||
mcp = MCPServer("Bistro")
|
||||
|
||||
|
||||
class AlternativeDate(BaseModel):
|
||||
accept_alternative: bool = Field(description="Try another date?")
|
||||
date: str = Field(default="2025-12-26", description="Alternative date (YYYY-MM-DD)")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def book_table(date: str, party_size: int, ctx: Context) -> str:
|
||||
"""Book a table at the bistro."""
|
||||
if date != "2025-12-25":
|
||||
return f"Booked a table for {party_size} on {date}."
|
||||
|
||||
result = await ctx.elicit(
|
||||
message=f"No tables for {party_size} on {date}. Would you like to try another date?",
|
||||
schema=AlternativeDate,
|
||||
)
|
||||
if result.action == "accept" and result.data.accept_alternative:
|
||||
return await book_table(result.data.date, party_size, ctx)
|
||||
return "No booking made."
|
||||
@@ -0,0 +1,24 @@
|
||||
from mcp.server import MCPServer
|
||||
from mcp.server.mcpserver import Context
|
||||
|
||||
mcp = MCPServer("Bistro")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def pay_deposit(booking_id: str, ctx: Context) -> str:
|
||||
"""Take the deposit that confirms a booking."""
|
||||
result = await ctx.elicit_url(
|
||||
message="A 20 EUR deposit confirms your booking.",
|
||||
url=f"https://pay.example.com/deposit/{booking_id}",
|
||||
elicitation_id=f"deposit-{booking_id}",
|
||||
)
|
||||
if result.action == "accept":
|
||||
return "Complete the payment in your browser."
|
||||
return "No deposit taken. The booking expires in one hour."
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def confirm_deposit(booking_id: str, ctx: Context) -> str:
|
||||
"""Record a payment reported by the payment provider."""
|
||||
await ctx.session.send_elicit_complete(f"deposit-{booking_id}")
|
||||
return f"Deposit received for booking {booking_id}."
|
||||
@@ -0,0 +1,22 @@
|
||||
from mcp_types import ElicitRequestParams, ElicitRequestURLParams, ElicitResult
|
||||
|
||||
from mcp import Client
|
||||
from mcp.client import ClientRequestContext
|
||||
|
||||
|
||||
async def handle_elicitation(context: ClientRequestContext, params: ElicitRequestParams) -> ElicitResult:
|
||||
if isinstance(params, ElicitRequestURLParams):
|
||||
print(f"Open this link to continue: {params.url}")
|
||||
return ElicitResult(action="accept")
|
||||
print(params.message)
|
||||
return ElicitResult(action="accept", content={"accept_alternative": True, "date": "2025-12-27"})
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(
|
||||
"http://127.0.0.1:8000/mcp",
|
||||
mode="legacy",
|
||||
elicitation_callback=handle_elicitation,
|
||||
) as client:
|
||||
result = await client.call_tool("book_table", {"date": "2025-12-25", "party_size": 2})
|
||||
print(result.content)
|
||||
@@ -0,0 +1,47 @@
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from mcp.server import MCPServer
|
||||
from mcp.server.mcpserver import (
|
||||
AcceptedElicitation,
|
||||
CancelledElicitation,
|
||||
DeclinedElicitation,
|
||||
Elicit,
|
||||
ElicitationResult,
|
||||
Resolve,
|
||||
)
|
||||
|
||||
mcp = MCPServer("Files")
|
||||
|
||||
_FOLDERS: dict[str, list[str]] = {"/tmp/empty": [], "/tmp/project": ["main.py", "README.md"]}
|
||||
|
||||
|
||||
class Confirm(BaseModel):
|
||||
ok: bool
|
||||
|
||||
|
||||
async def confirm_delete(path: str) -> Confirm | Elicit[Confirm]:
|
||||
"""Resolver: ask for confirmation only when the folder is not empty."""
|
||||
file_count = len(_FOLDERS.get(path, []))
|
||||
if file_count == 0:
|
||||
return Confirm(ok=True) # nothing to confirm, no round-trip to the client
|
||||
return Elicit(f"{path} has {file_count} file(s). Delete anyway?", Confirm)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def delete_folder(
|
||||
path: str,
|
||||
confirm: Annotated[ElicitationResult[Confirm], Resolve(confirm_delete)],
|
||||
) -> str:
|
||||
"""Delete a folder, asking for confirmation when it is not empty."""
|
||||
match confirm:
|
||||
case AcceptedElicitation(data=Confirm(ok=True)):
|
||||
_FOLDERS.pop(path, None)
|
||||
return f"deleted {path}"
|
||||
case AcceptedElicitation():
|
||||
return "kept the folder"
|
||||
case DeclinedElicitation():
|
||||
return "declined: folder not deleted"
|
||||
case CancelledElicitation():
|
||||
return "cancelled: folder not deleted"
|
||||
@@ -0,0 +1,4 @@
|
||||
from mcp.server.apps import Apps
|
||||
from mcp.server.mcpserver import MCPServer
|
||||
|
||||
mcp = MCPServer("demo", extensions=[Apps()])
|
||||
@@ -0,0 +1,5 @@
|
||||
from mcp.server.extension import Extension
|
||||
|
||||
|
||||
class Stamps(Extension):
|
||||
identifier = "com.example/stamps"
|
||||
@@ -0,0 +1,35 @@
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from mcp import Client
|
||||
from mcp.server.extension import Extension, ToolBinding
|
||||
from mcp.server.mcpserver import MCPServer
|
||||
|
||||
|
||||
def stamp(text: str) -> str:
|
||||
"""Stamp a message with the office seal."""
|
||||
return f"[stamped] {text}"
|
||||
|
||||
|
||||
class Stamps(Extension):
|
||||
"""A purely additive extension: one tool, one capability entry."""
|
||||
|
||||
identifier = "com.example/stamps"
|
||||
|
||||
def settings(self) -> dict[str, Any]:
|
||||
return {"sealed": True}
|
||||
|
||||
def tools(self) -> Sequence[ToolBinding]:
|
||||
return [ToolBinding(fn=stamp)]
|
||||
|
||||
|
||||
mcp = MCPServer("post-office", extensions=[Stamps()])
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(mcp) as client:
|
||||
print(client.server_capabilities.extensions)
|
||||
# {'com.example/stamps': {'sealed': True}}
|
||||
result = await client.call_tool("stamp", {"text": "hello"})
|
||||
print(result.content)
|
||||
# [TextContent(text='[stamped] hello')]
|
||||
@@ -0,0 +1,59 @@
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, Literal
|
||||
|
||||
import mcp_types as types
|
||||
from pydantic import Field
|
||||
|
||||
from mcp import Client
|
||||
from mcp.client import advertise
|
||||
from mcp.server.context import ServerRequestContext
|
||||
from mcp.server.extension import Extension, MethodBinding
|
||||
from mcp.server.mcpserver import MCPServer, require_client_extension
|
||||
|
||||
EXTENSION_ID = "com.example/search"
|
||||
|
||||
|
||||
class SearchParams(types.RequestParams):
|
||||
query: str
|
||||
limit: int = Field(default=10, ge=1, le=100)
|
||||
|
||||
|
||||
class SearchResult(types.Result):
|
||||
items: list[str]
|
||||
|
||||
|
||||
class SearchRequest(types.Request[SearchParams, Literal["com.example/search"]]):
|
||||
method: Literal["com.example/search"] = "com.example/search"
|
||||
params: SearchParams
|
||||
|
||||
|
||||
async def search(ctx: ServerRequestContext[Any, Any], params: SearchParams) -> SearchResult:
|
||||
require_client_extension(ctx, EXTENSION_ID)
|
||||
return SearchResult(items=[f"{params.query}-{n}" for n in range(params.limit)])
|
||||
|
||||
|
||||
class Search(Extension):
|
||||
"""An extension that serves its own request method."""
|
||||
|
||||
identifier = EXTENSION_ID
|
||||
|
||||
def methods(self) -> Sequence[MethodBinding]:
|
||||
return [
|
||||
MethodBinding(
|
||||
"com.example/search",
|
||||
SearchParams,
|
||||
search,
|
||||
protocol_versions=frozenset({"2026-07-28"}),
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
mcp = MCPServer("catalog", extensions=[Search()])
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(mcp, extensions=[advertise(EXTENSION_ID)]) as client:
|
||||
request = SearchRequest(params=SearchParams(query="mcp", limit=3))
|
||||
result = await client.session.send_request(request, SearchResult)
|
||||
print(result.items)
|
||||
# ['mcp-0', 'mcp-1', 'mcp-2']
|
||||
@@ -0,0 +1,34 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from mcp_types import CallToolRequestParams
|
||||
|
||||
from mcp.server.context import CallNext, HandlerResult, ServerRequestContext
|
||||
from mcp.server.extension import Extension
|
||||
from mcp.server.mcpserver import MCPServer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AuditLog(Extension):
|
||||
"""Observe every tools/call without touching its result."""
|
||||
|
||||
identifier = "com.example/audit"
|
||||
|
||||
async def intercept_tool_call(
|
||||
self,
|
||||
params: CallToolRequestParams,
|
||||
ctx: ServerRequestContext[Any, Any],
|
||||
call_next: CallNext,
|
||||
) -> HandlerResult:
|
||||
logger.info("tool %r called", params.name)
|
||||
return await call_next(ctx)
|
||||
|
||||
|
||||
mcp = MCPServer("audited", extensions=[AuditLog()])
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def add(a: int, b: int) -> int:
|
||||
"""Add two numbers."""
|
||||
return a + b
|
||||
@@ -0,0 +1,70 @@
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, Literal
|
||||
|
||||
import mcp_types as types
|
||||
|
||||
from mcp import Client
|
||||
from mcp.client import ClaimContext, ClientExtension, ResultClaim
|
||||
from mcp.server.context import CallNext, HandlerResult, ServerRequestContext
|
||||
from mcp.server.extension import Extension
|
||||
from mcp.server.mcpserver import MCPServer, require_client_extension
|
||||
|
||||
EXTENSION_ID = "com.example/receipts"
|
||||
|
||||
|
||||
class ReceiptResult(types.Result):
|
||||
"""The claimed result shape; `result_type` pins the wire tag."""
|
||||
|
||||
result_type: Literal["receipt"] = "receipt"
|
||||
receipt_token: str
|
||||
|
||||
|
||||
class ReceiptIssuer(Extension):
|
||||
"""Server half: answers `buy` with a receipt instead of a final result."""
|
||||
|
||||
identifier = EXTENSION_ID
|
||||
|
||||
async def intercept_tool_call(
|
||||
self,
|
||||
params: types.CallToolRequestParams,
|
||||
ctx: ServerRequestContext[Any, Any],
|
||||
call_next: CallNext,
|
||||
) -> HandlerResult:
|
||||
if params.name != "buy":
|
||||
return await call_next(ctx)
|
||||
require_client_extension(ctx, EXTENSION_ID)
|
||||
return {"resultType": "receipt", "receiptToken": "r-117"}
|
||||
|
||||
|
||||
class Receipts(ClientExtension):
|
||||
"""Client half: claims the `receipt` shape and supplies the code that finishes it."""
|
||||
|
||||
identifier = EXTENSION_ID
|
||||
|
||||
def claims(self) -> Sequence[ResultClaim[Any]]:
|
||||
return [ResultClaim(result_type="receipt", model=ReceiptResult, resolve=self._redeem)]
|
||||
|
||||
async def _redeem(self, claimed: ReceiptResult, ctx: ClaimContext) -> types.CallToolResult:
|
||||
return await ctx.session.call_tool("redeem", {"token": claimed.receipt_token})
|
||||
|
||||
|
||||
mcp = MCPServer("shop", extensions=[ReceiptIssuer()])
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def buy(item: str) -> types.CallToolResult:
|
||||
"""Buy an item."""
|
||||
raise NotImplementedError # ReceiptIssuer answers `buy` before the tool runs
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def redeem(token: str) -> str:
|
||||
"""Exchange a receipt token for the goods."""
|
||||
return f"goods for {token}"
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(mcp, extensions=[Receipts()]) as client:
|
||||
result = await client.call_tool("buy", {"item": "lamp"})
|
||||
print(result.content)
|
||||
# [TextContent(text='goods for r-117')]
|
||||
@@ -0,0 +1,50 @@
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, Literal
|
||||
|
||||
import mcp_types as types
|
||||
|
||||
from mcp import Client
|
||||
from mcp.client import advertise
|
||||
from mcp.server.context import ServerRequestContext
|
||||
from mcp.server.extension import Extension, MethodBinding
|
||||
from mcp.server.mcpserver import MCPServer
|
||||
|
||||
EXTENSION_ID = "com.example/jobs"
|
||||
|
||||
|
||||
class JobParams(types.RequestParams):
|
||||
job_id: str
|
||||
|
||||
|
||||
class JobStatus(types.Result):
|
||||
status: str
|
||||
|
||||
|
||||
class JobStatusRequest(types.Request[JobParams, Literal["com.example/jobs.status"]]):
|
||||
method: Literal["com.example/jobs.status"] = "com.example/jobs.status"
|
||||
params: JobParams
|
||||
name_param = "jobId" # params["jobId"] rides the Mcp-Name header
|
||||
|
||||
|
||||
async def job_status(ctx: ServerRequestContext[Any, Any], params: JobParams) -> JobStatus:
|
||||
return JobStatus(status=f"{params.job_id} is running")
|
||||
|
||||
|
||||
class Jobs(Extension):
|
||||
"""An extension whose verb names its subject, so the header can route on it."""
|
||||
|
||||
identifier = EXTENSION_ID
|
||||
|
||||
def methods(self) -> Sequence[MethodBinding]:
|
||||
return [MethodBinding("com.example/jobs.status", JobParams, job_status)]
|
||||
|
||||
|
||||
mcp = MCPServer("worker", extensions=[Jobs()])
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(mcp, extensions=[advertise(EXTENSION_ID)]) as client:
|
||||
request = JobStatusRequest(params=JobParams(job_id="job-7"))
|
||||
result = await client.session.send_request(request, JobStatus)
|
||||
print(result.status)
|
||||
# job-7 is running
|
||||
@@ -0,0 +1,21 @@
|
||||
from mcp.server import MCPServer
|
||||
|
||||
mcp = MCPServer("Demo")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def add(a: int, b: int) -> int:
|
||||
"""Add two numbers."""
|
||||
return a + b
|
||||
|
||||
|
||||
@mcp.resource("greeting://{name}")
|
||||
def greeting(name: str) -> str:
|
||||
"""Greet someone by name."""
|
||||
return f"Hello, {name}!"
|
||||
|
||||
|
||||
@mcp.prompt()
|
||||
def summarize(text: str) -> str:
|
||||
"""Summarize a piece of text in one sentence."""
|
||||
return f"Summarize the following text in one sentence:\n\n{text}"
|
||||
@@ -0,0 +1,13 @@
|
||||
from mcp.server import MCPServer
|
||||
|
||||
mcp = MCPServer("Bookshop")
|
||||
|
||||
CATALOG = {"Dune": "Frank Herbert", "Neuromancer": "William Gibson"}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def get_author(title: str) -> str:
|
||||
"""Look up the author of a book in the catalog."""
|
||||
if title not in CATALOG:
|
||||
raise ValueError(f"No book titled {title!r} in the catalog.")
|
||||
return CATALOG[title]
|
||||
@@ -0,0 +1,16 @@
|
||||
from mcp_types import INVALID_PARAMS
|
||||
|
||||
from mcp import MCPError
|
||||
from mcp.server import MCPServer
|
||||
|
||||
mcp = MCPServer("Bookshop")
|
||||
|
||||
CATALOG = {"Dune": "Frank Herbert", "Neuromancer": "William Gibson"}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def get_author(title: str) -> str:
|
||||
"""Look up the author of a book in the catalog."""
|
||||
if title not in CATALOG:
|
||||
raise MCPError(code=INVALID_PARAMS, message=f"No book titled {title!r} in the catalog.")
|
||||
return CATALOG[title]
|
||||
@@ -0,0 +1,14 @@
|
||||
from mcp.server import MCPServer
|
||||
from mcp.server.mcpserver.exceptions import ResourceNotFoundError
|
||||
|
||||
mcp = MCPServer("Bookshop")
|
||||
|
||||
CATALOG = {"Dune": "Frank Herbert", "Neuromancer": "William Gibson"}
|
||||
|
||||
|
||||
@mcp.resource("books://{title}")
|
||||
def book(title: str) -> str:
|
||||
"""The catalog entry for one book."""
|
||||
if title not in CATALOG:
|
||||
raise ResourceNotFoundError(f"No book titled {title!r} in the catalog.")
|
||||
return f"{title} by {CATALOG[title]}"
|
||||
@@ -0,0 +1,69 @@
|
||||
import time
|
||||
import uuid
|
||||
|
||||
import httpx
|
||||
import jwt
|
||||
|
||||
from mcp import Client
|
||||
from mcp.client.auth.extensions.identity_assertion import IdentityAssertionOAuthProvider
|
||||
from mcp.client.streamable_http import streamable_http_client
|
||||
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
|
||||
|
||||
IDP_SIGNING_KEY = "the-enterprise-idp-signing-key"
|
||||
|
||||
|
||||
class InMemoryTokenStorage:
|
||||
def __init__(self) -> None:
|
||||
self.tokens: OAuthToken | None = None
|
||||
self.client_info: OAuthClientInformationFull | None = None
|
||||
|
||||
async def get_tokens(self) -> OAuthToken | None:
|
||||
return self.tokens
|
||||
|
||||
async def set_tokens(self, tokens: OAuthToken) -> None:
|
||||
self.tokens = tokens
|
||||
|
||||
async def get_client_info(self) -> OAuthClientInformationFull | None:
|
||||
return self.client_info
|
||||
|
||||
async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
|
||||
self.client_info = client_info
|
||||
|
||||
|
||||
def idp_issue_id_jag(subject: str, audience: str, resource: str) -> str:
|
||||
now = int(time.time())
|
||||
claims = {
|
||||
"iss": "https://idp.example.com",
|
||||
"sub": subject,
|
||||
"aud": audience,
|
||||
"client_id": "finance-agent",
|
||||
"resource": resource,
|
||||
"scope": "notes:read",
|
||||
"jti": str(uuid.uuid4()),
|
||||
"iat": now,
|
||||
"exp": now + 300,
|
||||
}
|
||||
return jwt.encode(claims, IDP_SIGNING_KEY, algorithm="HS256", headers={"typ": "oauth-id-jag+jwt"})
|
||||
|
||||
|
||||
async def fetch_id_jag(audience: str, resource: str) -> str:
|
||||
return idp_issue_id_jag("alice@example.com", audience, resource)
|
||||
|
||||
|
||||
oauth = IdentityAssertionOAuthProvider(
|
||||
server_url="http://localhost:8001/mcp",
|
||||
storage=InMemoryTokenStorage(),
|
||||
client_id="finance-agent",
|
||||
client_secret="finance-agent-secret",
|
||||
issuer="https://auth.example.com/",
|
||||
assertion_provider=fetch_id_jag,
|
||||
scope="notes:read",
|
||||
)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with httpx.AsyncClient(auth=oauth, follow_redirects=True) as http_client:
|
||||
transport = streamable_http_client("http://localhost:8001/mcp", http_client=http_client)
|
||||
async with Client(transport) as client:
|
||||
result = await client.list_tools()
|
||||
print([tool.name for tool in result.tools])
|
||||
@@ -0,0 +1,107 @@
|
||||
import secrets
|
||||
import time
|
||||
|
||||
import jwt
|
||||
from pydantic import AnyHttpUrl
|
||||
from starlette.applications import Starlette
|
||||
|
||||
from mcp.server.auth.provider import (
|
||||
AccessToken,
|
||||
AuthorizationCode,
|
||||
AuthorizationParams,
|
||||
AuthorizeError,
|
||||
IdentityAssertionParams,
|
||||
OAuthAuthorizationServerProvider,
|
||||
RefreshToken,
|
||||
TokenError,
|
||||
)
|
||||
from mcp.server.auth.routes import create_auth_routes
|
||||
from mcp.shared.auth import JWT_BEARER_GRANT_TYPE, OAuthClientInformationFull, OAuthToken
|
||||
|
||||
ISSUER = "https://auth.example.com/"
|
||||
MCP_SERVER = "http://localhost:8001/mcp"
|
||||
IDP_ISSUER = "https://idp.example.com"
|
||||
IDP_SIGNING_KEY = "the-enterprise-idp-signing-key"
|
||||
|
||||
REGISTERED_CLIENTS = {
|
||||
"finance-agent": OAuthClientInformationFull(
|
||||
client_id="finance-agent",
|
||||
client_secret="finance-agent-secret",
|
||||
redirect_uris=None,
|
||||
grant_types=[JWT_BEARER_GRANT_TYPE],
|
||||
token_endpoint_auth_method="client_secret_post",
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
class EnterpriseAuthorizationServer(OAuthAuthorizationServerProvider[AuthorizationCode, RefreshToken, AccessToken]):
|
||||
def __init__(self) -> None:
|
||||
self.access_tokens: dict[str, AccessToken] = {}
|
||||
self.seen_jtis: set[str] = set()
|
||||
|
||||
async def get_client(self, client_id: str) -> OAuthClientInformationFull | None:
|
||||
return REGISTERED_CLIENTS.get(client_id)
|
||||
|
||||
async def load_access_token(self, token: str) -> AccessToken | None:
|
||||
return self.access_tokens.get(token)
|
||||
|
||||
async def exchange_identity_assertion(
|
||||
self, client: OAuthClientInformationFull, params: IdentityAssertionParams
|
||||
) -> OAuthToken:
|
||||
try:
|
||||
header = jwt.get_unverified_header(params.assertion)
|
||||
claims = jwt.decode(
|
||||
params.assertion,
|
||||
IDP_SIGNING_KEY,
|
||||
algorithms=["HS256"],
|
||||
issuer=IDP_ISSUER,
|
||||
audience=ISSUER,
|
||||
options={"require": ["iss", "sub", "aud", "exp", "iat", "jti", "client_id", "resource", "scope"]},
|
||||
)
|
||||
except jwt.InvalidTokenError as error:
|
||||
raise TokenError("invalid_grant", "the assertion did not verify") from error
|
||||
if header.get("typ") != "oauth-id-jag+jwt":
|
||||
raise TokenError("invalid_grant", "the assertion is not an ID-JAG")
|
||||
if claims["client_id"] != client.client_id:
|
||||
raise TokenError("invalid_grant", "the assertion was issued to a different client")
|
||||
if claims["resource"] != MCP_SERVER:
|
||||
raise TokenError("invalid_target", "the assertion is for a resource this server does not serve")
|
||||
if claims["jti"] in self.seen_jtis:
|
||||
raise TokenError("invalid_grant", "the assertion has already been used")
|
||||
self.seen_jtis.add(claims["jti"])
|
||||
scopes = claims["scope"].split()
|
||||
access_token = f"mcp_{secrets.token_hex(16)}"
|
||||
self.access_tokens[access_token] = AccessToken(
|
||||
token=access_token,
|
||||
client_id=claims["client_id"],
|
||||
scopes=scopes,
|
||||
expires_at=int(time.time()) + 300,
|
||||
resource=claims["resource"],
|
||||
subject=claims["sub"],
|
||||
)
|
||||
return OAuthToken(access_token=access_token, token_type="Bearer", expires_in=300, scope=" ".join(scopes))
|
||||
|
||||
async def authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str:
|
||||
raise AuthorizeError("unauthorized_client", "this authorization server only accepts ID-JAGs")
|
||||
|
||||
async def load_authorization_code(self, client: OAuthClientInformationFull, authorization_code: str) -> None:
|
||||
return None
|
||||
|
||||
async def exchange_authorization_code(
|
||||
self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode
|
||||
) -> OAuthToken:
|
||||
raise TokenError("invalid_grant", "this authorization server only accepts ID-JAGs")
|
||||
|
||||
async def load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) -> None:
|
||||
return None
|
||||
|
||||
async def exchange_refresh_token(
|
||||
self, client: OAuthClientInformationFull, refresh_token: RefreshToken, scopes: list[str]
|
||||
) -> OAuthToken:
|
||||
raise TokenError("invalid_grant", "this authorization server only accepts ID-JAGs")
|
||||
|
||||
|
||||
provider = EnterpriseAuthorizationServer()
|
||||
auth_app = Starlette(
|
||||
routes=create_auth_routes(provider, issuer_url=AnyHttpUrl(ISSUER), identity_assertion_enabled=True)
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
from mcp.server import MCPServer
|
||||
|
||||
mcp = MCPServer("Demo")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def add(a: int, b: int) -> int:
|
||||
"""Add two numbers."""
|
||||
return a + b
|
||||
|
||||
|
||||
@mcp.resource("greeting://{name}")
|
||||
def greeting(name: str) -> str:
|
||||
"""Greet someone by name."""
|
||||
return f"Hello, {name}!"
|
||||
@@ -0,0 +1,42 @@
|
||||
from typing import Annotated
|
||||
|
||||
from mcp_types import ElicitRequestParams, ElicitResult
|
||||
from pydantic import BaseModel
|
||||
|
||||
from mcp import Client
|
||||
from mcp.client import ClientRequestContext
|
||||
from mcp.server import MCPServer
|
||||
from mcp.server.mcpserver import AcceptedElicitation, Elicit, ElicitationResult, Resolve
|
||||
|
||||
mcp = MCPServer("Bookshop")
|
||||
|
||||
|
||||
class Quantity(BaseModel):
|
||||
copies: int
|
||||
|
||||
|
||||
async def ask_quantity() -> Elicit[Quantity]:
|
||||
"""Resolver: ask the user how many copies to put aside."""
|
||||
return Elicit("How many copies?", Quantity)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def reserve(title: str, quantity: Annotated[ElicitationResult[Quantity], Resolve(ask_quantity)]) -> str:
|
||||
"""Reserve copies of a book, asking the user how many."""
|
||||
if isinstance(quantity, AcceptedElicitation):
|
||||
return f"Reserved {quantity.data.copies} of {title!r}."
|
||||
return "Nothing reserved."
|
||||
|
||||
|
||||
async def answer(context: ClientRequestContext, params: ElicitRequestParams) -> ElicitResult:
|
||||
return ElicitResult(action="accept", content={"copies": 2})
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with (
|
||||
Client(mcp, mode="legacy", elicitation_callback=answer) as legacy,
|
||||
Client(mcp, elicitation_callback=answer) as modern,
|
||||
):
|
||||
for client in (legacy, modern):
|
||||
result = await client.call_tool("reserve", {"title": "Dune"})
|
||||
print(client.protocol_version, result.structured_content)
|
||||
@@ -0,0 +1,28 @@
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from mcp.server import MCPServer
|
||||
from mcp.server.mcpserver import AcceptedElicitation, Elicit, ElicitationResult, Resolve
|
||||
|
||||
mcp = MCPServer("Bookshop")
|
||||
|
||||
|
||||
class Quantity(BaseModel):
|
||||
copies: int
|
||||
|
||||
|
||||
async def ask_quantity() -> Elicit[Quantity]:
|
||||
"""Resolver: ask the user how many copies to put aside."""
|
||||
return Elicit("How many copies?", Quantity)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def reserve(title: str, quantity: Annotated[ElicitationResult[Quantity], Resolve(ask_quantity)]) -> str:
|
||||
"""Reserve copies of a book, asking the user how many."""
|
||||
if isinstance(quantity, AcceptedElicitation):
|
||||
return f"Reserved {quantity.data.copies} of {title!r}."
|
||||
return "Nothing reserved."
|
||||
|
||||
|
||||
app = mcp.streamable_http_app(stateless_http=True)
|
||||
@@ -0,0 +1,21 @@
|
||||
from mcp.server import MCPServer
|
||||
from mcp.server.mcpserver import Context
|
||||
|
||||
mcp = MCPServer("Bookshop")
|
||||
|
||||
STOCK = {"Dune": 3}
|
||||
|
||||
|
||||
@mcp.resource("stock://{title}")
|
||||
def stock(title: str) -> str:
|
||||
"""How many copies of one book are on the shelf."""
|
||||
return f"{STOCK[title]} in stock"
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def restock(title: str, copies: int, ctx: Context) -> str:
|
||||
"""Put copies of a book back on the shelf."""
|
||||
STOCK[title] = STOCK.get(title, 0) + copies
|
||||
await ctx.notify_resource_updated(f"stock://{title}")
|
||||
await ctx.session.send_resource_updated(f"stock://{title}")
|
||||
return f"{STOCK[title]} in stock"
|
||||
@@ -0,0 +1,41 @@
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
from dataclasses import dataclass
|
||||
|
||||
from mcp.server import MCPServer
|
||||
from mcp.server.mcpserver import Context
|
||||
|
||||
|
||||
class Database:
|
||||
@classmethod
|
||||
async def connect(cls) -> "Database":
|
||||
return cls()
|
||||
|
||||
async def disconnect(self) -> None: ...
|
||||
|
||||
def query(self) -> int:
|
||||
return 3
|
||||
|
||||
|
||||
@dataclass
|
||||
class AppContext:
|
||||
db: Database
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def app_lifespan(server: MCPServer) -> AsyncIterator[AppContext]:
|
||||
db = await Database.connect()
|
||||
try:
|
||||
yield AppContext(db=db)
|
||||
finally:
|
||||
await db.disconnect()
|
||||
|
||||
|
||||
mcp = MCPServer("Bookshop", lifespan=app_lifespan)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def count_books(genre: str, ctx: Context[AppContext]) -> str:
|
||||
"""Count the books in a genre."""
|
||||
db = ctx.request_context.lifespan_context.db
|
||||
return f"{db.query()} books in {genre!r}."
|
||||
@@ -0,0 +1,44 @@
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
from dataclasses import dataclass
|
||||
|
||||
from mcp.server import MCPServer
|
||||
from mcp.server.mcpserver import Context
|
||||
|
||||
|
||||
class Database:
|
||||
def __init__(self) -> None:
|
||||
self.connected = False
|
||||
|
||||
async def connect(self) -> None:
|
||||
self.connected = True
|
||||
|
||||
async def disconnect(self) -> None:
|
||||
self.connected = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class AppContext:
|
||||
db: Database
|
||||
|
||||
|
||||
database = Database()
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def app_lifespan(server: MCPServer) -> AsyncIterator[AppContext]:
|
||||
await database.connect()
|
||||
try:
|
||||
yield AppContext(db=database)
|
||||
finally:
|
||||
await database.disconnect()
|
||||
|
||||
|
||||
mcp = MCPServer("Bookshop", lifespan=app_lifespan)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def database_status(ctx: Context[AppContext]) -> str:
|
||||
"""Report whether the database connection is up."""
|
||||
db = ctx.request_context.lifespan_context.db
|
||||
return "connected" if db.connected else "disconnected"
|
||||
@@ -0,0 +1,14 @@
|
||||
import logging
|
||||
|
||||
from mcp.server import MCPServer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
mcp = MCPServer("Bookshop")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def search_books(query: str) -> str:
|
||||
"""Search the catalog by title or author."""
|
||||
logger.info("Searching for %r", query)
|
||||
return f"Found 3 books matching {query!r}."
|
||||
@@ -0,0 +1,33 @@
|
||||
from mcp_types import (
|
||||
CallToolRequestParams,
|
||||
CallToolResult,
|
||||
ListToolsResult,
|
||||
PaginatedRequestParams,
|
||||
TextContent,
|
||||
Tool,
|
||||
)
|
||||
|
||||
from mcp.server import Server, ServerRequestContext
|
||||
|
||||
SEARCH_BOOKS = Tool(
|
||||
name="search_books",
|
||||
description="Search the catalog by title or author.",
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {"query": {"type": "string"}, "limit": {"type": "integer"}},
|
||||
"required": ["query", "limit"],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:
|
||||
return ListToolsResult(tools=[SEARCH_BOOKS])
|
||||
|
||||
|
||||
async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:
|
||||
args = params.arguments or {}
|
||||
text = f"Found 3 books matching {args['query']!r} (showing up to {args['limit']})."
|
||||
return CallToolResult(content=[TextContent(type="text", text=text)])
|
||||
|
||||
|
||||
server = Server("Bookshop", on_list_tools=list_tools, on_call_tool=call_tool)
|
||||
@@ -0,0 +1,48 @@
|
||||
from mcp_types import (
|
||||
CallToolRequestParams,
|
||||
CallToolResult,
|
||||
ListToolsResult,
|
||||
PaginatedRequestParams,
|
||||
TextContent,
|
||||
Tool,
|
||||
)
|
||||
|
||||
from mcp.server import Server, ServerRequestContext
|
||||
|
||||
SEARCH_BOOKS = Tool(
|
||||
name="search_books",
|
||||
description="Search the catalog by title or author.",
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {"query": {"type": "string"}, "limit": {"type": "integer"}},
|
||||
"required": ["query", "limit"],
|
||||
},
|
||||
)
|
||||
|
||||
ADD_BOOK = Tool(
|
||||
name="add_book",
|
||||
description="Add a book to the catalog.",
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {"title": {"type": "string"}, "author": {"type": "string"}, "year": {"type": "integer"}},
|
||||
"required": ["title", "author", "year"],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:
|
||||
return ListToolsResult(tools=[SEARCH_BOOKS, ADD_BOOK])
|
||||
|
||||
|
||||
async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:
|
||||
args = params.arguments or {}
|
||||
if params.name == "search_books":
|
||||
text = f"Found 3 books matching {args['query']!r} (showing up to {args['limit']})."
|
||||
elif params.name == "add_book":
|
||||
text = f"Added {args['title']!r} by {args['author']} ({args['year']})."
|
||||
else:
|
||||
raise ValueError(f"Unknown tool: {params.name}")
|
||||
return CallToolResult(content=[TextContent(type="text", text=text)])
|
||||
|
||||
|
||||
server = Server("Bookshop", on_list_tools=list_tools, on_call_tool=call_tool)
|
||||
@@ -0,0 +1,41 @@
|
||||
from mcp_types import (
|
||||
CallToolRequestParams,
|
||||
CallToolResult,
|
||||
ListToolsResult,
|
||||
PaginatedRequestParams,
|
||||
TextContent,
|
||||
Tool,
|
||||
)
|
||||
|
||||
from mcp.server import Server, ServerRequestContext
|
||||
|
||||
SEARCH_BOOKS = Tool(
|
||||
name="search_books",
|
||||
description="Search the catalog by title or author.",
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {"query": {"type": "string"}, "limit": {"type": "integer"}},
|
||||
"required": ["query", "limit"],
|
||||
},
|
||||
output_schema={
|
||||
"type": "object",
|
||||
"properties": {"matches": {"type": "integer"}, "query": {"type": "string"}},
|
||||
"required": ["matches", "query"],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:
|
||||
return ListToolsResult(tools=[SEARCH_BOOKS])
|
||||
|
||||
|
||||
async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:
|
||||
args = params.arguments or {}
|
||||
data = {"matches": 3, "query": args["query"]}
|
||||
return CallToolResult(
|
||||
content=[TextContent(type="text", text=f"Found 3 books matching {args['query']!r}.")],
|
||||
structured_content=data,
|
||||
)
|
||||
|
||||
|
||||
server = Server("Bookshop", on_list_tools=list_tools, on_call_tool=call_tool)
|
||||
@@ -0,0 +1,42 @@
|
||||
from mcp_types import (
|
||||
CallToolRequestParams,
|
||||
CallToolResult,
|
||||
ListToolsResult,
|
||||
PaginatedRequestParams,
|
||||
TextContent,
|
||||
Tool,
|
||||
)
|
||||
|
||||
from mcp.server import Server, ServerRequestContext
|
||||
|
||||
SEARCH_BOOKS = Tool(
|
||||
name="search_books",
|
||||
description="Search the catalog by title or author.",
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {"query": {"type": "string"}, "limit": {"type": "integer"}},
|
||||
"required": ["query", "limit"],
|
||||
},
|
||||
output_schema={
|
||||
"type": "object",
|
||||
"properties": {"matches": {"type": "integer"}, "query": {"type": "string"}},
|
||||
"required": ["matches", "query"],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:
|
||||
return ListToolsResult(tools=[SEARCH_BOOKS])
|
||||
|
||||
|
||||
async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:
|
||||
args = params.arguments or {}
|
||||
data = {"matches": 3, "query": args["query"]}
|
||||
return CallToolResult(
|
||||
content=[TextContent(type="text", text=f"Found 3 books matching {args['query']!r}.")],
|
||||
structured_content=data,
|
||||
_meta={"bookshop/record_ids": ["bk_17", "bk_42", "bk_99"]},
|
||||
)
|
||||
|
||||
|
||||
server = Server("Bookshop", on_list_tools=list_tools, on_call_tool=call_tool)
|
||||
@@ -0,0 +1,51 @@
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
from dataclasses import dataclass
|
||||
|
||||
from mcp_types import (
|
||||
CallToolRequestParams,
|
||||
CallToolResult,
|
||||
ListToolsResult,
|
||||
PaginatedRequestParams,
|
||||
TextContent,
|
||||
Tool,
|
||||
)
|
||||
|
||||
from mcp.server import Server, ServerRequestContext
|
||||
|
||||
|
||||
@dataclass
|
||||
class Catalog:
|
||||
books: list[str]
|
||||
|
||||
def search(self, query: str) -> list[str]:
|
||||
return [title for title in self.books if query.lower() in title.lower()]
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(server: Server[Catalog]) -> AsyncIterator[Catalog]:
|
||||
yield Catalog(books=["Dune", "Dune Messiah", "Children of Dune"])
|
||||
|
||||
|
||||
SEARCH_BOOKS = Tool(
|
||||
name="search_books",
|
||||
description="Search the catalog by title or author.",
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {"query": {"type": "string"}},
|
||||
"required": ["query"],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def list_tools(ctx: ServerRequestContext[Catalog], params: PaginatedRequestParams | None) -> ListToolsResult:
|
||||
return ListToolsResult(tools=[SEARCH_BOOKS])
|
||||
|
||||
|
||||
async def call_tool(ctx: ServerRequestContext[Catalog], params: CallToolRequestParams) -> CallToolResult:
|
||||
matches = ctx.lifespan_context.search((params.arguments or {})["query"])
|
||||
text = f"Found {len(matches)} books: {', '.join(matches)}."
|
||||
return CallToolResult(content=[TextContent(type="text", text=text)])
|
||||
|
||||
|
||||
server = Server("Bookshop", lifespan=lifespan, on_list_tools=list_tools, on_call_tool=call_tool)
|
||||
@@ -0,0 +1,48 @@
|
||||
from mcp_types import (
|
||||
CallToolRequestParams,
|
||||
CallToolResult,
|
||||
ListToolsResult,
|
||||
PaginatedRequestParams,
|
||||
RequestParams,
|
||||
TextContent,
|
||||
Tool,
|
||||
)
|
||||
from pydantic import BaseModel
|
||||
|
||||
from mcp.server import Server, ServerRequestContext
|
||||
|
||||
SEARCH_BOOKS = Tool(
|
||||
name="search_books",
|
||||
description="Search the catalog by title or author.",
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {"query": {"type": "string"}, "limit": {"type": "integer"}},
|
||||
"required": ["query", "limit"],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:
|
||||
return ListToolsResult(tools=[SEARCH_BOOKS])
|
||||
|
||||
|
||||
async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:
|
||||
args = params.arguments or {}
|
||||
text = f"Found 3 books matching {args['query']!r} (showing up to {args['limit']})."
|
||||
return CallToolResult(content=[TextContent(type="text", text=text)])
|
||||
|
||||
|
||||
class ReindexParams(RequestParams):
|
||||
full: bool = False
|
||||
|
||||
|
||||
class ReindexResult(BaseModel):
|
||||
indexed: int
|
||||
|
||||
|
||||
async def reindex(ctx: ServerRequestContext, params: ReindexParams) -> ReindexResult:
|
||||
return ReindexResult(indexed=3)
|
||||
|
||||
|
||||
server = Server("Bookshop", on_list_tools=list_tools, on_call_tool=call_tool)
|
||||
server.add_request_handler("bookshop/reindex", ReindexParams, reindex)
|
||||
@@ -0,0 +1,16 @@
|
||||
import base64
|
||||
|
||||
from mcp.server import MCPServer
|
||||
from mcp.server.mcpserver import Image
|
||||
|
||||
mcp = MCPServer("Brand kit")
|
||||
|
||||
LOGO_PNG = base64.b64decode(
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGOQ9bsBAAHPAURf8l/aAAAAAElFTkSuQmCC"
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def logo() -> Image:
|
||||
"""The brand logo as a PNG."""
|
||||
return Image(data=LOGO_PNG, format="png")
|
||||
@@ -0,0 +1,24 @@
|
||||
import base64
|
||||
|
||||
from mcp.server import MCPServer
|
||||
from mcp.server.mcpserver import Audio, Image
|
||||
|
||||
mcp = MCPServer("Brand kit")
|
||||
|
||||
LOGO_PNG = base64.b64decode(
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGOQ9bsBAAHPAURf8l/aAAAAAElFTkSuQmCC"
|
||||
)
|
||||
|
||||
CHIME_WAV = base64.b64decode("UklGRjQAAABXQVZFZm10IBAAAAABAAEAQB8AAIA+AAACABAAZGF0YRAAAAAAAAAAAAAAAAAAAAAAAAAA")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def logo() -> Image:
|
||||
"""The brand logo as a PNG."""
|
||||
return Image(data=LOGO_PNG, format="png")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def chime() -> Audio:
|
||||
"""The notification chime as a WAV."""
|
||||
return Audio(data=CHIME_WAV, format="wav")
|
||||
@@ -0,0 +1,20 @@
|
||||
from mcp_types import Icon
|
||||
|
||||
from mcp.server import MCPServer
|
||||
|
||||
LOGO = Icon(src="https://example.com/brand-kit.png", mime_type="image/png", sizes=["48x48"])
|
||||
PALETTE = Icon(src="https://example.com/palette.svg", mime_type="image/svg+xml", sizes=["any"])
|
||||
|
||||
mcp = MCPServer("Brand kit", icons=[LOGO])
|
||||
|
||||
|
||||
@mcp.tool(icons=[PALETTE])
|
||||
def palette() -> list[str]:
|
||||
"""The brand colour palette as hex codes."""
|
||||
return ["#1d4ed8", "#f59e0b", "#10b981"]
|
||||
|
||||
|
||||
@mcp.resource("brand://guidelines", icons=[LOGO])
|
||||
def guidelines() -> str:
|
||||
"""How to use the brand assets."""
|
||||
return "Use the primary colour for calls to action."
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user