Files
modelcontextprotocol--pytho…/examples/stories/stickynotes/server_lowlevel.py
T
wehub-resource-sync 49b9bb6724
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
chore: import upstream snapshot with attribution
2026-07-13 12:10:27 +08:00

120 lines
4.5 KiB
Python

"""Capstone sticky-notes board on the lowlevel `Server`: handlers read lifespan state directly."""
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from dataclasses import dataclass, field
from typing import Any
import mcp_types as types
from mcp.server.context import ServerRequestContext
from mcp.server.lowlevel import Server
from stories._hosting import run_server_from_args
@dataclass
class Board:
notes: dict[str, str] = field(default_factory=dict[str, str])
_next: int = 1
def claim_id(self) -> str:
nid, self._next = str(self._next), self._next + 1
return nid
CONFIRM_SCHEMA: dict[str, Any] = {
"type": "object",
"properties": {"confirm": {"type": "boolean", "title": "Yes, permanently delete every sticky note"}},
"required": ["confirm"],
}
TOOLS = [
types.Tool(
name="add_note",
description="Add a sticky note.",
input_schema={"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"]},
),
types.Tool(
name="remove_note",
description="Remove one sticky note.",
input_schema={"type": "object", "properties": {"note_id": {"type": "string"}}, "required": ["note_id"]},
),
types.Tool(name="remove_all", description="Remove every note after confirmation.", input_schema={"type": "object"}),
]
def _result(text: str, structured: dict[str, Any]) -> types.CallToolResult:
return types.CallToolResult(content=[types.TextContent(text=text)], structured_content=structured)
def build_server() -> Server[Board]:
@asynccontextmanager
async def lifespan(_: Server[Board]) -> AsyncIterator[Board]:
yield Board()
async def list_tools(
ctx: ServerRequestContext[Board], params: types.PaginatedRequestParams | None
) -> types.ListToolsResult:
return types.ListToolsResult(tools=TOOLS)
async def list_resources(
ctx: ServerRequestContext[Board], params: types.PaginatedRequestParams | None
) -> types.ListResourcesResult:
board = ctx.lifespan_context
return types.ListResourcesResult(
resources=[
types.Resource(uri=f"note:///{nid}", name=f"note-{nid}", mime_type="text/plain") for nid in board.notes
]
)
async def read_resource(
ctx: ServerRequestContext[Board], params: types.ReadResourceRequestParams
) -> types.ReadResourceResult:
board = ctx.lifespan_context
nid = str(params.uri).removeprefix("note:///")
return types.ReadResourceResult(
contents=[types.TextResourceContents(uri=params.uri, mime_type="text/plain", text=board.notes[nid])]
)
async def call_tool(ctx: ServerRequestContext[Board], params: types.CallToolRequestParams) -> types.CallToolResult:
board = ctx.lifespan_context
args = params.arguments or {}
if params.name == "add_note":
nid = board.claim_id()
board.notes[nid] = args["text"]
await ctx.session.send_resource_list_changed()
return _result(f"added #{nid}", {"id": nid, "uri": f"note:///{nid}"})
if params.name == "remove_note":
removed = board.notes.pop(args["note_id"], None) is not None
if removed:
await ctx.session.send_resource_list_changed()
return _result("removed" if removed else "not found", {"result": removed})
if params.name == "remove_all":
if not board.notes:
return _result("empty", {"status": "empty", "removed": 0})
answer = await ctx.session.elicit_form(
f"Remove all {len(board.notes)} note(s)? This cannot be undone.", CONFIRM_SCHEMA, ctx.request_id
)
if answer.action == "cancel":
return _result("cancelled", {"status": "cancelled", "removed": 0})
if answer.action != "accept" or not (answer.content or {}).get("confirm"):
return _result("declined", {"status": "declined", "removed": 0})
count = len(board.notes)
board.notes.clear()
await ctx.session.send_resource_list_changed()
return _result(f"cleared {count}", {"status": "cleared", "removed": count})
raise NotImplementedError
return Server(
"stickynotes-example",
lifespan=lifespan,
on_list_tools=list_tools,
on_call_tool=call_tool,
on_list_resources=list_resources,
on_read_resource=read_resource,
)
if __name__ == "__main__":
run_server_from_args(build_server)