feat(org): model grants, org memory, MCP gateway (v2 phases A1-3..5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
c93b25741e
commit
dee2f72c46
@@ -1,6 +1,101 @@
|
||||
# mcp-gateway
|
||||
|
||||
HTTP (streamable) MCP server. Shell around `apps/api/app/interfaces/mcp/server.py`.
|
||||
Owned by the org-platform workstream; see `docs/plan/TODO-agent-1-org-platform.md` Phase 5.
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user