Files
smart-org/services/mcp-gateway/README.md
T

102 lines
3.7 KiB
Markdown

# 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.<root domain>` (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.