Add story-style examples suite (27 stories + harness + CI) (#2957)
This commit is contained in:
parent
9dc8c5f02d
commit
4caa41f6d5
4 changed files with 6801 additions and 26 deletions
53
examples/stories/custom_methods/README.md
Normal file
53
examples/stories/custom_methods/README.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# custom-methods
|
||||
|
||||
Register and call a vendor-prefixed JSON-RPC method that is not part of the
|
||||
MCP spec. The server uses the low-level `Server.add_request_handler` (there is
|
||||
no `MCPServer` surface for this, so `server.py` is lowlevel-native and there is
|
||||
no `server_lowlevel.py` sibling); the client drops to `client.session` to send
|
||||
it.
|
||||
|
||||
## Run it
|
||||
|
||||
```bash
|
||||
# stdio (default — the client spawns the server as a subprocess)
|
||||
uv run python -m stories.custom_methods.client
|
||||
|
||||
# HTTP — the client self-hosts the server on a free port, runs, then tears it down
|
||||
uv run python -m stories.custom_methods.client --http
|
||||
```
|
||||
|
||||
## What to look at
|
||||
|
||||
- `client.py` `main` — the body opens with `Client(target, mode=mode)`. The
|
||||
vendor request rides whichever protocol era `mode` selects; nothing else in
|
||||
the story changes between eras.
|
||||
- `server.py` `SearchParams` — subclasses `types.RequestParams` so `_meta`
|
||||
(and on a 2026-07-28 connection, the reserved `io.modelcontextprotocol/*`
|
||||
envelope keys) parse uniformly without extra code.
|
||||
- `server.py` `add_request_handler("acme/search", SearchParams, search)` — the
|
||||
method string is the wire `method`; use a vendor prefix so it can never
|
||||
collide with a future spec method.
|
||||
- `client.py` `client.session.send_request(...)` — `Client` only exposes spec
|
||||
verbs, so vendor methods go through the underlying `ClientSession`. The
|
||||
`cast("types.ClientRequest", ...)` is needed because `send_request`'s
|
||||
`request` parameter is currently typed as the closed spec union; widening it
|
||||
(or adding `Client.send_request`) is tracked for beta.
|
||||
|
||||
## Caveats
|
||||
|
||||
- The TypeScript SDK's equivalent example also shows a custom server→client
|
||||
**notification** (`acme/searchProgress`). The Python client currently drops
|
||||
any notification whose method is not in the spec registry
|
||||
(`ClientSession._on_notify` → `KeyError` → silent drop), and there is no
|
||||
`set_notification_handler` analogue. That half is omitted here.
|
||||
|
||||
## Spec
|
||||
|
||||
[Requests — basic protocol](https://modelcontextprotocol.io/specification/2025-11-25/basic#requests)
|
||||
(JSON-RPC request shape; vendor method names live outside the spec's reserved
|
||||
set).
|
||||
|
||||
## See also
|
||||
|
||||
`serve_one/` (the per-exchange driver that runs registered handlers),
|
||||
`middleware/` (wrapping every registered handler, including vendor methods).
|
||||
0
examples/stories/custom_methods/__init__.py
Normal file
0
examples/stories/custom_methods/__init__.py
Normal file
39
examples/stories/custom_methods/client.py
Normal file
39
examples/stories/custom_methods/client.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
"""Send a vendor-prefixed request via the `client.session` escape hatch."""
|
||||
|
||||
from typing import Literal, cast
|
||||
|
||||
import mcp_types as types
|
||||
|
||||
from mcp.client import Client
|
||||
from stories._harness import Target, run_client
|
||||
|
||||
|
||||
class SearchParams(types.RequestParams):
|
||||
query: str
|
||||
limit: int = 10
|
||||
|
||||
|
||||
class SearchRequest(types.Request[SearchParams, Literal["acme/search"]]):
|
||||
method: Literal["acme/search"] = "acme/search"
|
||||
params: SearchParams
|
||||
|
||||
|
||||
class SearchResult(types.Result):
|
||||
items: list[str]
|
||||
|
||||
|
||||
async def main(target: Target, *, mode: str = "auto") -> None:
|
||||
async with Client(target, mode=mode) as client:
|
||||
# `Client` only exposes spec-defined verbs, so vendor methods have to drop one
|
||||
# layer to `client.session` today — there is no `Client`-level API for them
|
||||
# yet, and whether `.session` stays public is undecided. `send_request` is
|
||||
# typed against the closed `ClientRequest` union, hence the cast; at runtime
|
||||
# the body only calls `.model_dump()` and the unknown method skips the
|
||||
# per-spec result-validation registry.
|
||||
request = SearchRequest(params=SearchParams(query="mcp", limit=3))
|
||||
result = await client.session.send_request(cast("types.ClientRequest", request), SearchResult)
|
||||
assert result.items == ["mcp-0", "mcp-1", "mcp-2"], result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_client(main)
|
||||
39
examples/stories/custom_methods/server.py
Normal file
39
examples/stories/custom_methods/server.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
"""Register a vendor-prefixed JSON-RPC method on the low-level Server.
|
||||
|
||||
`MCPServer` has no public surface for arbitrary method registration, so this
|
||||
story's `server.py` is lowlevel-native (no `server_lowlevel.py` sibling).
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
|
||||
class SearchParams(types.RequestParams):
|
||||
"""Subclass `RequestParams` so `_meta` (and the 2026 envelope keys) parse uniformly."""
|
||||
|
||||
query: str
|
||||
limit: int = 10
|
||||
|
||||
|
||||
class SearchResult(types.Result):
|
||||
items: list[str]
|
||||
|
||||
|
||||
def build_server() -> Server[Any]:
|
||||
server = Server("custom-methods-example")
|
||||
|
||||
async def search(ctx: ServerRequestContext[Any], params: SearchParams) -> SearchResult:
|
||||
items = [f"{params.query}-{i}" for i in range(params.limit)]
|
||||
return SearchResult(items=items)
|
||||
|
||||
server.add_request_handler("acme/search", SearchParams, search)
|
||||
return server
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_server_from_args(build_server)
|
||||
Loading…
Reference in a new issue