@mcpcloud/cli 0.11.0 → 0.12.0-next-20260721180156

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/dist/index.js +126 -7
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -159,6 +159,7 @@ _Generated from the live command tree by `bun run docs:readme` — do not edit b
159
159
 
160
160
  | Command | Description |
161
161
  | --- | --- |
162
+ | `mcp analytics` | Show the workspace analytics rollup (readiness, funnel, usage) |
162
163
  | `mcp completion <shell>` | Print a shell completion script (bash, zsh, or fish) |
163
164
  | `mcp doctor` | Diagnose the CLI environment: Node, config, base URL, API key, claude CLI, and updates |
164
165
  | `mcp help [topic]` | Show extended help for a topic (auth, profiles, agents, errors) |
package/dist/index.js CHANGED
@@ -4919,6 +4919,10 @@ var ERROR_REMEDIATION = {
4919
4919
  organization_access_required: "Not a member of that organization — a stale saved org is the usual cause after switching accounts. Run `mcp orgs list`, then `mcp config set-org <name>`.",
4920
4920
  organization_mismatch: "The deployment belongs to a different org. Pass --org <correct-id>.",
4921
4921
  insufficient_role: "Your role doesn't permit this action. Contact an org owner.",
4922
+ insufficient_scope: "This key is scoped. Mint one with the needed scope (`mcp api-keys create --scope <scope>` or `--preset <preset>`), or rotate from a signed-in browser session to widen.",
4923
+ scope_widening_requires_session: "Widening a key must be done from a signed-in browser session (Settings → API keys). Narrowing works from anywhere.",
4924
+ api_key_org_mismatch: "This key is pinned to a different organization. Pass the pinned org, or mint an unpinned key.",
4925
+ invalid_organization: "Pass an organization you belong to — check `mcp orgs list`.",
4922
4926
  resource_not_found: "No such resource for this org. Verify the ID with the matching `list` command.",
4923
4927
  project_id_required: "Pass --project <id> or look up tools via `mcp tools list --server <id>`.",
4924
4928
  deployment_id_required: "Pass a deploymentId. List with `mcp servers list` to find one.",
@@ -5022,6 +5026,16 @@ function handleError(err) {
5022
5026
  if (info.status)
5023
5027
  parts.push(`(status: ${info.status})`);
5024
5028
  printError(parts.join(" "));
5029
+ if (info.code === "insufficient_scope" && info.details) {
5030
+ const required = info.details["requiredScopes"];
5031
+ const granted = info.details["grantedScopes"];
5032
+ if (Array.isArray(required) && required.length > 0) {
5033
+ console.error(` this key lacks: ${required.join(" or ")}`);
5034
+ }
5035
+ if (Array.isArray(granted)) {
5036
+ console.error(` it has: ${granted.length > 0 ? granted.join(" ") : "(no scopes)"}`);
5037
+ }
5038
+ }
5025
5039
  const remediation = getRemediation(info.code);
5026
5040
  if (remediation)
5027
5041
  console.error(` ${remediation}`);
@@ -11380,6 +11394,26 @@ function parseExpiresInDays(raw) {
11380
11394
  function formatPreview(key) {
11381
11395
  return `${key.keyPrefix}…${key.lastFour}`;
11382
11396
  }
11397
+ function formatGrants(key) {
11398
+ if (key.scopes == null)
11399
+ return key.preset === "full" ? "full" : "full (legacy)";
11400
+ if (key.preset && key.preset !== "custom")
11401
+ return key.preset;
11402
+ return `custom (${key.scopes.length})`;
11403
+ }
11404
+ function collectScope(value, previous) {
11405
+ return [...previous, value];
11406
+ }
11407
+ function grantBodyFromOptions(opts) {
11408
+ if (opts.preset && opts.scope && opts.scope.length > 0) {
11409
+ throw new Error("--preset and --scope are mutually exclusive.");
11410
+ }
11411
+ return {
11412
+ ...opts.preset ? { preset: opts.preset } : {},
11413
+ ...opts.scope && opts.scope.length > 0 ? { scopes: opts.scope } : {},
11414
+ ...opts.clearOrg ? { orgId: null } : opts.org ? { orgId: opts.org } : {}
11415
+ };
11416
+ }
11383
11417
  var API_KEY_KIND = { label: "api key", listHint: "mcp api-keys list" };
11384
11418
  async function resolveApiKeyId(reference) {
11385
11419
  const ref = reference.trim();
@@ -11406,26 +11440,29 @@ function registerApiKeyCommands(program2) {
11406
11440
  id: k2.id,
11407
11441
  name: k2.name,
11408
11442
  preview: formatPreview(k2),
11409
- created: formatDate(k2.createdAt),
11443
+ grants: formatGrants(k2),
11444
+ org: k2.orgId ?? "—",
11410
11445
  "last used": k2.lastUsedAt ? formatDate(k2.lastUsedAt) : "—",
11411
11446
  expires: k2.expiresAt ? formatDate(k2.expiresAt) : "never"
11412
11447
  })), [
11413
11448
  { key: "id", label: "ID", width: 20 },
11414
11449
  { key: "name", label: "Name", width: 24 },
11415
11450
  { key: "preview", label: "Preview", width: 18 },
11416
- { key: "created", label: "Created", width: 22 },
11451
+ { key: "grants", label: "Grants", width: 14 },
11452
+ { key: "org", label: "Org", width: 20 },
11417
11453
  { key: "last used", label: "Last Used", width: 22 },
11418
11454
  { key: "expires", label: "Expires", width: 22 }
11419
11455
  ], data);
11420
11456
  }));
