2026-07-22 21:36:38 +02:00
|
|
|
|
---
|
|
|
|
|
|
id: QONTO-WP-0003
|
|
|
|
|
|
type: workplan
|
|
|
|
|
|
title: "Phase 2 — MCP surface for all harnesses"
|
|
|
|
|
|
domain: infotech
|
|
|
|
|
|
repo: qonto-assistant
|
2026-07-22 21:41:24 +02:00
|
|
|
|
status: active
|
2026-07-22 21:36:38 +02:00
|
|
|
|
owner: codex
|
|
|
|
|
|
topic_slug: the-custodian
|
|
|
|
|
|
created: "2026-07-22"
|
|
|
|
|
|
updated: "2026-07-22"
|
2026-07-22 21:37:10 +02:00
|
|
|
|
state_hub_workstream_id: "6ad0e906-5fbe-4b9e-8307-0abfc5c4a60a"
|
2026-07-22 21:36:38 +02:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
# Phase 2 — MCP surface for all harnesses
|
|
|
|
|
|
|
|
|
|
|
|
Execute **Phase 2** of `specs/ArchitectureBlueprint.md` §6: add a
|
|
|
|
|
|
streamable-HTTP MCP adapter on top of the *same* protocol-neutral capability
|
|
|
|
|
|
core and `decide(request, claims) -> Allow|Deny` policy kernel that Phase 1's
|
|
|
|
|
|
REST surface already uses. No new capabilities and no policy fork — MCP is a
|
|
|
|
|
|
second transport onto the existing allow-listed reads
|
|
|
|
|
|
(`org_summary`, `list_transactions`, `cost_run_rate_hints`, optionally
|
|
|
|
|
|
`snapshot_bundle`). Spend/volume-cost/credential-exfil stay hard-denied.
|
|
|
|
|
|
|
|
|
|
|
|
**Depends on:** QONTO-WP-0002 (done) for the capability core, policy kernel,
|
|
|
|
|
|
Qonto client, and audit layer this phase reuses. **Non-goals:** flex-auth
|
|
|
|
|
|
resource scopes and multi-tenant path layout (Phase 3/4); vendor
|
|
|
|
|
|
`qonto-mcp-server` as an internal backend (blueprint §4.3/§5.1 — explicitly
|
|
|
|
|
|
rejected).
|
|
|
|
|
|
|
|
|
|
|
|
## Task: MCP adapter skeleton on the shared capability core
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: QONTO-WP-0003-T01
|
2026-07-22 21:41:24 +02:00
|
|
|
|
status: done
|
2026-07-22 21:36:38 +02:00
|
|
|
|
priority: high
|
2026-07-22 21:37:10 +02:00
|
|
|
|
state_hub_task_id: "b899ea70-13d4-4b70-8b45-b602359ab90b"
|
2026-07-22 21:36:38 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Pick an MCP server library/transport for the Python 3.12 stack (streamable-HTTP,
|
|
|
|
|
|
per blueprint §6 Phase 2.1 — not stdio-only). Scaffold the adapter under
|
|
|
|
|
|
`src/qonto_assistant/` alongside the existing REST adapter, routing every tool
|
|
|
|
|
|
call through the same `CapabilityRequest` → `decide()` path used by REST
|
|
|
|
|
|
(`protocol: mcp` on the request). Do not duplicate policy logic in the MCP
|
|
|
|
|
|
layer.
|
|
|
|
|
|
|
|
|
|
|
|
Done when: adapter starts, exposes an empty/smoke tool list, and a manual
|
|
|
|
|
|
`decide()` call for a known-deny case is proven to hit the identical policy
|
|
|
|
|
|
path as the REST equivalent (e.g. shared unit test parametrized over both
|
|
|
|
|
|
protocols).
|
|
|
|
|
|
|
2026-07-22 21:41:24 +02:00
|
|
|
|
**Done 2026-07-22:** Picked the official `mcp` Python SDK (`FastMCP`,
|
|
|
|
|
|
streamable-HTTP transport, `stateless_http=True`) — added as a dependency in
|
|
|
|
|
|
`pyproject.toml`. `src/qonto_assistant/mcp_server.py` builds the adapter with
|
|
|
|
|
|
one smoke tool (`qonto_ping`, no bank call). `app.py` mounts it at `/mcp` with
|
|
|
|
|
|
a combined FastAPI lifespan (`AsyncExitStack` entering the MCP session
|
|
|
|
|
|
manager's lifespan) so the adapter starts/stops with the service — verified
|
|
|
|
|
|
live (`StreamableHTTP session manager started/shutting down` in logs,
|
|
|
|
|
|
`/v1/health` still 200, `/mcp` reachable). `tests/test_policy.py` adds
|
|
|
|
|
|
`test_policy_decision_identical_across_protocols`, parametrized over
|
|
|
|
|
|
`protocol="rest"`/`"mcp"`, proving `PolicyEngine.decide()` is transport-
|
|
|
|
|
|
agnostic (the `protocol` field is carried on `CapabilityRequest` but never
|
|
|
|
|
|
branches policy logic). `tests/test_mcp_server.py` proves the adapter starts
|
|
|
|
|
|
and lists/calls the smoke tool. `pytest-asyncio` added as a dev dependency
|
|
|
|
|
|
(`asyncio_mode = "auto"`). Verified: `pytest` → `20 passed`;
|
|
|
|
|
|
`python3 -m compileall src tests scripts`. No capability tools yet — that's
|
|
|
|
|
|
QONTO-WP-0003-T02.
|
|
|
|
|
|
|
2026-07-22 21:36:38 +02:00
|
|
|
|
## Task: MCP tool catalog mapped to allowed capabilities
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: QONTO-WP-0003-T02
|
2026-07-22 21:48:09 +02:00
|
|
|
|
status: done
|
2026-07-22 21:36:38 +02:00
|
|
|
|
priority: high
|
2026-07-22 21:37:10 +02:00
|
|
|
|
state_hub_task_id: "f9e0d403-d6ae-4800-8d42-847c3d70827f"
|
2026-07-22 21:36:38 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Implement MCP tools per the capability map in `specs/ArchitectureBlueprint.md`
|
|
|
|
|
|
§5:
|
|
|
|
|
|
|
|
|
|
|
|
| Internal capability id | MCP tool name |
|
|
|
|
|
|
| --- | --- |
|
|
|
|
|
|
| `org_summary` | `qonto_org_summary` |
|
|
|
|
|
|
| `list_transactions` | `qonto_list_transactions` |
|
|
|
|
|
|
| `cost_run_rate_hints` | `qonto_cost_run_rate_hints` |
|
|
|
|
|
|
| `snapshot_bundle` | optional — only if it stays a pure orchestrator over already-allowed reads, not a bigger export |
|
|
|
|
|
|
|
|
|
|
|
|
Tool names may differ from REST paths but must resolve to the same internal
|
|
|
|
|
|
capability id and go through the same deny-reason vocabulary (authz, tenant
|
|
|
|
|
|
scope, argument constraint, credential exfil). No `find_counterparties` or any
|
|
|
|
|
|
write/spend-shaped tool — those stay hard-denied per Phase 1 policy.
|
|
|
|
|
|
|
|
|
|
|
|
Done when: unit tests cover allow paths for each tool and deny paths for
|
|
|
|
|
|
out-of-catalog / spend-shaped tool names, mirroring the REST policy tests from
|
|
|
|
|
|
QONTO-WP-0002-T02.
|
|
|
|
|
|
|
2026-07-22 21:48:09 +02:00
|
|
|
|
**Done 2026-07-22:** Implemented `qonto_org_summary`, `qonto_list_transactions`,
|
|
|
|
|
|
and `qonto_cost_run_rate_hints` in `mcp_server.py`, all routed through
|
|
|
|
|
|
`CapabilityService` with `protocol="mcp"` (skipped `snapshot_bundle` — REST
|
|
|
|
|
|
already covers the composite read and it isn't a separate privilege).
|
|
|
|
|
|
`CapabilityService` now threads `protocol: ProtocolName` through `_execute`
|
|
|
|
|
|
and `_emit_audit` instead of hardcoding `"rest"`; added
|
|
|
|
|
|
`get_cost_run_rate_hints()` + `_build_cost_run_rate_hints_payload()` since
|
|
|
|
|
|
`cost_run_rate_hints` is its own policy capability, not only a snapshot
|
|
|
|
|
|
sub-field. Actor identity uses the same `X-Actor-*` header convention as
|
|
|
|
|
|
REST — `auth.py` now exposes a shared `actor_claims_from_headers()`, read
|
|
|
|
|
|
from the MCP `Context`'s underlying Starlette request when present, falling
|
|
|
|
|
|
back to defaults for stdio/no-request-context callers. Fixed a routing bug:
|
|
|
|
|
|
`FastMCP`'s default `streamable_http_path="/mcp"` plus mounting at `/mcp`
|
|
|
|
|
|
doubled to `/mcp/mcp` — set `streamable_http_path="/"` on the sub-app instead.
|
|
|
|
|
|
|
|
|
|
|
|
Verified: `tests/test_mcp_server.py` covers tool listing, an allow path
|
|
|
|
|
|
(`qonto_org_summary`, asserts redacted output + `protocol: "mcp"` audit
|
|
|
|
|
|
event), a deny path (`qonto_list_transactions` oversized `page_size` →
|
|
|
|
|
|
`ToolError`, `deny_reason: "arg_constraint"`), and `qonto_cost_run_rate_hints`.
|
|
|
|
|
|
Also ran a real end-to-end check with the `mcp` SDK's `streamablehttp_client`
|
|
|
|
|
|
against the live server (fixture-backed): tool list, `qonto_org_summary` call,
|
|
|
|
|
|
and confirmed `X-Actor-ID` header flows through to the audit log exactly like
|
|
|
|
|
|
REST. `pytest` → `25 passed`; `python3 -m compileall src tests scripts`.
|
|
|
|
|
|
|
2026-07-22 21:36:38 +02:00
|
|
|
|
## Task: Client auth and one shared config snippet
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: QONTO-WP-0003-T03
|
2026-07-23 09:41:25 +02:00
|
|
|
|
status: done
|
2026-07-22 21:36:38 +02:00
|
|
|
|
priority: high
|
2026-07-22 21:37:10 +02:00
|
|
|
|
state_hub_task_id: "ad43b09a-ffa5-4df0-8999-bfcef1f11732"
|
2026-07-22 21:36:38 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Wire OIDC/workload auth for the MCP endpoint — **no bank secrets ever reach
|
|
|
|
|
|
the client**, matching the REST surface's custody model (blueprint §4.1,
|
|
|
|
|
|
§7). Produce a single documented client config snippet usable across
|
|
|
|
|
|
Claude Code, Codex, Cursor, and Grok-style harnesses: URL + auth only, no
|
|
|
|
|
|
per-harness variants.
|
|
|
|
|
|
|
|
|
|
|
|
Done when: the snippet is in `docs/operator-runbook.md` (or a new
|
|
|
|
|
|
`docs/mcp-integration.md`), and a local smoke connects a real MCP client
|
|
|
|
|
|
against the running adapter using only that snippet.
|
|
|
|
|
|
|
2026-07-23 09:41:25 +02:00
|
|
|
|
**Done 2026-07-23 — with a scoped gap, called out explicitly:** No OIDC
|
|
|
|
|
|
issuer exists anywhere in this fleet yet, so standing up real
|
|
|
|
|
|
OIDC/workload-identity federation isn't something this repo can do alone —
|
|
|
|
|
|
implementing it would mean pointing `FastMCP`'s OAuth Protected Resource
|
|
|
|
|
|
flow at an issuer URL that doesn't serve real metadata, which is worse than
|
|
|
|
|
|
not having it. Shipped the deployable primitive instead: a shared-secret
|
|
|
|
|
|
bearer token (`QONTO_ASSISTANT_MCP_TOKEN`), enforced by
|
|
|
|
|
|
`src/qonto_assistant/mcp_auth.py::BearerTokenAuthMiddleware` in front of
|
|
|
|
|
|
`/mcp` only (REST untouched). Unset by default (fixture/local smoke stays
|
|
|
|
|
|
credential-free); when set, every non-matching or missing
|
|
|
|
|
|
`Authorization: Bearer <token>` gets `401 {"error_code": "unauthorized"}`
|
|
|
|
|
|
before reaching any tool or the policy kernel. Constant-time comparison
|
|
|
|
|
|
(`hmac.compare_digest`). This token is a **service credential, not a bank
|
|
|
|
|
|
credential** — never logged, never reaches Qonto. Per-actor identity is
|
|
|
|
|
|
still the self-asserted `X-Actor-*` header convention (not yet
|
|
|
|
|
|
cryptographically bound to the bearer token — that binding is Phase 3's
|
|
|
|
|
|
flex-auth resource `finance.qonto.read`, not this task).
|
|
|
|
|
|
|
|
|
|
|
|
Added `docs/mcp-integration.md`: tool table, the two-layer auth model
|
|
|
|
|
|
explained (workload auth today vs. the OIDC target and why it's deferred),
|
|
|
|
|
|
and the one shared `{"mcpServers": {...}}` config snippet (`url` + `headers`
|
|
|
|
|
|
— the convention shared by Claude Code, Claude Desktop, Cursor, and
|
|
|
|
|
|
Codex/Grok-style harnesses for remote streamable-HTTP MCP servers).
|
|
|
|
|
|
|
|
|
|
|
|
Verified live, exactly as the doc's smoke section describes: started the
|
|
|
|
|
|
service with `QONTO_ASSISTANT_MCP_TOKEN` set and `QONTO_FIXTURE_DIR` (no
|
|
|
|
|
|
real Qonto credentials); a request with no `Authorization` header never
|
|
|
|
|
|
reached a tool; a request with the wrong token got `401 Unauthorized` in the
|
|
|
|
|
|
server log; a request built from *only* the doc's snippet (URL + the two
|
|
|
|
|
|
headers) listed tools and called `qonto_org_summary` successfully, with
|
|
|
|
|
|
`X-Actor-ID` flowing into the audit event as before. New
|
|
|
|
|
|
`tests/test_mcp_auth.py` covers missing/wrong/matching token cases directly.
|
|
|
|
|
|
`pytest` → `28 passed`; `python3 -m compileall src tests scripts`.
|
|
|
|
|
|
|
2026-07-22 21:36:38 +02:00
|
|
|
|
## Task: agent-harness tool profile `finance-qonto-read`
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: QONTO-WP-0003-T04
|
QONTO-WP-0003-T04: document finance-qonto-read tool profile contract
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>
2026-07-23 09:48:36 +02:00
|
|
|
|
status: done
|
2026-07-22 21:36:38 +02:00
|
|
|
|
priority: medium
|
2026-07-22 21:37:10 +02:00
|
|
|
|
state_hub_task_id: "c6805812-8419-4200-b4ad-a048a541b3be"
|
2026-07-22 21:36:38 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Register the `finance-qonto-read` tool profile (blueprint §6 Phase 2.3) so
|
|
|
|
|
|
harness-side configuration grants this MCP surface only to the intended
|
|
|
|
|
|
assistant context, not blanket per-harness key sprawl. Coordinate with
|
|
|
|
|
|
whichever agent-harness config surface is authoritative for profile
|
|
|
|
|
|
definitions (outside this repo) — this task covers qonto-assistant's side:
|
|
|
|
|
|
documenting the profile contract (tool names, required scopes) it expects
|
|
|
|
|
|
callers to present.
|
|
|
|
|
|
|
|
|
|
|
|
Done when: profile contract is documented and referenced from the MCP
|
|
|
|
|
|
integration doc; at least one harness config example uses it end-to-end.
|
|
|
|
|
|
|
QONTO-WP-0003-T04: document finance-qonto-read tool profile contract
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>
2026-07-23 09:48:36 +02:00
|
|
|
|
**Done 2026-07-23:** Checked `~/agent-harness` (ADR-001,
|
|
|
|
|
|
`agent_harness/profiles.py`) directly rather than guessing the shape —
|
|
|
|
|
|
`ToolProfile` is real: instances declare a profile by name, the harness
|
|
|
|
|
|
resolves and enforces a hard `--allowedTools` allow-list, instances never
|
|
|
|
|
|
enumerate tools. Today only `green-commit-only` and `blue-mail-triage` are
|
|
|
|
|
|
registered, both CLI-only; no profile currently grants any MCP server.
|
|
|
|
|
|
|
|
|
|
|
|
Added a **`finance-qonto-read`** section to `docs/mcp-integration.md` with
|
|
|
|
|
|
the qonto-assistant-side contract: lane (`green`/`blue` only, never `red`),
|
|
|
|
|
|
the exact `mcp__qonto-assistant__*` allowed-tools list (all four tools,
|
|
|
|
|
|
nothing more), and the optional `finance.qonto.read` required scope. Included
|
|
|
|
|
|
a ready-to-paste proposed `ToolProfile` entry matching agent-harness's actual
|
|
|
|
|
|
`profiles.py` schema, explicitly marked as a proposal for agent-harness's own
|
|
|
|
|
|
PR — **not applied to that repo by this task**, since registering it there is
|
|
|
|
|
|
outside qonto-assistant's scope and a separate repo/workplan. Added the
|
|
|
|
|
|
end-to-end example the done-criteria asks for: an instance manifest
|
|
|
|
|
|
(`tool_profile: finance-qonto-read`) paired with the MCP server config the
|
|
|
|
|
|
harness would inject for that session, showing the full path from profile
|
|
|
|
|
|
name to the four allowed tool calls.
|
|
|
|
|
|
|
|
|
|
|
|
No code changes — this task is qonto-assistant-side documentation only, per
|
|
|
|
|
|
its own scope note. `pytest` → `28 passed` (unchanged).
|
|
|
|
|
|
|
2026-07-22 21:36:38 +02:00
|
|
|
|
## Task: Unified audit schema across REST and MCP
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: QONTO-WP-0003-T05
|
2026-07-23 11:01:16 +02:00
|
|
|
|
status: done
|
2026-07-22 21:36:38 +02:00
|
|
|
|
priority: medium
|
2026-07-22 21:37:10 +02:00
|
|
|
|
state_hub_task_id: "b7d70c3e-fe55-47d9-9180-cd4c0bedc31a"
|
2026-07-22 21:36:38 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Confirm MCP tool calls emit the exact same structured audit event shape as
|
|
|
|
|
|
REST calls (actor, capability, decision, deny_reason, latency, upstream HTTP
|
|
|
|
|
|
status, policy version — no secret fields), per blueprint §6 Phase 2.4. State
|
|
|
|
|
|
Hub continues to receive operator-level progress notes, not per-call
|
|
|
|
|
|
hot-path events — do not wire per-invocation State Hub writes.
|
|
|
|
|
|
|
|
|
|
|
|
Done when: a test asserts REST and MCP audit records for the same capability
|
|
|
|
|
|
call are schema-identical modulo protocol field.
|
|
|
|
|
|
|
2026-07-23 11:01:16 +02:00
|
|
|
|
**Done 2026-07-23:** This was already true structurally since T02
|
|
|
|
|
|
(`CapabilityService._emit_audit` is the single audit call site for both
|
|
|
|
|
|
transports), but it wasn't pinned down by a test — a future change could
|
|
|
|
|
|
have special-cased one transport's shape without anything failing. Added
|
|
|
|
|
|
`tests/test_audit_parity.py`: calls the same capability through
|
|
|
|
|
|
`protocol="rest"` and `protocol="mcp"` for both an allow path
|
|
|
|
|
|
(`org_summary`) and a deny path (`list_transactions` oversized `page_size`
|
|
|
|
|
|
→ `arg_constraint`), then asserts the two events have identical key sets
|
|
|
|
|
|
and identical values on every field except `request_id`/`timestamp`/
|
|
|
|
|
|
`latency_ms` (expected to vary per call) and `protocol` (expected to
|
|
|
|
|
|
differ by design). A third test asserts no audit event ever contains a
|
|
|
|
|
|
secret-shaped field name (`api_key`, `authorization`, `token`, etc.) —
|
|
|
|
|
|
belt-and-suspenders alongside `tests/test_audit.py`'s existing redaction
|
|
|
|
|
|
test.
|
|
|
|
|
|
|
|
|
|
|
|
Confirmed (`grep`) there is no State Hub coupling anywhere in
|
|
|
|
|
|
`src/qonto_assistant/`: the only audit sink is `AuditLogger` (structured
|
|
|
|
|
|
stdout JSON, or a test-injected `sink` callable) — no per-call State Hub
|
|
|
|
|
|
write exists to accidentally wire up. `pytest` → `31 passed`.
|
|
|
|
|
|
|
2026-07-22 21:36:38 +02:00
|
|
|
|
## Task: MCP smoke path + operator runbook update
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: QONTO-WP-0003-T06
|
2026-07-23 11:04:39 +02:00
|
|
|
|
status: done
|
2026-07-22 21:36:38 +02:00
|
|
|
|
priority: medium
|
2026-07-22 21:37:10 +02:00
|
|
|
|
state_hub_task_id: "1093ec79-eb16-4797-8c9c-5423a925db77"
|
2026-07-22 21:36:38 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Add an MCP-equivalent of `scripts/smoke_rest_api.py`: start the service,
|
|
|
|
|
|
connect over streamable-HTTP, exercise each allowed tool against fixture data
|
|
|
|
|
|
(`QONTO_FIXTURE_DIR`), and confirm deny behavior for an out-of-catalog tool.
|
|
|
|
|
|
Extend `docs/operator-runbook.md` with MCP start/verify instructions
|
|
|
|
|
|
alongside the existing REST section.
|
|
|
|
|
|
|
|
|
|
|
|
Done when: smoke script passes locally against fixtures without real Qonto
|
|
|
|
|
|
credentials, matching the Phase 1 REST smoke pattern.
|
|
|
|
|
|
|
2026-07-23 11:04:39 +02:00
|
|
|
|
**Done 2026-07-23:** Added `scripts/smoke_mcp.py`, same shape as
|
|
|
|
|
|
`scripts/smoke_rest_api.py` (random free port, subprocess-launch the
|
|
|
|
|
|
service against `QONTO_FIXTURE_DIR`, wait on `/v1/health`, assert, tear
|
|
|
|
|
|
down cleanly). Goes one step further than the REST smoke since MCP now has
|
|
|
|
|
|
an auth layer REST doesn't: generates a fresh `QONTO_ASSISTANT_MCP_TOKEN`
|
|
|
|
|
|
per run and connects with the `mcp` SDK's `streamablehttp_client` using it,
|
|
|
|
|
|
so the smoke exercises T03's bearer-token gate rather than bypassing it.
|
|
|
|
|
|
Lists tools and asserts the exact expected catalog; calls `qonto_ping`,
|
|
|
|
|
|
`qonto_org_summary`, `qonto_list_transactions`, and
|
|
|
|
|
|
`qonto_cost_run_rate_hints` against fixture data with the same balance/
|
|
|
|
|
|
recurring-debit assertions the REST smoke makes; calls a never-registered
|
|
|
|
|
|
tool name (`qonto_transfer_funds`) and asserts it comes back as a normal
|
|
|
|
|
|
`isError` result through the real wire protocol, not a crash — confirming
|
|
|
|
|
|
the low-level MCP protocol handler rejects unknown tools cleanly rather
|
|
|
|
|
|
than falling through to anything policy-adjacent.
|
|
|
|
|
|
|
|
|
|
|
|
Extended `docs/operator-runbook.md` with a "One-command MCP smoke" section
|
|
|
|
|
|
next to the existing REST one, and cross-linked it from
|
|
|
|
|
|
`docs/mcp-integration.md`'s manual smoke walkthrough so the two don't drift
|
|
|
|
|
|
out of sync.
|
|
|
|
|
|
|
|
|
|
|
|
Verified: `python3 scripts/smoke_mcp.py --python ../state-hub/.venv/bin/python`
|
|
|
|
|
|
exits `0` against fixtures, no real Qonto credentials. `pytest` → `31 passed`
|
|
|
|
|
|
(unchanged); `python3 -m compileall src tests scripts`.
|
|
|
|
|
|
|
2026-07-22 21:36:38 +02:00
|
|
|
|
## Task: Closure review
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: QONTO-WP-0003-T07
|
|
|
|
|
|
status: todo
|
|
|
|
|
|
priority: low
|
2026-07-22 21:37:10 +02:00
|
|
|
|
state_hub_task_id: "1dc90a1e-0dcc-40c1-80c9-c7403a348db0"
|
2026-07-22 21:36:38 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Mark workplan finished when T01–T06 are done; confirm REST and MCP surfaces
|
|
|
|
|
|
enforce identical policy outcomes end to end. Note Phase 3 seed (flex-auth
|
|
|
|
|
|
resource `finance.qonto.read`, graduated quotas, redaction profiles) in
|
|
|
|
|
|
closure. Run `statehub fix-consistency`.
|