# 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:`), 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 ` 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:` 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:` 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:` | | TTL | No default. Optional `--ttl 30d` / `never` / ISO date | | Default bindings | `--rbac=empty` (default), `--rbac=clone`, `--bind ` — 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 |