Files
FabledSteward/steward/mcp/tools/__init__.py
T
bvandeusenandClaude Opus 5.5 3984f639e8
CI / lint (push) Successful in 2s
CI / integration (push) Failing after 35s
CI / unit (push) Failing after 1m0s
CI / publish (push) Skipped
feat(mcp): /mcp endpoint with bearer auth and role-gated tools
Second step of milestone 461. An MCP client (Claude Code, Claude Desktop
via mcp-remote, Open WebUI) connects to /mcp with a personal API token.

- mcp>=2.2 (SDK v2 MCPServer). Mounted by wrapping app.asgi_app, so /mcp
  never enters Quart's pipeline; stateless streamable HTTP so a redeploy
  never strands a client on a session id the new process didn't issue.
- Auth: Authorization: Bearer stw_… resolves to a Caller (user +
  effective role) in a ContextVar; 401 with WWW-Authenticate otherwise.
  The web pipeline's database gate runs first, so a DB restart answers
  503 instead of failing inside a tool.
- Registry, default-deny: register_tool(fn, role=, read_only=) is the only
  door; role and read_only are required. Tools above the caller's role,
  or from a disabled plugin, are neither listed nor callable. Undeclared
  arguments are refused (not silently dropped); a tool's ValueError
  reaches the agent as its reason; a DB failure mid-tool says so.
- Plugins contribute tools via an optional register_mcp_tools(register)
  export, all-or-nothing, replaced on hot-reload. Documented in
  docs/plugins/writing-a-plugin.md.
- First tool: whoami.
- Tests drive app.asgi_app directly (Quart's test client never reaches
  /mcp); the integration test mints a real token against Postgres and
  runs initialize → tools/list → whoami → revoke → 401.

Task #4966 "MCP-2 — /mcp transport".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 07:24:00 -04:00

23 lines
797 B
Python

"""Core MCP tool modules.
Each module defines plain `async def` tools at module level (tests call them
directly) and a `register(register_tool)` that lists them with their minimum
role. `register_tool(fn, role=..., read_only=...)` takes both as required
keywords: a tool with no declared role is unreachable, and there is no default
read_only for a mutating tool to inherit by forgetting.
A tool's docstring IS its contract with the agent — it ships as the tool's
description. Say what it is for, when to reach for it over its siblings, what
each argument's empty value means, and what comes back.
"""
from __future__ import annotations
from . import session
_MODULES = (session,)
def register_all(register_tool) -> None:
for module in _MODULES:
module.register(register_tool)