feat(mcpd): McpToken schema + CRUD routes + introspection
Adds a new McpToken Prisma model (project-scoped, SHA-256 hashed at rest, optional expiry, revocable) plus backing repository, service, and REST routes. Tokens are a first-class RBAC subject: new 'McpToken' kind is added to the subject enum and the service auto-creates an RbacDefinition with subject McpToken:<sha> when bindings are provided. Creator-permission ceiling: the service rejects any requested binding the creator cannot already satisfy themselves (re-uses rbacService.canAccess / canRunOperation). rbacMode=clone snapshots the creator's full permissions into the token. Routes: POST /api/v1/mcptokens create (returns raw token once) GET /api/v1/mcptokens list (filter by project) GET /api/v1/mcptokens/:id describe (no secret in response) POST /api/v1/mcptokens/:id/revoke soft-delete + remove RbacDef DELETE /api/v1/mcptokens/:id hard-delete GET /api/v1/mcptokens/introspect validate raw bearer (used by mcplocal) Extends AuditEvent with optional tokenName/tokenSha fields (indexed) so token-driven activity can be filtered later. Adds token helpers in @mcpctl/shared: TOKEN_PREFIX='mcpctl_pat_', generateToken, hashToken, isMcpToken, timingSafeEqualHex. Follow-up PRs add the auth-hook dispatch on the prefix, the CLI verbs, and the HTTP-mode mcplocal that calls /introspect. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
78
docs/mcptoken-implementation.md
Normal file
78
docs/mcptoken-implementation.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# mcptoken + HTTP-mode mcplocal — implementation log
|
||||
|
||||
Companion to the approved plan at `/home/michal/.claude/plans/lets-discuss-something-i-bright-lovelace.md`.
|
||||
This file is updated as each milestone lands, so you can review what was actually done vs. what was planned.
|
||||
|
||||
## Context (why)
|
||||
|
||||
You're running your own vLLM inference outside Claude Code and want it to consume mcpctl over MCP with the same UX Claude gets: project-scoped server discovery, proxy models, the pipeline cache. Today `mcplocal` is systemd-only and serves STDIO — unreachable from off-host and unauthenticated. This work adds:
|
||||
|
||||
1. A containerized, network-accessible `mcplocal` serving Streamable HTTP.
|
||||
2. A new `McpToken` resource (CLI: `mcpctl get/create/delete mcptoken`) — project-scoped bearer tokens with the same RBAC stack as users. Hashed at rest; raw value shown once.
|
||||
3. Tokens as a first-class RBAC subject kind (`McpToken:<sha>`), with a creator-permission ceiling so non-admins cannot mint escalated tokens.
|
||||
4. k8s deploy (Service `mcp`, Ingress `mcp.ad.itaz.eu`, PVC-backed `FileCache`).
|
||||
5. A CLI breaking change: `mcpctl create rbac --binding edit:servers` → `--roleBindings role:edit,resource:servers`. You explicitly asked for this; only one command uses it.
|
||||
6. A product-grade `mcpctl test mcp <url>` verb for validating any Streamable-HTTP MCP endpoint, reused by smoke tests.
|
||||
|
||||
## Branch
|
||||
|
||||
All work lives on `feat/mcptoken` (off `main` at `3149ea3`).
|
||||
|
||||
## Pre-work committed to main (outside this branch)
|
||||
|
||||
Before starting the feature, we flushed your in-flight changes to main so they wouldn't travel with the branch:
|
||||
|
||||
- **`3149ea3 fix: MCP proxy resilience — discovery cache, default liveness probes`** — per-server `tools/list` cache in `McpRouter` with positive+negative TTL so dead upstreams only stall the first call; default liveness probe (tools/list through the real production path) applied to any RUNNING instance without an explicit healthCheck. Already pushed to origin.
|
||||
|
||||
## Status legend
|
||||
|
||||
- ✅ done
|
||||
- 🚧 in progress
|
||||
- ⬜ not started
|
||||
|
||||
## PR 1 — Schema + token helpers + mcpd CRUD routes ✅
|
||||
|
||||
| # | Step | Status |
|
||||
|---|---|---|
|
||||
| 1 | `McpToken` Prisma model + Project/User reverse relations; `AuditEvent.tokenName` / `tokenSha` + index | ✅ |
|
||||
| 2 | `src/shared/src/tokens/index.ts` — `generateToken`, `hashToken`, `isMcpToken`, `timingSafeEqualHex`, `TOKEN_PREFIX` | ✅ |
|
||||
| 3 | `src/mcpd/src/repositories/mcp-token.repository.ts` + new interfaces in `repositories/interfaces.ts` | ✅ |
|
||||
| 4 | `src/mcpd/src/services/mcp-token.service.ts` — creator-ceiling via `rbacService.canAccess`/`canRunOperation`, raw token returned only once, auto-creates an `RbacDefinition` with subject `McpToken:<sha>` when bindings are non-empty | ✅ |
|
||||
| 5 | `src/mcpd/src/routes/mcp-tokens.ts` — POST / GET / GET:id / DELETE:id + POST:id/revoke + GET /introspect | ✅ |
|
||||
| 6 | Wired into `main.ts` — repo/service constructed, routes registered, `mcptokens` added to URL→permission map + name resolver; `/mcptokens/introspect` added to auth-skip list so mcplocal can call it with a raw McpToken bearer | ✅ |
|
||||
| 7 | RBAC extensions: new subject kind `McpToken` in `rbac-definition.schema.ts`; `mcptokens` added to `RBAC_RESOURCES` and `RESOURCE_ALIASES`; `rbac.service.ts` threads optional `mcpTokenSha` through `canAccess`, `canRunOperation`, `getAllowedScope`, `getPermissions`; resolver matches `{kind:'McpToken', name: sha}` | ✅ |
|
||||
| 8 | Unit tests — `tests/mcp-token-service.test.ts` covering: empty/clone modes, ceiling rejection, RbacDefinition auto-create with correct `McpToken:<sha>` subject, duplicate-name conflict, introspect valid/revoked/expired/unknown, revoke deletes the RbacDefinition. 11/11 green. Full mcpd suite still 648/648. | ✅ |
|
||||
|
||||
### What this PR does NOT do yet (coming in PR 3)
|
||||
|
||||
- The mcpd **auth middleware** does not yet dispatch on the token prefix. A raw `mcpctl_pat_…` bearer sent to any `/api/v1/*` endpoint (other than `/introspect`) is still rejected as an invalid session. That's intentional — PR 3 extends `middleware/auth.ts` to recognize both session bearers and McpToken bearers.
|
||||
- No CLI yet. Tokens can be created only via `POST /api/v1/mcptokens` for now.
|
||||
|
||||
## PR 2 — RBAC CLI migration
|
||||
|
||||
_(blocked by PR 1 — parser is reused by PR 3)_
|
||||
|
||||
## PR 3 — CLI mcptoken verbs + mcpd auth dispatch + audit
|
||||
|
||||
_(blocked)_
|
||||
|
||||
## PR 4 — HTTP-mode mcplocal + container + `mcpctl test mcp` + smoke
|
||||
|
||||
_(blocked)_
|
||||
|
||||
## Design decisions recap (so you don't have to re-read the plan)
|
||||
|
||||
| Decision | Choice |
|
||||
|---|---|
|
||||
| Transport | Streamable HTTP only |
|
||||
| Binary shape | Same `@mcpctl/mcplocal` package, two entry files (`main.ts` STDIO, `serve.ts` HTTP) |
|
||||
| Container runtime | Node (not bun-compiled) — mirrors mcpd |
|
||||
| Cache | PVC at `/var/lib/mcplocal/cache` |
|
||||
| Hostname | k8s Service `mcp`, Ingress `mcp.ad.itaz.eu` |
|
||||
| Token format | `mcpctl_pat_<32-byte base62>`, stored as SHA-256, shown-once at create |
|
||||
| Resource | `McpToken`, CLI noun `mcptoken`, one-project-per-token, FK cascade |
|
||||
| Subject kind | New `McpToken:<sha>` |
|
||||
| TTL | No default. Optional `--ttl 30d` / `never` / ISO date |
|
||||
| Default bindings | `--rbac=empty` (default), `--rbac=clone`, `--bind <kv>` — creator ceiling enforced server-side |
|
||||
| Binding CLI | `--roleBindings role:view,resource:servers[,name:foo]` or `--roleBindings action:logs` |
|
||||
| Project enforcement | Endpoint visibility only (no strict create-time check) — same mechanism Claude uses |
|
||||
Reference in New Issue
Block a user