# mcp-gateway Streamable-HTTP [MCP](https://modelcontextprotocol.io) server that lets an AI agent work inside **one** organization of the platform. Owned by the org-platform workstream; see `docs/plan/TODO-agent-1-org-platform.md` Phase 5. pnpm mcp:dev That runs `python -m app.interfaces.mcp.server`, which binds `MCP_GATEWAY_HOST:MCP_GATEWAY_PORT` (127.0.0.1:8765 by default) and serves `/mcp` on the streamable-HTTP transport. ## What a client sees Eight tools, each guarded by a scope the token was minted with: | tool | scope | what it does | |---|---|---| | `app_get_profile` | `app:read` | the org's app profile: type, language, framework, build/test commands, port | | `tasks_list` | `tasks:read` | the org's tasks, newest first | | `tasks_get` | `tasks:read` | one task by id | | `tasks_create` | `tasks:write` | a new task for the org's manager, as a user order | | `memory_search` | `memory:read` | rules, preferences, lessons, code map | | `memory_write` | `memory:write` | teach a `fact` or a `preference` (nothing else) | | `knowledge_search` | `memory:read` | the org's long-form knowledge sources | | `pipeline_status` | `delivery:read` | delivery state — currently always `{"available": false}` | ## Rules the gateway enforces - **The organization comes from the token, never from the arguments.** A tool body cannot be pointed at another tenant, whatever it is asked. - **A token is shown once.** Mint one in `/settings` → «یکپارچه‌سازی (MCP)»; the raw value (`smo_…`) appears in that one response and is never stored — only its hash and a six-character prefix, which is what the audit trail carries. - **Scopes are a floor.** A `tasks:read` token cannot create a task. - **Every call is audited** (`mcp.tool_call`, token prefix, tool, ok/error) and **rate-limited** to `MCP_RATE_LIMIT_PER_MINUTE` (60) per token, in memory. - **Every output is redacted.** A secret that reaches the gateway does not leave it. - **Two kill switches.** The platform owner can disable the gateway globally (`/admin/mcp`) or for one organization; both are honoured on the next call. ## Client configuration The gateway speaks plain MCP over HTTP with a bearer token: ```json { "mcpServers": { "smart-org": { "url": "https://mcp.example.com/mcp", "headers": { "Authorization": "Bearer smo_PASTE_YOUR_TOKEN" } } } } ``` Local, without Caddy: ```bash curl -sS http://127.0.0.1:8765/mcp \ -H "Authorization: Bearer smo_PASTE_YOUR_TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' ``` ## Putting Caddy in front of it Suggested host: `mcp.` (the platform owner configures Caddy — see `infra/`): ```caddy mcp.example.com { reverse_proxy 127.0.0.1:8765 } ``` Keep the gateway bound to loopback in the normal case: the process is small, it has no session state of its own, and the platform's TLS termination and rate limiting belong at the edge. `MCP_GATEWAY_HOST=0.0.0.0` is for a container that must be reached directly, not for a public deployment without a proxy. ## Environment | variable | default | meaning | |---|---|---| | `MCP_GATEWAY_HOST` | `127.0.0.1` | bind address | | `MCP_GATEWAY_PORT` | `8765` | bind port | | `MCP_GATEWAY_ENABLED` | `true` | the owner's global switch | | `MCP_TOKEN_TTL_DAYS` | `90` | default token lifetime | | `MCP_RATE_LIMIT_PER_MINUTE` | `60` | calls per token per minute, in memory | ## Tests pnpm --filter @agy/api test tests/test_mcp.py They drive `McpGateway.call` directly (the SDK is a thin adapter over it) and cover tenant isolation, scope enforcement, revoked/expired tokens, the rate limit, audit rows, and secret redaction.