feat: add paginated list decorators for prompts, resources, and tools (#1286)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Max Isbey 2025-09-23 14:58:06 +01:00 • committed by GitHub
commit 4fb975c6dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 1274 additions and 19 deletions

View file

@ -0,0 +1,54 @@
import inspect
from collections.abc import Callable
from typing import Any, TypeVar, get_type_hints
T = TypeVar("T")
R = TypeVar("R")
def create_call_wrapper(func: Callable[..., R], request_type: type[T]) -> Callable[[T], R]:
"""
Create a wrapper function that knows how to call func with the request object.
Returns a wrapper function that takes the request and calls func appropriately.
The wrapper handles three calling patterns:
1. Positional-only parameter typed as request_type (no default): func(req)
2. Positional/keyword parameter typed as request_type (no default): func(**{param_name: req})
3. No request parameter or parameter with default: func()
"""
try:
sig = inspect.signature(func)
type_hints = get_type_hints(func)
except (ValueError, TypeError, NameError):
return lambda _: func()
# Check for positional-only parameter typed as request_type
for param_name, param in sig.parameters.items():
if param.kind == inspect.Parameter.POSITIONAL_ONLY:
param_type = type_hints.get(param_name)
if param_type == request_type:
# Check if it has a default - if so, treat as old style
if param.default is not inspect.Parameter.empty:
return lambda _: func()
# Found positional-only parameter with correct type and no default
return lambda req: func(req)
# Check for any positional/keyword parameter typed as request_type
for param_name, param in sig.parameters.items():
if param.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY):
param_type = type_hints.get(param_name)
if param_type == request_type:
# Check if it has a default - if so, treat as old style
if param.default is not inspect.Parameter.empty:
return lambda _: func()
# Found keyword parameter with correct type and no default
# Need to capture param_name in closure properly
def make_keyword_wrapper(name: str) -> Callable[[Any], Any]:
return lambda req: func(**{name: req})
return make_keyword_wrapper(param_name)
# No request parameter found - use old style
return lambda _: func()

View file

@ -11,7 +11,7 @@ Usage:
2. Define request handlers using decorators:
@server.list_prompts()
async def handle_list_prompts() -> list[types.Prompt]:
async def handle_list_prompts(request: types.ListPromptsRequest) -> types.ListPromptsResult:
# Implementation
@server.get_prompt()
@ -21,7 +21,7 @@ Usage:
# Implementation
@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
async def handle_list_tools(request: types.ListToolsRequest) -> types.ListToolsResult:
# Implementation
@server.call_tool()
@ -82,6 +82,7 @@ from pydantic import AnyUrl
from typing_extensions import TypeVar
import mcp.types as types
from mcp.server.lowlevel.func_inspection import create_call_wrapper
from mcp.server.lowlevel.helper_types import ReadResourceContents
from mcp.server.models import InitializationOptions
from mcp.server.session import ServerSession
@ -229,12 +230,22 @@ class Server(Generic[LifespanResultT, RequestT]):
return request_ctx.get()
def list_prompts(self):
def decorator(func: Callable[[], Awaitable[list[types.Prompt]]]):
def decorator(
func: Callable[[], Awaitable[list[types.Prompt]]]
| Callable[[types.ListPromptsRequest], Awaitable[types.ListPromptsResult]],
):
logger.debug("Registering handler for PromptListRequest")
async def handler(_: Any):
prompts = await func()
return types.ServerResult(types.ListPromptsResult(prompts=prompts))
wrapper = create_call_wrapper(func, types.ListPromptsRequest)
async def handler(req: types.ListPromptsRequest):
result = await wrapper(req)
# Handle both old style (list[Prompt]) and new style (ListPromptsResult)
if isinstance(result, types.ListPromptsResult):
return types.ServerResult(result)
else:
# Old style returns list[Prompt]
return types.ServerResult(types.ListPromptsResult(prompts=result))
self.request_handlers[types.ListPromptsRequest] = handler
return func
@ -257,12 +268,22 @@ class Server(Generic[LifespanResultT, RequestT]):
return decorator
def list_resources(self):
def decorator(func: Callable[[], Awaitable[list[types.Resource]]]):
def decorator(
func: Callable[[], Awaitable[list[types.Resource]]]
| Callable[[types.ListResourcesRequest], Awaitable[types.ListResourcesResult]],
):
logger.debug("Registering handler for ListResourcesRequest")
async def handler(_: Any):
resources = await func()
return types.ServerResult(types.ListResourcesResult(resources=resources))
wrapper = create_call_wrapper(func, types.ListResourcesRequest)
async def handler(req: types.ListResourcesRequest):
result = await wrapper(req)
# Handle both old style (list[Resource]) and new style (ListResourcesResult)
if isinstance(result, types.ListResourcesResult):
return types.ServerResult(result)
else:
# Old style returns list[Resource]
return types.ServerResult(types.ListResourcesResult(resources=result))
self.request_handlers[types.ListResourcesRequest] = handler
return func
@ -380,16 +401,30 @@ class Server(Generic[LifespanResultT, RequestT]):
return decorator
def list_tools(self):
def decorator(func: Callable[[], Awaitable[list[types.Tool]]]):
def decorator(
func: Callable[[], Awaitable[list[types.Tool]]]
| Callable[[types.ListToolsRequest], Awaitable[types.ListToolsResult]],
):
logger.debug("Registering handler for ListToolsRequest")
async def handler(_: Any):
tools = await func()
# Refresh the tool cache
self._tool_cache.clear()
for tool in tools:
self._tool_cache[tool.name] = tool
return types.ServerResult(types.ListToolsResult(tools=tools))
wrapper = create_call_wrapper(func, types.ListToolsRequest)
async def handler(req: types.ListToolsRequest):
result = await wrapper(req)
# Handle both old style (list[Tool]) and new style (ListToolsResult)
if isinstance(result, types.ListToolsResult):
# Refresh the tool cache with returned tools
for tool in result.tools:
self._tool_cache[tool.name] = tool
return types.ServerResult(result)
else:
# Old style returns list[Tool]
# Clear and refresh the entire tool cache
self._tool_cache.clear()
for tool in result:
self._tool_cache[tool.name] = tool
return types.ServerResult(types.ListToolsResult(tools=result))
self.request_handlers[types.ListToolsRequest] = handler
return func