@cloudgrid-io/mcp 0.20.36 → 0.20.38

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudgrid-io/mcp",
3
- "version": "0.20.36",
3
+ "version": "0.20.38",
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": {
@@ -50,7 +50,6 @@
50
50
  "snapshot:corpus": "node scripts/snapshot-corpus.mjs",
51
51
  "build:mcpb": "node scripts/build-mcpb.mjs",
52
52
  "test:widgets": "node test/apps-widgets.test.mjs",
53
- "test:fork": "node test/fork.test.mjs",
54
53
  "test:set-sharing": "node test/set-sharing.test.mjs",
55
54
  "test:check-deploy": "node test/check-deploy.test.mjs",
56
55
  "test:staleness": "node test/staleness.test.mjs",
@@ -18,7 +18,7 @@ Operating rules:
18
18
  15. Editing an existing thing from just its URL (a fresh chat, no prior context — e.g. "change the background to green here <url>"): call grid_get_app_source(url) first. It resolves the entity_id and returns the current HTML plus its kind, single_html, capabilities, and replug_handle — read those to pick the branch:
19
19
  - Single-HTML inspiration you can re-plug (single_html: true and capabilities.replug: true): edit the returned HTML and call grid_plug with target_entity_id (or grid+slug — the replug_handle) to update the SAME URL in place. This works on every edition, including hosted.
20
20
  - Multi-file app or agent (kind is app or agent, or single_html: false): do NOT try to edit it as one inline HTML file. Tell the user it is a multi-file <kind>, give them the entity_id and the source (source_download_url), and explain that rebuilding it needs the local edition (Claude Desktop/Code) or the CLI — the hosted server cannot rebuild a multi-file app.
21
- - Not yours (capabilities.replug: false, reason not_owner): do NOT attempt a re-plug. Offer to fork it into the user's own grid with grid_fork and edit the copy instead.
21
+ - Not yours (capabilities.replug: false, reason not_owner): do NOT attempt a re-plug. Offer to fork it into the user's own grid with grid_pickup and edit the copy instead.
22
22
  16. Help CloudGrid QA. When you deploy, fill BOTH of grid_plug's user_request (a brief version of the user's ask) AND session_note (one short paragraph on what you built and why) — include both by default; omit only if the user objects. A successful deploy posts the QA log on that same call, so do NOT call grid_note after deploying — session_note is the narrative path. Use grid_note only BEFORE a deploy, or in a session that ends without one. Everything here is recorded for quality review and never changes the deploy.
23
23
 
24
24
  17. Auth check before an owned deploy, with an anonymous fallback. grid_start reports signed_in. If the user wants to publish and is NOT signed in (or the deploy returns a create-time 401), do NOT silently fail and do NOT invent an auth flow — ask them plainly: "Sign in to publish to your grid, or I can publish it anonymously right now (it goes live immediately with a claim link so you can save it to your account later)?" If they choose anonymous, re-call grid_plug with anon: true and surface the claim_url + owner_token. This is distinct from rule 6: rule 6 forbids the anon fallback on a SERVER error (5xx) for a signed-in user (it burns quota and downgrades ownership); THIS rule is about not being authenticated at all, where anonymous is a legitimate, offered choice.
@@ -1,4 +1,4 @@
1
- // Direct-API tool internals: grid_deploy (runPlug), grid, claim, report, fork,
1
+ // Direct-API tool internals: grid_plug (runPlug), pull, pickup, report,
2
2
  // download, visibility, and source retrieval.
3
3
  // Extracted verbatim from src/tools.js (refactor: split tools.js into modules).
4
4
 
@@ -51,7 +51,7 @@ export async function fetchUserOrgs(token) {
51
51
  }
52
52
  }
53
53
 
54
- // ── Shared grid disambiguation (grid_deploy) ──────────────────
54
+ // ── Shared grid disambiguation (grid_plug) ──────────────────
55
55
  // The stateless "which grid?" ask on an authed create. Given the caller's token
56
56
  // and a supplied grid, it decides:
57
57
  // - supplied grid matches a membership → { proceed: true, grid }
@@ -162,7 +162,7 @@ function looksLikePath(s) {
162
162
  return /^(~|\.{0,2}\/|[A-Za-z]:[\\/]|\/)/.test(t) || /\.[A-Za-z0-9]{1,8}$/.test(t);
163
163
  }
164
164
 
165
- // Normalize an inline `html` string — grid_deploy's ergonomic single-file publish
165
+ // Normalize an inline `html` string — grid_plug's ergonomic single-file publish
166
166
  // path — into ONE index.html artifact, reusing the same hardening the old drop
167
167
  // verb used (decodeIfBase64Html, the @-path/file-path rejection, the base64
168
168
  // guard, and the small-fragment wrap). Returns { path, buffer, type }. Throws on
@@ -512,7 +512,13 @@ export async function runReport(
512
512
  });
513
513
  }
514
514
 
