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) Has been cancelled
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) Has been cancelled
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user