Files

6.7 KiB

Agent 1 — Phase 5 report (MCP gateway)

Branch: dev · Continues the org-platform workstream (PLAN-v2 §6, TODO-agent-1 Phase 5). Provenance: reconstructed from the sibling agent1/org-platform branch and reconciled onto dev, then fully re-verified.

What I did

5.1 mcp_tokens (in models_org.py, migration a8b9c0d1e2f3)

id, organization_id, name, token_hash (unique), prefix (ten characters, e.g. smo_abc123, display only), scopes (CSV), created_by_user_id, expires_at, last_used_at, revoked_at, created_at. The raw token is never stored.

5.2 mcp_service.py

McpService: issue (validates scopes, refuses an empty list, mints smo_<24 random bytes>), list, revoke (a second revoke is 404), verify (hash lookup + hmac.compare_digest; unknown, revoked and expired all return None), touch, creator_of, count_per_org. TokenRateLimiter: in-memory, per token, MCP_RATE_LIMIT_PER_MINUTE (60). MCP_SCOPES is the single list of scopes the gateway understands.

5.3 app/interfaces/mcp/

  • gateway.py — the tools, framework-free and directly callable (what the tests drive). Every call resolves the org from the token (an organization_id argument is ignored), checks the scope, checks org_policies.mcp_enabled, rate-limits, executes, redacts, audits (mcp.tool_call + prefix + tool + ok/error) and commits. Tools: app_get_profile, tasks_list/tasks_get, tasks_create (the current public WorkforceService.create_task signature, called with the token's minter), memory_search, memory_write (fact|preference only, source=user), knowledge_search, pipeline_status ({"available": false}).
  • server.py — the official mcp SDK (streamable HTTP), the eight tools with typed signatures, bearer token from the Authorization header, and main() for python -m app.interfaces.mcp.server → pnpm mcp:dev. Outbound payloads are redacted too.

5.4 services/mcp-gateway/README.md

How to run it, the tool/scope table, the rules the gateway enforces, a client config snippet, the mcp.<root> Caddy suggestion, the environment table and the test command.

5.5 Router mcp_tokens.py

Method Path Who
GET /api/organizations/{org}/mcp-tokens any member (prefix + scopes + status)
POST /api/organizations/{org}/mcp-tokens admin+ (raw token once)
DELETE /api/organizations/{org}/mcp-tokens/{id} admin+
GET /api/organizations/{org}/mcp-tokens/settings any member
GET /api/admin/mcp owner
PUT /api/admin/mcp owner (global kill switch)
PUT /api/admin/mcp/organizations/{id} owner (per-org, org_policies.mcp_enabled)

Audit actions: mcp_token.create, mcp_token.revoke, mcp.global_switch, mcp.org_switch, plus mcp.tool_call per gateway call. The global switch lives in settings.mcp_gateway_enabled (MCP_GATEWAY_ENABLED, added to the agent-1 config.py block and .env.example).

5.6 UI

(app)/settings → «یکپارچه‌سازی (MCP)» tab: switch warnings, token table, mint form with scope checkboxes, show-once modal with a copy button, revoke confirmation, toasts. /admin/mcp: stats row, global switch, per-org table with token counts.

5.7 tests/test_mcp.py (13 tests)

Token shown once and stored only as a hash · unknown/revoked/expired all the same answer · unknown scope refused at mint · a token from org A cannot read org B · scope enforcement per tool · memory_write limited to fact/preference · a provider-key shape never returned · knowledge search and pipeline_status · a task created through the gateway attributed to the token's minter · bad token / unknown tool / global switch / org switch refused · the rate limit · two audit rows per call with the prefix and no raw token · HTTP (viewer 403 on mint, admin 201, 422 on unknown scope, 404 on second revoke, owner-only /admin/mcp). Web src/test/org-mcp.test.tsx (5 tests) covers the settings tab.

Files changed

New: app/application/services/mcp_service.py, app/interfaces/mcp/{__init__,gateway,server}.py, tests/test_mcp.py, apps/web/src/test/org-mcp.test.tsx, apps/web/src/app/(app)/admin/mcp/page.tsx, services/mcp-gateway/README.md, alembic/versions/a8b9c0d1e2f3_mcp_tokens.py. Modified: app/infrastructure/db/models_org.py (+McpTokenRow), app/core/config.py, .env.example, app/domain/identity/audit_actions.py, app/interfaces/http/routers/mcp_tokens.py, apps/web/src/lib/api-org.ts, apps/web/src/app/(app)/settings/page.tsx.

Integration adaptations (dev vs. the reference branch)

  • app_get_profile no longer uses build_org_setup_service. The reference container exported a build_org_setup_service; on dev the wizard service is built inline in app_setup.py, so the gateway now reads the profile through AppProfileRepository.get_for_org + ProjectRepository.get directly. Same output, no new builder, no extra coupling.
  • The settings tab was merged into dev's page, not copied wholesale: dev's settings page differs from the reference branch's (tab set and member section), so only the MCP state/handlers/tab/modals were added on top.

Test output summary

apps/api: pytest -q    → 587 passed, 1 skipped
apps/api: ruff check . → All checks passed!
apps/api: mypy app     → Success: no issues found in 147 source files
apps/api: alembic upgrade → downgrade -1 → upgrade → check → clean, single head
apps/web: tsc --noEmit → clean
apps/web: next lint    → No ESLint warnings or errors
apps/web: vitest run   → 99 passed (13 files)

Deviations / notes

  • The global switch is a setting, not a table row (MCP_GATEWAY_ENABLED): it must hold before the first token exists.
  • Unattributable attempts (bad token, disabled gateway) go to the error ledger, not the audit trail — they happen before the caller is known.
  • The rate limiter is process-local; a restart forgives a burst.
  • pipeline_status is registered and answers {"available": false} until Agent 3's delivery tables are wired at integration.
  • Output redaction happens twice (on write and on the way out).

CHANGE REQUESTs

  1. pipeline_status needs Agent 3's tables. Point the one function in app/interfaces/mcp/gateway.py at the real delivery/deploy tables.
  2. mcp:dev runs the gateway as a separate process (SessionFactory). If it should live inside the API process instead, that is a one-line mount in rpc_app.py.
  3. @agy/contracts has no memory.changed / MCP event types — the web compares raw strings.
  4. build_memory_service / build_model_grant_service are importable only from container_org. Re-export from container.py if one import surface is preferred.