Replaces the interim shared-secret bearer token's role as the identity
boundary with real key-cape JWKS-based verification, closing the gap
docs/mcp-integration.md called out explicitly ("no OIDC issuer exists
in this fleet yet") -- key-cape's /jwks is a standard RS256 endpoint
and needed no key-cape-side work to consume.
KeyCapeTokenVerifier fetches and caches signing keys over httpx
(matching FlexAuthCheckClient's pattern elsewhere in this codebase),
validates iss/aud/exp and the IAM Profile v0.3 required claims, and
derives ActorClaims from the token (tenant, scopes, and a lane
inferred from the roles claim). Wired into auth.actor_claims_from_headers,
the single seam both REST and MCP already used -- a verified bearer
token now takes precedence over self-asserted X-Actor-* headers, and
can be made mandatory via QONTO_KEY_CAPE_REQUIRED once real tokens are
issued to callers. Off by default (no QONTO_KEY_CAPE_JWKS_URL set) so
existing deployments are unaffected until configured.
The QONTO_ASSISTANT_MCP_TOKEN shared secret remains as a documented
local-dev/legacy fallback, not the auth boundary going forward.
Verified: 13 new tests (test_key_cape_auth.py) covering valid/expired/
wrong-audience/wrong-issuer/missing-claim/unknown-key/rotated-key
tokens plus the auth.py precedence and required-vs-optional
integration paths, using a real generated RSA keypair and JWKS served
over httpx.MockTransport. Full suite -> 52 passed; REST and MCP smokes
both still pass against fixtures; compileall clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add scripts/smoke_mcp.py, same shape as smoke_rest_api.py (random port,
subprocess-launch against QONTO_FIXTURE_DIR, wait on /v1/health, assert,
clean teardown), but goes further: generates a fresh
QONTO_ASSISTANT_MCP_TOKEN per run and connects through it with the mcp
SDK's streamablehttp_client, so the smoke exercises T03's bearer-token gate
instead of bypassing it. Lists tools, calls all four, and confirms an
out-of-catalog tool name comes back as a normal isError result through the
real wire protocol rather than a crash.
Extend docs/operator-runbook.md with a "One-command MCP smoke" section next
to the REST one; cross-link from docs/mcp-integration.md's manual smoke
walkthrough so the two don't drift.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
qonto-assistant's side only, per task scope: the contract a profile named
finance-qonto-read must satisfy in agent-harness (~/agent-harness, ADR-001,
ToolProfile) to reach this MCP surface -- lane (green/blue, never red), the
exact mcp__qonto-assistant__* allowed-tools list, and the optional
finance.qonto.read scope.
Checked agent-harness/agent_harness/profiles.py directly: only CLI-only
profiles exist today (green-commit-only, blue-mail-triage), none grant an
MCP server yet. Included a ready-to-paste ToolProfile entry matching its
actual schema, marked as a proposal for agent-harness's own PR -- not
applied here, since registering it is a separate repo/workplan. Added the
end-to-end example: instance manifest -> harness-injected MCP server config
-> the four allowed tool calls.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Gate /mcp with a shared-secret bearer token (QONTO_ASSISTANT_MCP_TOKEN,
mcp_auth.py::BearerTokenAuthMiddleware, constant-time compare, REST
untouched) since no OIDC issuer exists in this fleet yet -- pointing
FastMCP's OAuth Protected Resource flow at a non-existent issuer would be
worse than not having it. This token is a service credential, never a bank
credential; per-actor identity stays the existing X-Actor-* convention.
Add docs/mcp-integration.md: tool catalog, the two-layer auth model (workload
auth today vs. deferred OIDC target), and one shared {"mcpServers": {...}}
client config snippet (url + headers) usable across Claude Code, Claude
Desktop, Cursor, and Codex/Grok-style harnesses.
Verified live using only that snippet: unauthenticated and wrong-token
requests get 401 before reaching any tool; a request built from the
snippet's URL + headers lists tools and calls qonto_org_summary
successfully against the fixture-backed server.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>