@cloudgrid-io/mcp 0.21.0 → 0.21.1

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.
package/README.md CHANGED
@@ -197,16 +197,16 @@ recording whether the user agreed to share the conversation (`deploy.js:544`),
197
197
  and a `context` object the model supplies — which by its declared shape
198
198
  (`register.js:964-975`) can carry the failing inputs (for example the HTML or
199
199
  args) and the original request. The transcript itself is not sent by this tool.
200
- The report scrub is **key-name-only**: a value is redacted only when its *key*
201
- matches a secret-looking name (`password`, `token`, `api_key`, …), so a secret
202
- embedded in a value — for example an API key inside HTML sent under
203
- `context.inputs` — is not redacted, and the error summary (`message`) is not
204
- scrubbed at all. This same key-name filter is applied client-side
205
- (`deploy.js:439-448`); the platform's error endpoint applies a key-name filter
206
- too, not a value-based one, so neither layer catches a secret that sits in a
207
- value. To keep a secret out of a report, do not place it in the `message` or
208
- `context` you send, or set `CLOUDGRID_TELEMETRY=off` to disable reporting
209
- entirely.
200
+ The report scrub is **two-layer**: values AND the `message` field are scanned
201
+ for known secret shapes (API key prefixes such as `sk-ant-`, `sk-proj-`,
202
+ `AKIA…`; JWTs; PEM private-key blocks; Bearer tokens; basic-auth URL
203
+ credentials) and matches are replaced with `[REDACTED]`. Arbitrary or
204
+ unknown-shape secrets that do not match a known pattern still transit
205
+ unredacted. The client-side scrub (`scrubReportContext` + `scrubSecretValues`
206
+ in deploy.js) runs before the POST; the server endpoint applies a server-side
207
+ key-name filter on top. To keep a secret out of a report, do not place it in
208
+ the `message` or `context` you send, or set `CLOUDGRID_TELEMETRY=off` to
209
+ disable reporting entirely.
210
210
 
211
211
  **Version check.** At startup the local edition makes one request to
212
212
  `https://registry.npmjs.org/@cloudgrid-io/mcp/latest` to detect a stale `.mcpb`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudgrid-io/mcp",
3
- "version": "0.21.0",
3
+ "version": "0.21.1",
4
4
  "description": "MCP access to CloudGrid — the live runtime environment where the user's apps run WITH the infrastructure they need out of the box: managed MongoDB, Redis (cache/queues/pub-sub), a pgvector store, persistent disk, and built-in AI with no API keys, in any language or stack, no Docker or Kubernetes to manage. Brainstorm, build, plug, and improve apps, games, dashboards, and tools; instant single-page publishes and full runtime-app deploys with a live URL — for developers and non-technical users. Three editions: local stdio (full toolset), hosted web (light, CLI-free), and a public docs-search server.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -43,6 +43,22 @@ function isProvisioningCode(code) {
43
43
  return PROVISIONING_CODES.has(code);
44
44
  }
45
45
 
46
+ // Same org->grid rename, same dual-accept safety (cloudgrid-io/cloudgrid#2673).
47
+ // These two codes gate the first-time-user "no grid" path and the wrong-grid
48
+ // path — if the MCP recognizes only the old ORG_* spelling, the API renaming it
49
+ // silently darks those paths (the account-has-no-grid guidance and the typo
50
+ // hint stop firing). NO_ACTIVE_GRID is ALREADY emitted by the API alongside
51
+ // NO_ACTIVE_ORG; GRID_NOT_ACCESSIBLE is the anticipated rename of
52
+ // ORG_NOT_ACCESSIBLE. Accept both spellings so the API rename can land anytime.
53
+ const NO_ACTIVE_GRID_CODES = new Set(["NO_ACTIVE_GRID", "NO_ACTIVE_ORG"]);
54
+ function isNoActiveGridCode(code) {
55
+ return NO_ACTIVE_GRID_CODES.has(code);
56
+ }
57
+ const GRID_NOT_ACCESSIBLE_CODES = new Set(["GRID_NOT_ACCESSIBLE", "ORG_NOT_ACCESSIBLE"]);
58
+ function isGridNotAccessibleCode(code) {
59
+ return GRID_NOT_ACCESSIBLE_CODES.has(code);
60
+ }
61
+
46
62
 