11421
- apiKeys.command("create").description("Create a new API key (the secret is shown once)").requiredOption("--name <name>", "Display name for the key").option("--expires-in <duration>", "Auto-expire the key after this long: 90d, 12w, 6m, or a day count (1–365). Default: never.").action(runAction(async (opts) => {
11457
+ apiKeys.command("create").description("Create a new API key (the secret is shown once)").requiredOption("--name <name>", "Display name for the key").option("--expires-in <duration>", "Auto-expire the key after this long: 90d, 12w, 6m, or a day count (1–365). Default: never.").option("--preset <preset>", "Scope preset: full, agent, read-only, or ci-deploy. Default: full authority.").option("--scope <scope>", "Grant a single scope (repeatable, e.g. --scope servers:read --scope servers:deploy). Mutually exclusive with --preset.", collectScope, []).option("--org <orgId>", "Pin the key to one organization (must be one you belong to).").action(runAction(async (opts) => {
11422
11458
  const name = opts.name.trim();
11423
11459
  if (!name)
11424
11460
  throw new Error("--name must not be empty.");
11425
11461
  const expiresInDays = parseExpiresInDays(opts.expiresIn);
11426
11462
  const data = await api.post("/api/v1/api-keys", {
11427
11463
  name,
11428
- ...expiresInDays !== undefined ? { expiresInDays } : {}
11464
+ ...expiresInDays !== undefined ? { expiresInDays } : {},
11465
+ ...grantBodyFromOptions(opts)
11429
11466
  });
11430
11467
  if (isJsonMode()) {
11431
11468
  printJson(data);
@@ -11439,6 +11476,9 @@ function registerApiKeyCommands(program2) {
11439
11476
  key: data.apiKey,
11440
11477
  "key prefix": data.keyPrefix,
11441
11478
  "last four": data.lastFour,
11479
+ grants: formatGrants(data),
11480
+ scopes: data.scopes?.join(" ") ?? "all (full authority)",
11481
+ org: data.orgId ?? "any you belong to",
11442
11482
  expires: data.expiresAt ? formatDate(data.expiresAt) : "never"
11443
11483
  });
11444
11484
  console.log("");
@@ -11457,27 +11497,36 @@ function registerApiKeyCommands(program2) {
11457
11497
  name: k2.name,
11458
11498
  preview: formatPreview(k2),
11459
11499
  status: k2.status,
11500
+ grants: formatGrants(k2),
11501
+ scopes: k2.scopes?.join(" ") ?? "all (full authority)",
11502
+ org: k2.orgId ?? "any you belong to",
11460
11503
  created: formatDate(k2.createdAt),
11461
11504
  "last used": k2.lastUsedAt ? formatDate(k2.lastUsedAt) : "—",
11462
11505
  expires: k2.expiresAt ? formatDate(k2.expiresAt) : "never",
11463
11506
  revoked: k2.revokedAt ? formatDate(k2.revokedAt) : "—"
11464
11507
  });
11465
11508
  }));
11466
- apiKeys.command("rotate <apiKey>").description("Mint a replacement key and grace-expire the old one (24h window; accepts id or name). The new secret is shown once.").option("--expires-in <duration>", "Expiry for the NEW key: 90d, 12w, 6m, or a day count (1–365). Default: never.").addHelpText("after", [
11509
+ apiKeys.command("rotate <apiKey>").description("Mint a replacement key and grace-expire the old one (24h window; accepts id or name). The new secret is shown once.").option("--expires-in <duration>", "Expiry for the NEW key: 90d, 12w, 6m, or a day count (1–365). Default: never.").option("--preset <preset>", "Re-scope the new key to a preset (narrowing works from anywhere; widening needs a browser session).").option("--scope <scope>", "Re-scope the new key to explicit scopes (repeatable). Mutually exclusive with --preset.", collectScope, []).option("--org <orgId>", "Pin the new key to one organization.").option("--clear-org", "Remove the organization pin (requires a browser session).").addHelpText("after", [
11467
11510
  "",
11468
11511
  "The old key keeps working for 24h so running automation can swap to the",
11469
11512
  "new secret before the old one stops. Update your secrets within that window.",
11470
11513
  "",
11514
+ "Scopes: omitted flags inherit the old key’s grants. Narrowing (fewer",
11515
+ "scopes, adding an org pin) works from any credential; widening (more",
11516
+ "scopes, clearing/changing a pin) must come from a signed-in session.",
11517
+ "",
11471
11518
  "Examples:",
11472
11519
  " $ mcp api-keys rotate key_123",
11473
- ' $ mcp api-keys rotate "CI deploy key" --expires-in 90d'
11520
+ ' $ mcp api-keys rotate "CI deploy key" --expires-in 90d',
11521
+ " $ mcp api-keys rotate ci --preset ci-deploy --org org_abc"
11474
11522
  ].join(`
11475
11523
  `)).action(runAction(async (apiKeyRef, opts) => {
11476
11524
  const apiKeyId = await resolveApiKeyId(apiKeyRef);
11477
11525
  const expiresInDays = parseExpiresInDays(opts.expiresIn);
11478
11526
  const data = await api.patch("/api/v1/api-keys", {
11479
11527
  apiKeyId,
11480
- ...expiresInDays !== undefined ? { expiresInDays } : {}
11528
+ ...expiresInDays !== undefined ? { expiresInDays } : {},
11529
+ ...grantBodyFromOptions(opts)
11481
11530
  });
11482
11531
  if (isJsonMode()) {
11483
11532
  printJson(data);
@@ -11491,6 +11540,9 @@ function registerApiKeyCommands(program2) {
11491
11540
  key: data.apiKey,
11492
11541
  "key prefix": data.keyPrefix,
11493
11542
  "last four": data.lastFour,
11543
+ grants: formatGrants(data),
11544
+ scopes: data.scopes?.join(" ") ?? "all (full authority)",
11545
+ org: data.orgId ?? "any you belong to",
11494
11546
  expires: data.expiresAt ? formatDate(data.expiresAt) : "never",
11495
11547
  "old key": data.rotatedFrom?.apiKeyId ?? "—",
11496
11548
  "old key valid until": data.rotatedFrom ? formatDate(data.rotatedFrom.gracePeriodEndsAt) : "—"
@@ -32931,6 +32983,72 @@ function registerOrgCommands(program2) {
32931
32983
  }));
32932
32984
  }
32933
32985
 
32986
+ // src/commands/analytics.ts
32987
+ function parseWindowDays2(raw) {
32988
+ if (!raw)
32989
+ return;
32990
+ const match = /^(\d{1,2})d?$/.exec(raw.trim());
32991
+ const days = match ? Number.parseInt(match[1], 10) : Number.NaN;
32992
+ if (!Number.isFinite(days) || days < 1 || days > 90) {
32993
+ throw new Error("--since must be a day count between 1 and 90 (e.g. 14d).");
32994
+ }
32995
+ return days;
32996
+ }
32997
+ function registerAnalyticsCommands(program2) {
32998
+ program2.command("analytics").description("Show the workspace analytics rollup (readiness, funnel, usage)").option("--org <organizationId>", "Organization ID").option("--since <window>", "Window in days over closed days (1–90, default 14)").addHelpText("after", [
32999
+ "",
33000
+ "Examples:",
33001
+ " $ mcp analytics # 14-day rollup",
33002
+ " $ mcp analytics --since 30d",
33003
+ " $ mcp --json analytics | jq '.summary.usage.totals'"
33004
+ ].join(`
33005
+ `)).action(runAction(async (opts) => {
33006
+ const orgId = await resolveOrgId(opts.org);
33007
+ const windowDays = parseWindowDays2(opts.since);
33008
+ const data = await api.get("/api/v1/analytics/summary", {
33009
+ organizationId: orgId,
33010
+ ...windowDays ? { windowDays: String(windowDays) } : {}
33011
+ });
33012
+ if (isJsonMode()) {
33013
+ printJson(data);
33014
+ return;
33015
+ }
33016
+ const { summary } = data;
33017
+ printKeyValue({
33018
+ Window: `${summary.windowDays}d (closed days)`,
33019
+ Servers: `${summary.readiness.total} total · ${summary.readiness.built} built · ${summary.readiness.deployed} deployed · ${summary.readiness.called} called`,
33020
+ Status: `${summary.status.live} live · ${summary.status.draft} draft · ${summary.status.needsAttention} need attention`,
33021
+ Funnel: `imported ${summary.funnel.imported} → built ${summary.funnel.built} → deployed ${summary.funnel.deployed} → installed ${summary.funnel.installed} → first call ${summary.funnel.firstCall}`,
33022
+ Projects: String(summary.projectsTotal)
33023
+ });
33024
+ if (!summary.usage) {
33025
+ console.log(`
33026
+ Usage detail requires admin organization access.`);
33027
+ return;
33028
+ }
33029
+ const { usage } = summary;
33030
+ printKeyValue({
33031
+ "Tool calls": `${usage.totals.toolCalls} (${usage.totals.toolErrors} errors · ${(usage.totals.errorRate * 100).toFixed(1)}%)`,
33032
+ Latency: `p50 ${usage.latency.p50Ms ?? "—"}ms · p95 ${usage.latency.p95Ms ?? "—"}ms`,
33033
+ Edge: `${usage.edge.requests} requests · ${usage.edge.unauthorized} unauthorized · ${usage.edge.rateLimited} rate-limited · ${usage.edge.failed} failed`,
33034
+ "Projection savings": `${usage.projectionSavedBytes} bytes`
33035
+ });
33036
+ if (usage.topTools.length) {
33037
+ printList(usage.topTools, [
33038
+ { key: "toolName", label: "Tool", width: 36 },
33039
+ { key: "calls", label: "Calls", width: 8 },
33040
+ { key: "errors", label: "Errors", width: 8 }
33041
+ ], usage.topTools);
33042
+ }
33043
+ if (usage.topServers.length) {
33044
+ printList(usage.topServers, [
33045
+ { key: "name", label: "Server", width: 36 },
33046
+ { key: "requests", label: "Requests", width: 10 }
33047
+ ], usage.topServers);
33048
+ }
33049
+ }));
33050
+ }
33051
+
32934
33052
  // src/commands/connections.ts
32935
33053
  async function readKeyFromStdin() {
32936
33054
  const chunks = [];
@@ -37656,6 +37774,7 @@ function createProgram() {
37656
37774
  registerDeploymentCommands(program2);
37657
37775
  registerOrgCommands(program2);
37658
37776
  registerUsageCommands(program2);
37777
+ registerAnalyticsCommands(program2);
37659
37778
  registerConnectionsCommands(program2);
37660
37779
  registerMetricsCommands(program2);
37661
37780
  registerDevCommand(program2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcpcloud/cli",
3
- "version": "0.11.0",
3
+ "version": "0.12.0-next-20260721180156",
4
4
  "description": "The official CLI for MCPCloud — manage projects, servers, skills, and API keys from the terminal",
5
5
  "type": "module",
6
6
  "bin": {