Source code for duck.contrib.mcp.capabilities
"""
Base capability system for MCP views. Lets related JSON-RPC methods be
grouped under one namespace and registered as a single pluggable unit
instead of being wired into `self.handlers` one by one.
"""
from typing import Any, Callable, Dict, Optional
[docs]
class Capability:
"""
Base class for a pluggable MCP capability.
A capability owns one namespace of the method space. Registering it
with `view.register_capability("tools", ToolsCapability(view))` routes
every `tools/*` method here automatically - `action` is the part of
the method after the slash (e.g. "call" for `tools/call`).
Args:
view: The MCPView this capability is attached to.
Example:
```python
class ToolsCapability(Capability):
def setup(self):
self.handlers = {
"list": self.list,
"call": self.call,
}
async def list(self, params):
return {"tools": []}
async def call(self, params):
return {"content": [], "isError": False}
view.register_capability("tools", ToolsCapability(view))
```
"""
def __init__(self, view: "MCPView"):
self.view = view
self.namespace = None
self.handlers: Dict[str, Callable] = {}
self.setup()
[docs]
def setup(self) -> None:
"""
Called once on registration. Populate `self.handlers` here.
"""
pass
[docs]
def cleanup(self) -> None:
"""
Called when the capability is removed or the view is torn down.
"""
pass
[docs]
def describe(self) -> dict:
"""
Build this namespace's entry in the 'initialize' capabilities object.
Returns:
dict: An empty object by default. Override to advertise
anything beyond bare namespace presence (e.g.
{"listChanged": True}).
"""
return {}
[docs]
async def before_request(self, action: str, params: dict) -> Optional[Any]:
"""
Optional hook run before an action's handler.
Args:
action: The part of the method after the slash (e.g. "call").
params: The method parameters.
Returns:
Any non-None value short-circuits the handler entirely and is
returned as the result instead - useful for things like
per-capability rate limiting or caching.
"""
return None
[docs]
async def after_request(self, action: str, result: Any) -> Any:
"""
Optional hook run after an action's handler.
Args:
action: The part of the method after the slash (e.g. "call").
result: The value returned by the handler.
Returns:
The (possibly modified) result.
"""
return result
[docs]
async def dispatch(self, action: str, params: dict) -> Any:
"""
Route a namespace-local action to its registered handler, running the before/after hooks around it.
Args:
action: The part of the method after the slash (e.g. "call").
params: The method parameters.
Raises:
ValueError: If no handler is registered for this action.
"""
handler = self.handlers.get(action)
if handler is None:
raise ValueError(f"Unknown action: {self.namespace}/{action}")
# Let the hook short-circuit before the real handler runs.
early_result = await self.before_request(action, params)
if early_result is not None:
return early_result
result = await handler(params)
return await self.after_request(action, result)