@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 +1 -2
- package/src/corpus/playbook.md +1 -1
- package/src/tools/deploy.js +82 -108
- package/src/tools/register.js +76 -135
- package/src/tools.js +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudgrid-io/mcp",
|
|
3
|
-
"version": "0.20.
|
|
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",
|
package/src/corpus/playbook.md
CHANGED
|
@@ -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
|
|
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.
|
package/src/tools/deploy.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Direct-API tool internals:
|
|
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 (
|
|
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 —
|
|
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
|
-
|
|
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: "
|
|
597
|
-
structured: {
|
|
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(`
|
|
618
|
+
throw new Error(`Pull failed (HTTP ${res.status}): ${msg}`);
|
|
602
619
|
}
|
|
603
620
|
|
|
604
621
|
ctx.state.lastAnonClaim = null;
|
|
605
|
-
//
|
|
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
|
|
613
|
-
|
|
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
|
-
|
|
618
|
-
|
|
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
|
-
// ──
|
|
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
|
|
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
|
|
1512
|
-
(
|
|
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.
|
|
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
|
-
//
|
|
1627
|
-
//
|
|
1628
|
-
//
|
|
1629
|
-
//
|
|
1630
|
-
//
|
|
1631
|
-
//
|
|
1632
|
-
|
|
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: "
|
|
1688
|
+
verb: "Pickup",
|
|
1692
1689
|
});
|
|
1693
1690
|
const gridSlug = data?.grid_slug ?? data?.org?.slug ?? null;
|
|
1694
1691
|
const lines = [
|
|
1695
|
-
`
|
|
1696
|
-
|
|
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
|
|
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
|
|
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
|
|
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, ... }
|
package/src/tools/register.js
CHANGED
|
@@ -20,11 +20,9 @@ import {
|
|
|
20
20
|
resolveGridOrAsk,
|
|
21
21
|
detectSourceManifest,
|
|
22
22
|
runReport,
|
|
23
|
-
|
|
23
|
+
runPull,
|
|
24
24
|
runPlug,
|
|
25
|
-
|
|
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
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
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
|
|
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
|
|
133
|
-
claim_token: z.string().optional().describe("
|
|
134
|
-
claim_url: z.string().optional().describe("Alternative to claim_token for an anonymous drop — the claim_url
|
|
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
|
-
|
|
138
|
-
|
|
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
|
|
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 —
|
|
283
|
-
"
|
|
284
|
-
"
|
|
285
|
-
"
|
|
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
|
|
550
|
-
"
|
|
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
|
-
"
|
|
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
|
|
1134
|
-
target_dir: z.string().optional().describe("Directory to
|
|
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("
|
|
1137
|
-
force: z.boolean().optional().describe("
|
|
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 = ["
|
|
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");
|