47
63
  // ── Org listing (bearer-authed, web edition) ──────────────────────────────────
48
64
  // Fetches the signed-in user's orgs via GET /api/v2/orgs. The JWT does not
@@ -480,13 +496,51 @@ const REPORT_SECRET_KEY_PATTERNS = [
480
496
  /access[_-]?key/i,
481
497
  ];
482
498
 
483
- // Light client-side scrub of obviously secret-looking values in the report
484
- // context. Redacts values under secret-looking KEYS; leaves everything else
485
- // intact (the server applies the same key-name filter, not a value-based one —
486
- // a secret embedded in a value transits unredacted). Bounded depth so a
487
- // pathological object can't loop.
499
+ // Value-level secret patterns — must stay in sync with TEXT_SECRET_PATTERNS
500
+ // in session-logger.js (which scrubs free text for session logs). The lists are
501
+ // separate because this module operates on structured object values via
502
+ // scrubReportContext, while session-logger operates on free text via scrubText.
503
+ // Both scan string content for the same secret shapes; if you add a pattern to
504
+ // one, add it to the other.
505
+ const REPORT_SECRET_VALUE_PATTERNS = [
506
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
507
+ /eyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{0,}/g,
508
+ /sk-ant-[A-Za-z0-9_-]{16,}/g,
509
+ /sk-or-v1-[A-Za-z0-9]{16,}/g,
510
+ /sk-proj-[A-Za-z0-9_-]{16,}/g,
511
+ /\bsk-[A-Za-z0-9]{20,}/g,
512
+ /AIza[0-9A-Za-z_-]{30,}/g,
513
+ /ghp_[A-Za-z0-9]{30,}/g,
514
+ /github_pat_[A-Za-z0-9_]{30,}/g,
515
+ /AKIA[0-9A-Z]{16}/g,
516
+ /xox[baprs]-[A-Za-z0-9-]{10,}/g,
517
+ /\bsk_(?:live|test)_[0-9A-Za-z]{16,}\b/g,
518
+ /\bBearer\s+[A-Za-z0-9._-]{8,}/gi,
519
+ ];
520
+
521
+ // Replace known secret shapes inside a string value with [REDACTED].
522
+ // Complementary to the key-name check in scrubReportContext — this catches
523
+ // secrets that live inside values (e.g. an API key pasted into HTML source).
524
+ function scrubSecretValues(str) {
525
+ if (typeof str !== "string") return str;
526
+ let out = str;
527
+ for (const re of REPORT_SECRET_VALUE_PATTERNS) {
528
+ out = out.replace(re, "[REDACTED]");
529
+ }
530
+ out = out.replace(/([a-zA-Z][a-zA-Z0-9+.\-]*:\/\/)[^\s:@/]+:[^\s:@/]+@/g, "$1[REDACTED]@");
531
+ return out;
532
+ }
533
+
534
+ // Client-side scrub of the report context. Two layers:
535
+ // 1. Key-name check: values under secret-looking KEYS are fully redacted.
536
+ // 2. Value scan: string values in the tree (up to depth 5) are scanned for known
537
+ // secret shapes (API keys, Bearer tokens) and matches are replaced.
538
+ // Bounded depth so a pathological object can't loop.
488
539
  export function scrubReportContext(obj, depth = 0) {
489
- if (depth > 5 || obj === null || typeof obj !== "object") return obj;
540
+ if (depth > 5 || obj === null || typeof obj !== "object") {
541
+ // Leaf value — scrub known secret shapes from strings.
542
+ return typeof obj === "string" ? scrubSecretValues(obj) : obj;
543
+ }
490
544
  if (Array.isArray(obj)) return obj.map((item) => scrubReportContext(item, depth + 1));
491
545
  const out = {};
492
546
  for (const [key, value] of Object.entries(obj)) {
@@ -575,11 +629,11 @@ export async function runReport(
575
629
  // category: default "mcp" (or the failing tool name the agent passes).
576
630
  category: typeof category === "string" && category.trim() ? category.trim() : "mcp",
577
631
  app: "mcp",
578
- message: summary.slice(0, 5000),
632
+ message: scrubSecretValues(summary).slice(0, 5000),
579
633
  context: safeContext,
580
634
  // Diagnostic pivots (match the CLI) — only when the agent forwards them.
581
635
  ...(typeof trace_id === "string" && trace_id ? { trace_id } : {}),
582
- ...(typeof failed_step === "string" && failed_step ? { failed_step } : {}),
636
+ ...(typeof failed_step === "string" && failed_step ? { failed_step: scrubSecretValues(failed_step) } : {}),
583
637
  ...(typeof http_status === "number" && Number.isFinite(http_status) ? { http_status } : {}),
584
638
  // Attribution, ALSO top-level (persisted once the handler accepts these keys;
585
639
  // context.origin is the fallback until then).
@@ -750,22 +804,23 @@ export async function runPull(ctx, { claim_token, claim_url, entity_id, grid } =
750
804
  };
751
805
  }
752
806
  const code = data?.error?.code || null;
753
- // ORG_NOT_ACCESSIBLE: the `grid` param has a typo or the user isn't a
754
- // member of that grid. The server message already contains an "Available:"
755
- // hint listing valid grid slugs — surface it so the agent can self-correct.
756
- if (code === "ORG_NOT_ACCESSIBLE") {
807
+ // ORG_NOT_ACCESSIBLE / GRID_NOT_ACCESSIBLE: the `grid` param has a typo or
808
+ // the user isn't a member of that grid. The server message already contains
809
+ // an "Available:" hint listing valid grid slugs — surface it so the agent
810
+ // can self-correct.
811
+ if (isGridNotAccessibleCode(code)) {
757
812
  const serverMsg = data?.error?.message || "The specified grid is not accessible.";
758
813
  const hint = data?.error?.details?.[0]?.hint || "";
759
814
  return {
760
815
  text:
761
816
  `${serverMsg}${hint ? ` ${hint}` : ""} ` +
762
817
  `Check the grid slug for typos and retry grid_pull with the correct \`grid\` parameter.`,
763
- structured: { error: { code: "ORG_NOT_ACCESSIBLE" } },
818
+ structured: { error: { code } },
764
819
  };
765
820
  }
766
- // NO_ACTIVE_ORG: the account has no grid at all — route to in-flow grid
767
- // creation instead of the misleading "no push access" advice.
768
- if (code === "NO_ACTIVE_ORG") {
821
+ // NO_ACTIVE_ORG / NO_ACTIVE_GRID: the account has no grid at all — route to
822
+ // in-flow grid creation instead of the misleading "no push access" advice.
823
+ if (isNoActiveGridCode(code)) {
769
824
  return {
770
825
  text:
771
826
  "The account has no grid yet. Do not send the user to the console — create one from here: " +
@@ -1346,10 +1401,10 @@ export function errorGuidance({ status, code, edition, isEdit, isAnon, signedIn
1346
1401
  : "Your sign-in is missing or expired. Run grid_login, then retry the same grid_plug. Do not offer anonymous publishing as a fix for a failed sign-in; anonymous is only for a user who explicitly asks to publish without attribution.";
1347
1402
  }
1348
1403
  if (status === 403) {
1349
- // NO_ACTIVE_ORG is not a role problem — the account has no grid at all.
1350
- // The generic pull/pickup hint here sent a first-time user in circles
1351
- // (field bug 2026-07-27); route them to in-flow grid creation instead.
1352
- if (code === "NO_ACTIVE_ORG") {
1404
+ // NO_ACTIVE_ORG / NO_ACTIVE_GRID is not a role problem — the account has no
1405
+ // grid at all. The generic pull/pickup hint here sent a first-time user in
1406
+ // circles (field bug 2026-07-27); route them to in-flow grid creation instead.
1407
+ if (isNoActiveGridCode(code)) {
1353
1408
  return "The account has no grid yet. Do not send the user to the console — create one from here: suggest a short slug, confirm it with the user, call grid_create_grid, then re-call grid_plug with grid: <slug>.";
1354
1409
  }
1355
1410
  return "You lack the role to plug this target. To get push access to the SAME entity, use `grid_collab` — it grants permission only and fetches nothing, so run `grid_pull` again once it is granted (if the owner gates access, it becomes a request they approve). To make your own separate copy (a fork), use `grid_pickup`.";