# MCP Integration (Phase 2) `qonto-assistant` exposes a streamable-HTTP MCP adapter at `/mcp`, mounted on the same FastAPI process as the REST surface. It shares the exact policy kernel and audit layer REST uses — see `specs/ArchitectureBlueprint.md` §6. Tools exposed (read-only, default-deny policy underneath): | MCP tool | Capability | Notes | | --- | --- | --- | | `qonto_ping` | none (no bank call) | Smoke/liveness check | | `qonto_org_summary` | `org_summary` | Organization + per-account balances | | `qonto_list_transactions` | `list_transactions` | Capped, filtered history | | `qonto_cost_run_rate_hints` | `cost_run_rate_hints` | Normalized recurring-cost hints, not a raw export | No spend, transfer, card, or volume-cost tool is ever exposed here — those stay hard-denied by the shared policy kernel regardless of transport. ## Auth model Two independent layers, same as the blueprint's identity separation (`specs/ArchitectureBlueprint.md` §4.7): 1. **Workload auth (is this caller allowed to reach the service at all?)** Today: a static shared-secret bearer token (`QONTO_ASSISTANT_MCP_TOKEN`), checked by `BearerTokenAuthMiddleware` in front of `/mcp`. If the env var is unset, the middleware is not installed — used only for local fixture-backed smoke work, never for a deployment holding real credentials. This is **not** the OIDC/workload-identity target the blueprint describes — there is no OIDC issuer in this fleet yet to federate against. A shared-secret bearer token is the deployable "or similar" primitive for now; upgrading to real OIDC/workload identity is fleet-level follow-on work (tracked as a Phase 2 gap, not closed by this task). This token is a **service credential**, not a bank credential — it never reaches Qonto and is never logged. Treat it like any other shared secret: store it in OpenBao or your harness's secret manager, not in plaintext config committed to a repo. 2. **Actor identity (who is calling, for policy and audit purposes?)** Same `X-Actor-ID` / `X-Tenant-ID` / `X-Actor-Lane` / `X-Actor-Scopes` header convention REST already uses (`auth.actor_claims_from_headers`). These are self-asserted today, not cryptographically bound to the bearer token — tightening that binding is exactly what Phase 3's flex-auth resource (`finance.qonto.read`) is for. ## One shared client config snippet The same `url` + `headers` shape works across MCP-capable harnesses that support a remote streamable-HTTP server (Claude Code, Claude Desktop, Cursor, and Codex/Grok-style harnesses that follow the MCP client config convention): ```json { "mcpServers": { "qonto-assistant": { "url": "http://127.0.0.1:8080/mcp", "headers": { "Authorization": "Bearer ${QONTO_ASSISTANT_MCP_TOKEN}", "X-Actor-ID": "${AGENT_ACTOR_ID}", "X-Tenant-ID": "binky" } } } } ``` Replace the `url` host/port with wherever the service is actually deployed. `${QONTO_ASSISTANT_MCP_TOKEN}` and `${AGENT_ACTOR_ID}` are placeholders for whatever secret-interpolation syntax your harness config supports — never paste the literal token into a shared config file. ## Local smoke: start with auth enabled, connect with only this snippet ```bash export QONTO_FIXTURE_DIR=tests/fixtures/qonto export QONTO_ASSISTANT_MCP_TOKEN=dev-local-token make run # or: PYTHONPATH=src ../state-hub/.venv/bin/python -m qonto_assistant.main ``` Then, using only the URL and `Authorization: Bearer dev-local-token` header from the snippet above, connect with the `mcp` Python SDK's client (or any MCP Inspector-style tool): ```python import asyncio from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client async def main(): async with streamablehttp_client( "http://127.0.0.1:8080/mcp", headers={ "Authorization": "Bearer dev-local-token", "X-Actor-ID": "local-operator", "X-Tenant-ID": "binky", }, ) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() print([tool.name for tool in (await session.list_tools()).tools]) print((await session.call_tool("qonto_org_summary", {})).structuredContent) asyncio.run(main()) ``` A request without the `Authorization` header (or with the wrong token) gets a `401 {"error_code": "unauthorized"}` before it ever reaches a tool. ## Related - `docs/operator-runbook.md` — REST operator runbook (credential sources, fixture mode, `/v1/*` endpoints) - `specs/ArchitectureBlueprint.md` §6 Phase 2 — the plan this doc implements - `workplans/QONTO-WP-0003-mcp-surface.md` — Phase 2 workplan and task status