515
- export async function runClaim(ctx, { claim_token, claim_url, entity_id }) {
515
+ // grid_pull — continue/edit an existing entity IN PLACE (POST /entities/:id/pickup).
516
+ // Like `git clone` of the SAME entity: your next grid_plug (target_entity_id)
517
+ // updates it. Needs push access — you must OWN it or be a COLLABORATOR. A
518
+ // view-only caller is told they can't edit/plug it (make a copy with grid_pickup,
519
+ // or request collaborator access with the CLI `grid collab`). A claim_token also
520
+ // claims an anonymous drop into your account (ownership transfer).
521
+ export async function runPull(ctx, { claim_token, claim_url, entity_id }) {
516
522
  const token = await ctx.getToken();
517
523
  if (!token) {
518
524
  throw new Error("You are not signed in. Run grid_login first, then claim.");
@@ -593,35 +599,67 @@ export async function runClaim(ctx, { claim_token, claim_url, entity_id }) {
593
599
  if (res.status === 409) {
594
600
  ctx.state.lastAnonClaim = null;
595
601
  return {
596
- text: "Nothing to claim — it was already claimed.",
597
- structured: { claimed: 0, urls: [] },
602
+ text: "Already yours — nothing to pull.",
603
+ structured: { owner_is_you: true, can_edit: true },
604
+ };
605
+ }
606
+ const code = data?.error?.code || null;
607
+ // No push access → not an error to throw; tell the user their options.
608
+ if (res.status === 403 || code === "NOT_ALLOWLISTED" || code === "PICKUP_DISABLED" || code === "FORBIDDEN_ROLE") {
609
+ return {
610
+ text:
611
+ `You don't have push access to this entity, so you can't edit or plug it. Two options: ` +
612
+ `make your own separate copy with grid_pickup, or ask the owner for collaborator access with the CLI ` +
613
+ `\`grid collab <entity>\` (rolling out soon) and pull again once granted.`,
614
+ structured: { can_edit: false, owner_is_you: false, access: "view_only" },
598
615
  };
599
616
  }
600
617
  const msg = data?.error?.message || data?.message || raw || `HTTP ${res.status}`;
601
- throw new Error(`Claim failed (HTTP ${res.status}): ${msg}`);
618
+ throw new Error(`Pull failed (HTTP ${res.status}): ${msg}`);
602
619
  }
603
620
 
604
621
  ctx.state.lastAnonClaim = null;
605
- // The entity is authed-owned now — the anon owner token is dead weight (a
606
- // claimed drop can no longer be edited anonymously). Future re-drops of it
607
- // must ride the authed wire.
622
+ // A claimed anon drop is authed-owned now — its anon owner token is dead weight.
608
623
  if (ctx.state.lastDrop?.entity_id === targetId) {
609
624
  ctx.state.lastDrop.owner_token = null;
610
625
  }
611
626
  const url = data?.url || data?.redirect_url || ctx.state.lastDrop?.url || null;
612
- const lines = ["Claimed 1, now yours:"];
613
- lines.push(`${url ?? ""}${data?.new_expires_at ? ` (expires ${data.new_expires_at})` : ""}`.trim());
627
+ const slug = data?.slug || targetId;
628
+ const where = data?.grid ? ` in ${data.grid}` : "";
629
+ // A claim_token transfers ownership to you; otherwise read the pickup contract.
630
+ const ownerIsYou = data?.owner?.is_you === true || Boolean(claimToken);
631
+ const canReplug = data?.capabilities?.replug === true;
632
+ let head, canEdit;
633
+ if (ownerIsYou) {
634
+ head = `${slug}${where} is yours — your next grid_plug (with target_entity_id) updates it in place.`;
635
+ canEdit = true;
636
+ } else if (canReplug) {
637
+ head = `You have collaborator push access to ${slug}${where} — your next grid_plug (target_entity_id) updates the SHARED entity; the team sees the new version and can roll it back.`;
638
+ canEdit = true;
639
+ } else {
640
+ head = `You can view ${slug}${where} but do NOT have push access — you can't edit or plug it. Make your own copy with grid_pickup, or request collaborator access with the CLI \`grid collab ${slug}\` (rolling out soon), then pull again.`;
641
+ canEdit = false;
642
+ }
643
+ const lines = [head];
644
+ if (url) lines.push(`URL: ${url}`);
645
+ if (canEdit && data?.entity_id) {
646
+ lines.push(`Re-plug handle: entity_id=${data.entity_id} — pass it as grid_plug's target_entity_id to update in place.`);
647
+ }
614
648
  return {
615
649
  text: lines.join("\n"),
616
650
  structured: {
617
- claimed: 1,
618
- urls: url ? [url] : [],
651
+ ...(data?.entity_id ? { entity_id: data.entity_id } : {}),
652
+ ...(slug ? { slug } : {}),
653
+ grid: data?.grid ?? null,
654
+ ...(url ? { url } : {}),
655
+ owner_is_you: ownerIsYou,
656
+ can_edit: canEdit,
619
657
  },
620
658
  };
621
659
  }
622
660
 
623
661
 
624
- // ── grid_deploy — the unified create/re-plug verb (spec v2 §3) ──────────────
662
+ // ── grid_plug — the unified create/re-plug verb (spec v2 §3) ──────────────
625
663
 
626
664
  // Total upload budget mirrors the server's multipart cap (100 MB).
627
665
  const PLUG_MAX_TOTAL_BYTES = 100 * 1024 * 1024;
@@ -964,7 +1002,7 @@ export function errorGuidance({ status, code, edition, isEdit, isAnon, signedIn
964
1002
  if (status === 401) {
965
1003
  return isEdit
966
1004
  ? "That did not authorize this entity (wrong entity, expired, or already claimed). Sign in if you own it (grid_login), pass its owner_token for an anonymously-created drop, or omit target_entity_id to create a new entity."
967
- : "Not signed in. Ask the user: sign in (grid_login) to publish to their grid, OR publish anonymously now (re-call grid_deploy with anon: true) - it goes live immediately with a claim_url + owner_token to claim into their account later. Do not silently fail; offer both.";
1005
+ : "Not signed in. Ask the user: sign in (grid_login) to publish to their grid, OR publish anonymously now (re-call grid_plug with anon: true) - it goes live immediately with a claim_url + owner_token to claim into their account later. Do not silently fail; offer both.";
968
1006
  }
969
1007
  if (status === 403) {
970
1008
  return "You lack the role to plug this target. To re-plug someone else's entity, pick it up first (grid_edit_existing_app / grid_claim_anonymous_deploy).";
@@ -1507,11 +1545,20 @@ export async function runPlug(ctx, input, deps = {}) {
1507
1545
  if (looksRuntime) {
1508
1546
  const gridSlug = data.grid ?? grid ?? null;
1509
1547
  const eid = data.entity_id ?? targetEntityId ?? null;
1548
+ // The disk CLI is the reliable route for a multi-file app — BUT it only
1549
+ // works where the CLI is already SIGNED IN: a terminal or Claude Code
1550
+ // where `grid login` was run once. Do NOT tell the agent to `grid login`
1551
+ // inside an ephemeral chat sandbox — that login is a long-lived poll (up
1552
+ // to 5 min) and the sandbox is time-bounded + non-persistent, so it won't
1553
+ // complete or won't survive to the next command (observed live). If this
1554
+ // was inline-only, the fix is to move to a signed-in shell OR ask the
1555
+ // user — never silently drop large files.
1556
+ const replug = eid
1557
+ ? `npx -y @cloudgrid-io/cli plug --existing ${eid}${gridSlug ? ` --grid ${gridSlug}` : ""} --verbose`
1558
+ : "npx -y @cloudgrid-io/cli plug";
1510
1559
  cliSteer =
1511
- "For a multi-file app the local CLI is more reliable — it reads from disk, so lockfiles and binaries can't truncate the way inline copying can. " +
1512
- (eid
1513
- ? `If you have the folder: \`cd <app> && npx -y @cloudgrid-io/cli plug\`. If not, pick it up first: \`npx -y @cloudgrid-io/cli pickup ${gridSlug ? `${gridSlug}/` : ""}<slug> --force\` then \`cd <slug> && npx -y @cloudgrid-io/cli plug\`. Or re-plug directly: \`npx -y @cloudgrid-io/cli plug --existing ${eid}${gridSlug ? ` --grid ${gridSlug}` : ""} --verbose\`.`
1514
- : "Prefer `npx -y @cloudgrid-io/cli plug` from the app folder over inlining files.");
1560
+ "Heads-up: this went in INLINE. For a multi-file app the disk-based CLI is more reliable (lockfiles/binaries can't truncate), but it must run where the CLI is already signed in — a terminal or Claude Code that has done `grid login` once. Do NOT try `grid login` inside a chat sandbox: its login poll is long-lived and the sandbox is ephemeral, so it won't stick. " +
1561
+ `If you had to leave any files out to make this inline deploy safe, tell the user — offer the complete deploy from Claude Code or a terminal (\`${replug}\`), or confirm they accept the reduced version. Do not drop files silently.`;
1515
1562
  }
1516
1563
  }
1517
1564
 
@@ -1613,7 +1660,7 @@ async function authedApiCall(ctx, { method, pathName, body, verb }) {
1613
1660
  const msg = data?.error?.message || data?.message || raw || `HTTP ${res.status}`;
1614
1661
  const codeStr = data?.error?.code || null;
1615
1662
  const err = new Error(`${verb} failed (HTTP ${res.status}${codeStr ? ` ${codeStr}` : ""}): ${msg}`);
1616
- // Expose the structured status/code so callers can branch (e.g. runFork's
1663
+ // Expose the structured status/code so callers can branch (e.g. runPickup's
1617
1664
  // NOT_A_RUNTIME → inspiration-route fallback). Additive: existing callers
1618
1665
  // only read `.message`.
1619
1666
  err.status = res.status;
@@ -1623,63 +1670,13 @@ async function authedApiCall(ctx, { method, pathName, body, verb }) {
1623
1670
  return data;
1624
1671
  }
1625
1672
 
1626
- // Fork: start a NEW entity from an existing source, copy-on-write with lineage.
1627
- // Kind-aware (one fork verb, mirrors the CLI `fork` command): the caller may
1628
- // not know the source's kind, so try the runtime route first and, when the
1629
- // server reports the target is an inspiration (`400 NOT_A_RUNTIME`, emitted by
1630
- // requireEntityAccess before the runtime handler runs), retry the inspiration
1631
- // route. The inspiration remix lands in the caller's active grid, so no
1632
- // `into_org_slug`; `source_version_id` has no meaning for inspirations.
1633
- export async function runFork(ctx, { id, into_org_slug, name, source_version_id }) {
1634
- let data;
1635
- try {
1636
- data = await authedApiCall(ctx, {
1637
- method: "POST",
1638
- pathName: `/api/v2/runtimes/${encodeURIComponent(id)}/fork`,
1639
- body: {
1640
- ...(into_org_slug ? { into_org_slug } : {}),
1641
- ...(name ? { name } : {}),
1642
- ...(source_version_id ? { source_version_id } : {}),
1643
- },
1644
- verb: "Fork",
1645
- });
1646
- } catch (err) {
1647
- if (err && err.code === "NOT_A_RUNTIME") {
1648
- data = await authedApiCall(ctx, {
1649
- method: "POST",
1650
- pathName: `/api/v2/inspirations/${encodeURIComponent(id)}/fork`,
1651
- body: { ...(name ? { name } : {}) },
1652
- verb: "Fork",
1653
- });
1654
- } else {
1655
- throw err;
1656
- }
1657
- }
1658
- const gridSlug = data?.grid_slug ?? data?.org?.slug ?? null;
1659
- const lines = [
1660
- `Forked: ${data?.name ?? id} (entity_id=${data?.entity_id ?? "?"})${gridSlug ? ` in grid ${gridSlug}` : ""}`,
1661
- `Lineage: forked_from=${data?.forked_from ?? "?"}${data?.forked_from_version_id ? ` @ ${data.forked_from_version_id}` : ""}`,
1662
- ];
1663
- return {
1664
- text: lines.join("\n"),
1665
- structured: {
1666
- entity_id: data?.entity_id ?? null,
1667
- name: data?.name ?? null,
1668
- kind: data?.kind ?? null,
1669
- grid_slug: gridSlug,
1670
- forked_from: data?.forked_from ?? null,
1671
- forked_from_version_id: data?.forked_from_version_id ?? null,
1672
- current_version_id: data?.current_version_id ?? null,
1673
- },
1674
- };
1675
- }
1676
-
1677
- // Remix: cross-grid "make my own copy". Unlike fork (same-grid, byte-carrying),
1678
- // remix mints a NEW entity in ANY grid the caller can build in, keeps lineage
1679
- // back to the source, and strips the source's secrets/connection credentials —
1680
- // the remixer supplies their own before plugging. Hits POST
1681
- // /api/v2/runtimes/:id/remix.
1682
- export async function runRemix(ctx, { id, into_org_slug, name, source_version_id }) {
1673
+ // grid_pickup — "make your own copy" (like a git fork). Mints a NEW entity in a
1674
+ // grid you can build in, keeps lineage back to the source, and strips the
1675
+ // source's secrets/connection credentials — you set your own before plugging.
1676
+ // Plugging your copy creates/updates YOUR entity; the original is untouched.
1677
+ // Hits POST /api/v2/runtimes/:id/remix (the copy route). Mirrors the CLI's
1678
+ // `grid pickup`. To edit the ORIGINAL in place, that's grid_pull, not this.
1679
+ export async function runPickup(ctx, { id, into_org_slug, name, source_version_id }) {
1683
1680
  const data = await authedApiCall(ctx, {
1684
1681
  method: "POST",
1685
1682
  pathName: `/api/v2/runtimes/${encodeURIComponent(id)}/remix`,
@@ -1688,12 +1685,13 @@ export async function runRemix(ctx, { id, into_org_slug, name, source_version_id
1688
1685
  ...(name ? { name } : {}),
1689
1686
  ...(source_version_id ? { source_version_id } : {}),
1690
1687
  },
1691
- verb: "Remix",
1688
+ verb: "Pickup",
1692
1689
  });
1693
1690
  const gridSlug = data?.grid_slug ?? data?.org?.slug ?? null;
1694
1691
  const lines = [
1695
- `Remixed: ${data?.name ?? id} (entity_id=${data?.entity_id ?? "?"})${gridSlug ? ` in grid ${gridSlug}` : ""}`,
1696
- `Lineage: forked_from=${data?.forked_from ?? "?"}. Secrets were not copied — set your own before you plug.`,
1692
+ `Picked up a copy: ${data?.name ?? id} (entity_id=${data?.entity_id ?? "?"})${gridSlug ? ` in grid ${gridSlug}` : ""}.`,
1693
+ "This is a NEW entity — plug it (with this entity_id as target_entity_id) to create/update YOUR copy; the original is untouched.",
1694
+ `Lineage kept (forked_from=${data?.forked_from ?? "?"}). The source's secrets were NOT copied — set your own before you plug.`,
1697
1695
  ];
1698
1696
  return {
1699
1697
  text: lines.join("\n"),
@@ -1709,30 +1707,6 @@ export async function runRemix(ctx, { id, into_org_slug, name, source_version_id
1709
1707
  };
1710
1708
  }
1711
1709
 
1712
- // Download: signed, time-limited (15-minute) source-bundle URLs. No entity is
1713
- // created and no registry state changes.
1714
- export async function runDownload(ctx, { id, version }) {
1715
- const qs = version ? `?version=${encodeURIComponent(version)}` : "";
1716
- const data = await authedApiCall(ctx, {
1717
- method: "GET",
1718
- pathName: `/api/v2/runtimes/${encodeURIComponent(id)}/source${qs}`,
1719
- verb: "Download",
1720
- });
1721
- const services = data?.services && typeof data.services === "object" ? data.services : {};
1722
- const lines = [`Source bundle URLs for ${data?.name ?? id} (valid ~15 minutes):`];
1723
- for (const [svc, u] of Object.entries(services)) lines.push(` ${svc}: ${u}`);
1724
- if (Object.keys(services).length === 0) lines.push(" (no services returned)");
1725
- return {
1726
- text: lines.join("\n"),
1727
- structured: {
1728
- entity_id: data?.entity_id ?? null,
1729
- name: data?.name ?? null,
1730
- services,
1731
- domain: data?.domain ?? null,
1732
- },
1733
- };
1734
- }
1735
-
1736
1710
  // Change an entity's visibility — inspiration OR runtime app/agent. Authed,
1737
1711
  // direct API — works on the hosted edition where the CLI-wrapping share tool is
1738
1712
  // unavailable. Kind-aware routing (see below). Defaults to the drop made in this
@@ -1796,7 +1770,7 @@ export async function runVisibility(ctx, { target, visibility, kind, org }) {
1796
1770
  // Route order. `space` is inspiration-only; a known runtime kind goes straight
1797
1771
  // to the entities route; a known inspiration goes straight to the inspiration
1798
1772
  // route; an unknown kind tries the runtime route first and falls back to the
1799
- // inspiration route on a not-found (mirrors runFork).
1773
+ // inspiration route on a not-found (mirrors runPickup).
1800
1774
  let order;
1801
1775
  if (vis === "space") order = ["inspiration"];
1802
1776
  else if (isRuntimeKind) order = ["runtime"];
@@ -1966,7 +1940,7 @@ export async function runCheckDeploy(ctx, { poll_url, grid } = {}) {
1966
1940
  });
1967
1941
  if (!target) {
1968
1942
  throw new Error(
1969
- "No build to check. Pass poll_url from a grid_deploy result, or deploy something first in this session. (Instant inspiration deploys are live on return and have no build to poll.)",
1943
+ "No build to check. Pass poll_url from a grid_plug result, or deploy something first in this session. (Instant inspiration deploys are live on return and have no build to poll.)",
1970
1944
  );
1971
1945
  }
1972
1946
  const verdict = await fetchDeployTrace(ctx, { pollUrl: target, grid: gridSlug });
@@ -2078,7 +2052,7 @@ async function readInspirationSourceViaApi(ctx, { entityId, slug, grid }) {
2078
2052
 
2079
2053
  // Resolve a public URL / grid+slug to a REAL entity_id (+ edition metadata) via
2080
2054
  // the deployed pickup contract (POST /api/v2/entities/:target/pickup — the same
2081
- // endpoint runClaim uses). Used by runSource when a bare URL arrives with no
2055
+ // endpoint runPull uses). Used by runSource when a bare URL arrives with no
2082
2056
  // session entity_id (a fresh chat): the contract returns
2083
2057
  // { entity_id, slug, grid, kind, single_html, capabilities, replug_handle,
2084
2058
  // source_download_url, ... }
@@ -20,11 +20,9 @@ import {
20
20
  resolveGridOrAsk,
21
21
  detectSourceManifest,
22
22
  runReport,
23
- runClaim,
23
+ runPull,
24
24
  runPlug,
25
- runFork,
26
- runRemix,
27
- runDownload,
25
+ runPickup,
28
26
  runVisibility,
29
27
  runSource,
30
28
  runCheckDeploy,
@@ -120,28 +118,68 @@ export function registerTools(server, ctx) {
120
118
 
121
119
  // ── Direct-API tools (both editions) ──────────────────────────────────────
122
120
 
123
- // Pickup — both editions. Adopts an entity into the signed-in account's active
124
- // grid (ownership transfer via POST /api/v2/entities/:id/pickup). Pass an
125
- // anonymous drop's owner token as `claim_token` to take over something you
126
- // dropped anonymously — that path replaces the old standalone "claim" verb.
121
+ // ── Copy / adopt verbs (both editions) — mirror the CLI: ──────────────────
122
+ // grid_pickup = make YOUR OWN COPY (git-fork style) → POST /runtimes/:id/remix
123
+ // grid_pull = continue/edit the SAME entity in place (push access needed)
124
+ // → POST /entities/:id/pickup
125
+ // (The CLI verbs `download`, `fork`, `remix`, `link` were retired; no MCP tool
126
+ // targets them.)
127
+
128
+ // grid_pickup — make your own copy of any app you can see.
127
129
  reg(
128
130
  "grid_pickup",
129
131
  {
130
- description: "Pick up (adopt) an entity into the signed-in account's active grid — an ownership transfer, keeping the same public URL. Use it to take over an entity you have access to, or to keep an anonymous drop you created: pass the drop's `claim_token` and it becomes owned and stops expiring on the anonymous schedule. Requires sign-in (grid_login). Calls the API directly, so it works on every edition. (This is the direct-API adopt; on the local edition grid_edit_existing_app additionally downloads the source and links the folder for editing.)",
132
+ description: "Pick up an app: make your OWN COPY of any app you can see (like a git fork) into a grid you can build in. It mints a NEW entity with lineage back to the source and WITHOUT the source's secrets (set your own before you plug). Plugging your copy creates/updates YOUR entity — the original is never touched. Requires sign-in. To edit the ORIGINAL entity in place (as its owner or a collaborator), use grid_pull instead.",
133
+ inputSchema: {
134
+ id: z.string().describe("The source app to copy: a canonical UUID or <grid-slug>/<entity-slug>."),
135
+ into_org_slug: z.string().optional().describe("Grid to create your copy in. Required only when you belong to more than one grid."),
136
+ name: z.string().optional().describe("Slug for your copy. Omit to derive one from the source."),
137
+ source_version_id: z.string().optional().describe("Copy an older version instead of HEAD, e.g. v_a1b2c3d."),
138
+ },
139
+ outputSchema: {
140
+ entity_id: z.string().nullable().describe("Your copy's entity id — pass as grid_plug's target_entity_id to update it."),
141
+ name: z.string().nullable().describe("Your copy's slug."),
142
+ kind: z.string().nullable().describe("app | agent."),
143
+ grid_slug: z.string().nullable().describe("The grid your copy landed in."),
144
+ forked_from: z.string().nullable().describe("Source entity_id (lineage)."),
145
+ forked_from_version_id: z.string().nullable().describe("Source version, when a specific one was copied."),
146
+ current_version_id: z.string().nullable().describe("Your copy's current version id."),
147
+ },
148
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
149
+ },
150
+ async (input) => {
151
+ try {
152
+ if (!input?.id) return fail("`id` is required (a canonical UUID or <grid-slug>/<entity-slug>).");
153
+ return okResult(await runPickup(ctx, input));
154
+ } catch (err) {
155
+ return fail(err.message);
156
+ }
157
+ },
158
+ );
159
+
160
+ // grid_pull — continue/edit the SAME entity in place (push access required).
161
+ reg(
162
+ "grid_pull",
163
+ {
164
+ description: "Pull an app to continue/edit it IN PLACE — like `git clone` of the SAME entity: your next grid_plug (with its target_entity_id) updates that entity, and the team sees the new version. Requires PUSH ACCESS: you must own it or be a collaborator. If you can only view it, you CANNOT edit or plug it — say so and tell the user they can make their own copy with grid_pickup, or request collaborator access with the CLI `grid collab <entity>` (rolling out soon). Passing an anonymous drop's `claim_token` claims it into your account. Requires sign-in. Calls the API directly (both editions).",
131
165
  inputSchema: {
132
- entity_id: z.string().optional().describe("The entity id to pick up. Defaults to this session's last anonymous drop."),
133
- claim_token: z.string().optional().describe("For an anonymous drop: its owner token (the owner_token in the drop result, also embedded in claim_url). Passing it takes over that drop."),
134
- claim_url: z.string().optional().describe("Alternative to claim_token for an anonymous drop — the claim_url from the drop result; the token is read from it."),
166
+ entity_id: z.string().optional().describe("The entity id to pull. Defaults to this session's last anonymous drop."),
167
+ claim_token: z.string().optional().describe("Anonymous drop only: its owner token — claims that drop into your account (ownership transfer)."),
168
+ claim_url: z.string().optional().describe("Alternative to claim_token for an anonymous drop — the claim_url; the token is read from it."),
135
169
  },
136
170
  outputSchema: {
137
- claimed: z.number().describe("Number of entities picked up."),
138
- urls: z.array(z.string()).describe("Public URLs of the picked-up entities."),
171
+ entity_id: z.string().optional().describe("The entity you pulled."),
172
+ slug: z.string().optional().describe("Its slug."),
173
+ grid: z.string().nullable().optional().describe("Its grid."),
174
+ url: z.string().optional().describe("Its public URL."),
175
+ owner_is_you: z.boolean().optional().describe("True if you own it."),
176
+ can_edit: z.boolean().optional().describe("True if you can plug/update it (owner or collaborator). False = view-only."),
139
177
  },
140
178
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
141
179
  },
142
180
  async (input) => {
143
181
  try {
144
- return okResult(await runClaim(ctx, input || {}));
182
+ return okResult(await runPull(ctx, input || {}));
145
183
  } catch (err) {
146
184
  return fail(err.message);
147
185
  }
@@ -276,13 +314,17 @@ export function registerTools(server, ctx) {
276
314
  "If you need to edit a page but don't have its HTML, call grid_get_app_source first, then deploy with " +
277
315
  "target_entity_id. " +
278
316
  // Suggestion 1: steer multi-file runtime apps to the CLI up front — inline
279
- // copying truncates large lockfiles/binaries and silently fails the build.
317
+ // copying truncates large lockfiles/binaries and silently fails the build —
318
+ // but only where the CLI is actually signed in (see the login caveat).
280
319
  "RELIABILITY: `html` and `artifact_files` are sent INLINE (copied through this call), which can " +
281
320
  "truncate large files (lockfiles, binaries) on a multi-file app. For a real framework (Next.js, etc.), " +
282
- "a lockfile, or binary assets — and for re-plugging an existing runtime app — prefer the local CLI, " +
283
- "which reads from disk: `npx -y @cloudgrid-io/cli plug` in the app folder (pick it up first with " +
284
- "`npx -y @cloudgrid-io/cli pickup <grid>/<slug> --force` if you don't have it locally). Reserve inline " +
285
- "for a single page or a few small text files. (CloudGrid calls this operation 'plug'.)",
321
+ "a lockfile, or binary assets — and for re-plugging an existing runtime app — the disk-based CLI is more " +
322
+ "reliable: `npx -y @cloudgrid-io/cli plug` in the app folder. BUT it only works where the CLI is already " +
323
+ "signed in — a terminal or Claude Code that ran `grid login` once. Do NOT attempt `grid login` inside a " +
324
+ "chat sandbox: its login is a long poll and the sandbox is ephemeral, so it will not complete. If you can " +
325
+ "only deploy inline here and would have to omit large files, do NOT drop them silently — tell the user " +
326
+ "and offer to run the full deploy from Claude Code or a terminal (same entity_id). Reserve inline for a " +
327
+ "single page or a few small text files. (CloudGrid calls this operation 'plug'.)",
286
328
  inputSchema: plugInputSchema,
287
329
  outputSchema: {
288
330
  entity_id: z.string().optional().describe("Globally unique — pass back as target_entity_id to re-plug."),
@@ -428,111 +470,6 @@ export function registerTools(server, ctx) {
428
470
  // "publish"/"ship"/"make live" live in grid_plug's description, so
429
471
  // deploy-intent routing lands there without a second listed name.
430
472
 
431
- // ── grid_fork / grid_remix / grid_download — direct-API copy/source verbs ──────
432
- // fork = same-grid byte-carrying copy; remix = cross-grid copy with secrets
433
- // stripped; download = signed source-bundle URLs. All direct-API (both editions).
434
- reg(
435
- "grid_fork",
436
- {
437
- description:
438
- "Fork an entity: start a NEW entity from an existing one (copy-on-write, lineage recorded), carrying " +
439
- "the source bytes. Lands in the source's home grid by default; cross-grid only for system templates " +
440
- "or forkable:'public' sources — for a general cross-grid copy use grid_remix instead. Requires sign-in. " +
441
- "Kind-aware: forks a runtime (app/agent) OR an inspiration — tries POST /api/v2/runtimes/:id/fork and " +
442
- "falls back to /api/v2/inspirations/:id/fork.",
443
- inputSchema: {
444
- id: z.string().describe("The source entity: a canonical UUID or <grid-slug>/<entity-slug>."),
445
- into_org_slug: z.string().optional().describe("Destination grid slug. Required only when you belong to more than one grid."),
446
- name: z.string().optional().describe("Slug for the new entity. Omit to derive one from the source."),
447
- source_version_id: z.string().optional().describe("Fork an older version instead of HEAD, e.g. v_a1b2c3d."),
448
- },
449
- outputSchema: {
450
- entity_id: z.string().nullable().describe("The new entity's id."),
451
- name: z.string().nullable().describe("The new entity's slug."),
452
- kind: z.string().nullable().describe("app | agent | inspiration."),
453
- grid_slug: z.string().nullable().describe("The grid the fork landed in."),
454
- forked_from: z.string().nullable().describe("Source entity_id."),
455
- forked_from_version_id: z.string().nullable().describe("Source version, when a specific one was forked."),
456
- current_version_id: z.string().nullable().describe("The fork's current version id."),
457
- },
458
- annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
459
- },
460
- async (input) => {
461
- try {
462
- if (!input?.id) return fail("`id` is required (a canonical UUID or <grid-slug>/<entity-slug>).");
463
- return okResult(await runFork(ctx, input));
464
- } catch (err) {
465
- return fail(err.message);
466
- }
467
- },
468
- );
469
-
470
- reg(
471
- "grid_remix",
472
- {
473
- description:
474
- "Remix an app: make your own copy of any app you can SEE (link/public/authenticated, or a grid you " +
475
- "belong to) into ANY grid you can build in — cross-grid is allowed. Mints a NEW entity with lineage " +
476
- "back to the source, WITHOUT the source's secrets or connection credentials (set your own before you " +
477
- "plug). Distinct from grid_fork, which is same-grid and byte-carrying. Requires sign-in. Calls POST " +
478
- "/api/v2/runtimes/:id/remix.",
479
- inputSchema: {
480
- id: z.string().describe("The source app: a canonical UUID or <grid-slug>/<entity-slug>."),
481
- into_org_slug: z.string().optional().describe("Grid to create your copy in. Required only when you belong to more than one grid."),
482
- name: z.string().optional().describe("Slug for your copy. Omit to derive one from the source."),
483
- source_version_id: z.string().optional().describe("Remix an older version instead of HEAD, e.g. v_a1b2c3d."),
484
- },
485
- outputSchema: {
486
- entity_id: z.string().nullable().describe("Your copy's entity id."),
487
- name: z.string().nullable().describe("Your copy's slug."),
488
- kind: z.string().nullable().describe("app | agent."),
489
- grid_slug: z.string().nullable().describe("The grid your copy landed in."),
490
- forked_from: z.string().nullable().describe("Source entity_id."),
491
- forked_from_version_id: z.string().nullable().describe("Source version, when a specific one was remixed."),
492
- current_version_id: z.string().nullable().describe("Your copy's current version id."),
493
- },
494
- annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
495
- },
496
- async (input) => {
497
- try {
498
- if (!input?.id) return fail("`id` is required (a canonical UUID or <grid-slug>/<entity-slug>).");
499
- return okResult(await runRemix(ctx, input));
500
- } catch (err) {
501
- return fail(err.message);
502
- }
503
- },
504
- );
505
-
506
- reg(
507
- "grid_download",
508
- {
509
- description:
510
- "Download an entity's source: the bundle last deployed for a runtime, as one signed, time-limited " +
511
- "(15-minute) read URL per service tarball. No entity is created and no registry state changes (unlike " +
512
- "grid_pickup/grid_fork/grid_remix, which adopt or copy). Requires sign-in. Calls GET " +
513
- "/api/v2/runtimes/:id/source directly.",
514
- inputSchema: {
515
- id: z.string().describe("The runtime to download: a canonical UUID or <grid-slug>/<entity-slug>."),
516
- version: z.string().optional().describe("Download an older version's bundle instead of HEAD, e.g. v_a1b2c3d."),
517
- },
518
- outputSchema: {
519
- entity_id: z.string().nullable().describe("The runtime's entity id."),
520
- name: z.string().nullable().describe("The runtime's slug."),
521
- services: z.record(z.string()).describe("Service name → signed read URL (valid ~15 minutes)."),
522
- domain: z.string().nullable().optional().describe("The runtime's domain, if any."),
523
- },
524
- annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
525
- },
526
- async (input) => {
527
- try {
528
- if (!input?.id) return fail("`id` is required (a canonical UUID or <grid-slug>/<entity-slug>).");
529
- return okResult(await runDownload(ctx, input));
530
- } catch (err) {
531
- return fail(err.message);
532
- }
533
- },
534
- );
535
-
536
473
  // Source — both editions. Fetches a drop's current deployed HTML inline so an
537
474
  // agent that lost the content can edit it and re-plug in place.
538
475
  reg(
@@ -546,9 +483,9 @@ export function registerTools(server, ctx) {
546
483
  "entity_id via the pickup contract and also returns the entity's kind, single_html, capabilities " +
547
484
  "(replug/fork), and replug_handle — read those to decide whether to edit in place (single-HTML + " +
548
485
  "capabilities.replug), fall back for a multi-file app/agent (use source_download_url + the local " +
549
- "edition/CLI), or offer a fork when it isn't yours. For multi-file or runtime (app/agent) source " +
550
- "bundles, use grid_download (signed tarball URLs). Reads the HTML from the API server-side; " +
551
- "read-only, creates nothing.",
486
+ "edition/CLI), or offer a copy (grid_pickup) when it isn't yours. For a multi-file or runtime " +
487
+ "(app/agent) source, use source_download_url, or grid_pull to get the files and link the folder. " +
488
+ "Reads the HTML from the API server-side; read-only, creates nothing.",
552
489
  inputSchema: {
553
490
  entity_id: z.string().optional().describe("The drop's durable id. Defaults to this session's last drop."),
554
491
  url: z.string().optional().describe("The public URL of the drop (e.g. https://<grid>.cloudgrid.io/<slug>). Defaults to this session's last drop URL."),
@@ -1126,23 +1063,27 @@ export function registerTools(server, ctx) {
1126
1063
  cliTool(({ grid }) => ["describe", "grid", grid, "--json"]),
1127
1064
  );
1128
1065
 
1066
+ // Local (CLI-wrapping) counterpart of the direct-API grid_pull: downloads the
1067
+ // source + cloudgrid.yaml and links the folder, so the next `grid plug` updates
1068
+ // the SAME entity in place. Wraps `grid pull` (NOT `grid pickup` — pickup now
1069
+ // makes a separate copy). Push access required; a view-only entity can't be
1070
+ // pulled — use grid_pickup to copy it, or `grid collab` to request access.
1129
1071
  regTool(
1130
1072
  "grid_edit_existing_app",
1131
- "Download an entity's source + cloudgrid.yaml and link the folder to it. Overwrites with --force. Wraps `grid pickup`.",
1073
+ "Continue/edit an EXISTING entity locally: download its source + cloudgrid.yaml and link the folder so your next `grid plug` updates it IN PLACE. Requires push access (owner or collaborator). Wraps `grid pull`. To make your own separate copy instead, use grid_pickup.",
1132
1074
  {
1133
- name: z.string().describe("Entity slug or id to pick up."),
1134
- target_dir: z.string().optional().describe("Directory to pick up into (relative to cwd). Defaults to the entity name."),
1075
+ name: z.string().describe("Entity slug or id to pull."),
1076
+ target_dir: z.string().optional().describe("Directory to pull into (relative to cwd). Defaults to the entity name."),
1135
1077
  grid: z.string().optional().describe("Grid to resolve the entity in. Defaults to the active grid."),
1136
- version: z.string().optional().describe("Pick up an older version's source instead of HEAD."),
1137
- force: z.boolean().optional().describe("Pick up into a non-empty directory."),
1078
+ version: z.string().optional().describe("Pull an older version's source instead of HEAD."),
1079
+ force: z.boolean().optional().describe("Pull into a non-empty directory."),
1138
1080
  no_bind: z.boolean().optional().describe("Download source only — skip cloudgrid.yaml and the link."),
1139
1081
  cwd: z.string().optional().describe("Working directory the CLI runs in. The download lands here; pass an explicit, writable directory."),
1140
1082
  },
1141
1083
  { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
1142
1084
  cliTool(({ name, target_dir, grid, version, force, no_bind }) => {
1143
- const args = ["pickup", name];
1085
+ const args = ["pull", name];
1144
1086
  if (target_dir) args.push(target_dir);
1145
- // CLI 0.12's pickup exposes `--grid <slug>` (was legacy `--org`).
1146
1087
  if (grid) args.push("--grid", grid);
1147
1088
  if (version) args.push("--version", version);
1148
1089
  if (force) args.push("--force");
package/src/tools.js CHANGED
@@ -33,7 +33,8 @@ export {
33
33
  parseCliPlugUrl,
34
34
  plugViaCliFallback,
35
35
  runPlug,
36
- runFork,
36
+ runPickup,
37
+ runPull,
37
38
  runSource,
38
39
  runVisibility,
39
40
  runCheckDeploy,