feat: mcpctl mcptoken verbs + mcpd auth dispatch + audit plumbing
Adds the end-to-end CLI surface for McpTokens and the mcpd auth dispatch
that recognizes them.
mcpd auth middleware:
- Dispatch on the `mcpctl_pat_` bearer prefix. McpToken bearers resolve
through a new `findMcpToken(hash)` dep, populating `request.mcpToken`
and `request.userId = ownerId`. Everything else follows the existing
session path.
- Returns 401 for revoked / expired / unknown tokens.
- Global RBAC hook now threads `mcpTokenSha` into `canAccess` /
`canRunOperation` / `getAllowedScope`, and enforces a hard
project-scope check: a McpToken principal can only hit
`/api/v1/projects/<its-project>/...`.
CLI verbs:
- `mcpctl create mcptoken <name> -p <proj> [--rbac empty|clone]
[--bind role:view,resource:servers] [--ttl 30d|never|ISO]
[--description ...] [--force]` — returns the raw token once.
- `mcpctl get mcptokens [-p <proj>]` — table with
NAME/PROJECT/PREFIX/CREATED/LAST USED/EXPIRES/STATUS.
- `mcpctl get mcptoken <name> -p <proj>` and
`mcpctl describe mcptoken <name> -p <proj>` — describe surfaces the
auto-created RBAC bindings.
- `mcpctl delete mcptoken <name> -p <proj>`.
- `apply -f` support with `kind: mcptoken`. Tokens are immutable, so
apply creates if missing and skips if the name is already active.
Audit plumbing:
- `AuditEvent` / collector now carry optional `tokenName` / `tokenSha`.
`setSessionMcpToken` sits alongside `setSessionUserName`; both feed a
per-session principal map used at emit time.
- `AuditEventService` query accepts `tokenName` / `tokenSha` filters.
- Console `AuditEvent` type carries the new fields so a follow-up can
add a TOKEN column.
Completions regenerated. 1764/1764 tests pass workspace-wide.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -99,6 +99,25 @@ export function createProgram(): Command {
|
||||
}
|
||||
}
|
||||
|
||||
// --project scoping for mcptokens
|
||||
if (!nameOrId && resource === 'mcptokens' && projectName) {
|
||||
return client.get<unknown[]>(`/api/v1/mcptokens?projectName=${encodeURIComponent(projectName)}`);
|
||||
}
|
||||
|
||||
// Name-based lookup for mcptokens: names are unique only within a project
|
||||
if (nameOrId && resource === 'mcptokens' && !/^c[a-z0-9]{24}/.test(nameOrId)) {
|
||||
if (!projectName) {
|
||||
throw new Error('mcptoken names are scoped to a project — pass --project <name> or use the token id (cuid)');
|
||||
}
|
||||
const items = await client.get<Array<{ id: string; name: string }>>(
|
||||
`/api/v1/mcptokens?projectName=${encodeURIComponent(projectName)}`,
|
||||
);
|
||||
const match = items.find((i) => i.name === nameOrId);
|
||||
if (!match) throw new Error(`mcptoken '${nameOrId}' not found in project '${projectName}'`);
|
||||
const item = await client.get(`/api/v1/mcptokens/${match.id}`);
|
||||
return [item];
|
||||
}
|
||||
|
||||
if (nameOrId) {
|
||||
// Glob pattern — use query param filtering
|
||||
if (nameOrId.includes('*')) {
|
||||
@@ -132,6 +151,19 @@ export function createProgram(): Command {
|
||||
return client.get(`/api/v1/${resource}/${match.id as string}`);
|
||||
}
|
||||
|
||||
// Mcptokens: names are project-scoped. CUIDs pass straight through.
|
||||
if (resource === 'mcptokens' && !/^c[a-z0-9]{24}/.test(nameOrId)) {
|
||||
if (!projectName) {
|
||||
throw new Error('mcptoken names are scoped to a project — pass --project <name> or use the token id (cuid)');
|
||||
}
|
||||
const items = await client.get<Array<Record<string, unknown>>>(
|
||||
`/api/v1/mcptokens?projectName=${encodeURIComponent(projectName)}`,
|
||||
);
|
||||
const match = items.find((item) => item.name === nameOrId);
|
||||
if (!match) throw new Error(`mcptoken '${nameOrId}' not found in project '${projectName}'`);
|
||||
return client.get(`/api/v1/mcptokens/${match.id as string}`);
|
||||
}
|
||||
|
||||
let id: string;
|
||||
try {
|
||||
id = await resolveNameOrId(client, resource, nameOrId);
|
||||
|
||||
Reference in New Issue
Block a user