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>
Foundation for the Ansible automation milestone (#37) — makes the existing
manual playbook runner actually executable and the schema automation-ready.
- pyproject: [ansible] extra (full ansible package, batteries-included, pinned)
- Dockerfile: pip install .[ansible]; add openssh-client for remote runs
- models/ansible.py + migration 0012: AnsibleRun.triggered_by now nullable so
automated (alert/schedule) runs need no human actor
- ansible/routes.py + run_detail.html: show 'Triggered by' (username or 'system')
- CI: integration lane installs .[dev,ansible]; new tests/integration/
test_ansible_foundation.py runs a real connection:local playbook end-to-end,
asserts success+output, and round-trips a NULL-triggered run
No extra-vars/limit/credentials/scheduling here — those are their own #37 tasks.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Single-repo CI now that plugins are bundled in-tree. Three lanes on push to
dev/main, modeled on FabledCurator's canonical ci.yml (minus frontend/Redis):
- lint: ruff check steward/ plugins/ tests/ (no dep install)
- unit: pytest -m 'not integration' — whole current suite (testing=True mocks DB)
- integration: postgres:16-alpine service via socket-discovered bridge IP; a
boot-and-migrate test creates the real app, running core + all bundled-plugin
migrations, then round-trips the DB — guards the folded-in migration graph.
Registers the 'integration' pytest marker; adds tests/integration/test_boot.py.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Renames the Python package directory, CLI command, env var prefix,
docker-compose service/container/image, Postgres role/db, and all
visible branding. Marketing form is "Fabled Steward".
Clean break from the previous rebrand: drops the fabledscryer→roundtable
import shim in __init__.py and the FABLEDSCRYER_* env var fallback in
config.py and migrations/env.py. Env vars are now STEWARD_* only.
Heads-up for existing deployments:
- Postgres user/db renamed fabledscryer → steward in docker-compose.yml.
Existing volumes need the role/db renamed inside Postgres, or override
POSTGRES_USER/POSTGRES_DB to keep the old names.
- Host-agent systemd unit is now steward-agent.service. Existing agents
keep running under the old name; reinstall to switch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>