@cloudgrid-io/mcp 0.20.48 → 0.20.50

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
@@ -6,17 +6,17 @@ It ships in two editions from one codebase:
6
6
 
7
7
  - **Local (stdio)** — runs on your machine, full toolset including the CLI-wrapping
8
8
  tools. This README covers it. For Claude Code, Cursor, Claude Desktop.
9
- - **Web (hosted HTTP)** — a light, CLI-free toolset (drop, claim, login,
10
- visibility) for web clients like claude.ai. See [REMOTE.md](REMOTE.md).
9
+ - **Web (hosted HTTP)** — a lighter toolset (14 shared direct-API tools, no CLI
10
+ wrappers) for web clients like claude.ai. See [REMOTE.md](REMOTE.md).
11
11
 
12
12
  The local edition wraps the `grid` CLI for authenticated operations (the CLI
13
13
  handles auth, org context, and error formatting) and calls the API directly for the
14
- drop, claim, and login tools.
14
+ plug, login, and visibility tools.
15
15
 
16
16
  ## CLI compatibility
17
17
 
18
18
  The MCP uses an installed CLI only when it meets the platform floor
19
- (`MIN_CLI_VERSION`, currently 0.15.2 - kept in lockstep with the API's live
19
+ (`MIN_CLI_VERSION`, currently 0.15.14 — kept in lockstep with the API's live
20
20
  `cli_compat` floor, which rejects older CLIs with HTTP 426). Below the floor it
21
21
  skips the local/global CLI and lazily fetches `@cloudgrid-io/cli@latest` via
22
22
  npx instead. A CI drift guard (`npm run test:drift-guard`) asserts every
@@ -50,13 +50,27 @@ npx -y @cloudgrid-io/mcp
50
50
  Or from a clone:
51
51
 
52
52
  ```
53
- cd mcp-server
53
+ cd mcp
54
54
  npm install
55
55
  npm start
56
56
  ```
57
57
 
58
58
  It speaks MCP over stdio. Point any MCP client at the `cloudgrid-mcp` command.
59
59
 
60
+ ### One CloudGrid server per client
61
+
62
+ Run EITHER this local (stdio) server — standalone or via the Claude Code
63
+ plugin, which bundles it — OR a hosted connector (`mcp.cloudgrid.io` /
64
+ `mcp-connected.cloudgrid.io`) in a given client. Never both: each `grid_*`
65
+ tool appears twice, the two servers hold separate sign-in state (the local
66
+ edition shares the CLI's `~/.cloudgrid/credentials`; the connector holds an
67
+ OAuth session), and they can disagree about whether you are signed in — the
68
+ model may then act on whichever answered last. How to tell: duplicate
69
+ `grid_*` entries in the client's tool list, or two CloudGrid servers in its
70
+ MCP/connector settings. Which to keep: the local edition wherever you build
71
+ runtime apps from folders (it has `path` + the CLI); the connector in
72
+ chat-only clients where you publish single pages.
73
+
60
74
  ### QA session log (optional)
61
75
 
62
76
  Set `CLOUDGRID_QA_SLACK_WEBHOOK` to an **internal / private** Slack incoming-webhook
@@ -67,24 +81,29 @@ MUST be internal. Unset → the feature is fully dark (nothing captured, nothing
67
81
  posted). `CLOUDGRID_QA_IDLE_MS` overrides the abandoned-idle window (default
68
82
  900000 = 15 min).
69
83
 
84
+ <!-- gen:tools -->
70
85
  ## Tools
71
86
 
87
+ 36 tools registered (14 shared across both editions, 22 local-only). No deprecated aliases. MIN_CLI_VERSION: 0.15.14.
88
+
72
89
  ### Direct-API tools (both editions)
73
90
 
74
- | Tool | Wraps | Notes |
91
+ | Tool | Wraps | Summary |
75
92
  |---|---|---|
76
- | `grid_deploy` | `POST /api/v2/plug` | THE deploy/publish verb (CloudGrid calls it "plug"): create a new entity, or update the SAME entity in place with `target_entity_id` (same URL). Source via inline `html` (single page), local `path` (local edition), or inline `artifact_files` (both). Anonymous drops return an `entity_id` + `owner_token` re-plug/claim handle. |
77
- | `grid_claim_anonymous_deploy` | `POST /api/v2/entities/:id/pickup` | Claim an anonymous drop into the signed-in account (the claim token IS the drop's owner token). |
78
- | `grid_copy_app` | `POST /api/v2/runtimes/:id/fork` | Start a new entity from an existing runtime (lineage recorded). Needs sign-in. |
79
- | `grid_download_source` | `GET /api/v2/runtimes/:id/source` | Signed 15-minute source-bundle URLs. Needs sign-in. |
80
- | `grid_get_app_source` | `GET /api/v2/.../source` | Fetch a deployed entity's source (resolves entity_id from a URL). Read-only. |
81
- | `grid_login` | `GET /auth/login` | Start a CLI-free sign-in; returns a URL to open. |
82
- | `grid_login_status` | `GET /auth/status` | Finish the sign-in; saves the token to the shared CLI credentials. |
83
- | `grid_visibility` | `PATCH /api/v2/inspirations/<id>` | Change who can see a drop (private, space, authenticated, org, link). Needs sign-in. |
84
- | `grid_list_grids` | `GET /api/v2/orgs` | List the caller's grids. Read-only. |
85
- | `grid_start` | corpus | Orientation: the playbook + workflow index + live context. Call FIRST. Read-only. |
86
- | `grid_get_template` | corpus | Fetch a workflow / template / example / doc from the bundled corpus. Read-only. (`grid_fetch` is a kept deprecated alias.) |
87
- | `grid_note` / `grid_report` / `grid_feedback` | API | Session notes, consent-gated error reports, feedback feed. |
93
+ | `grid_pickup` | API | 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. |
94
+ | `grid_pull` | API | Pull an app to continue/edit it IN PLACE — like `git clone` of the SAME entity: your next grid_plug (with its target_... |
95
+ | `grid_create_grid` | API | Create a new grid (workspace) for the signed-in user — they become its admin. |
96
+ | `grid_note` | API | Optionally leave a one-paragraph summary of what you built this session and why. |
97
+ | `grid_plug` | API | Plug an app, website, game, or single HTML page into CloudGrid — the live runtime that runs it and provides its infra... |
98
+ | `grid_get_app_source` | API | Retrieve the CURRENT deployed HTML of an inspiration/drop inline as text, so you can edit it and re-plug the SAME URL... |
99
+ | `grid_login` | API | Start a CLI-free CloudGrid sign-in. |
100
+ | `grid_login_status` | API | Finish a sign-in started by grid_login. |
101
+ | `grid_visibility` | API | Change who can see a CloudGrid inspiration OR runtime app/agent. |
102
+ | `grid_check_deploy` | API | Check whether an async runtime-app build has finished and the app is live. |
103
+ | `grid_list_grids` | API | List the signed-in user's grids, each with slug, name, role, and provisioning status. |
104
+ | `grid_start` | API | Orient before building with CloudGrid — the live runtime environment where the user's apps run WITH the infrastructur... |
105
+ | `grid_get_template` | corpus | Load a specific CloudGrid workflow, template, example, rule, or doc by name — deterministic retrieval from the bundle... |
106
+ | `grid_report` | API | Report a genuine CloudGrid failure to the CloudGrid team — ONLY with the user's explicit consent. |
88
107
 
89
108
  The direct-API tools call the platform without the CLI, so they also work in
90
109
  the web edition. `grid_login` writes the same `~/.cloudgrid/credentials` the
@@ -92,40 +111,38 @@ CLI uses, so the two share one identity.
92
111
 
93
112
  ### CLI-wrapping tools (local edition only)
94
113
 
95
- | Tool | Wraps | Notes |
114
+ | Tool | Wraps | Summary |
96
115
  |---|---|---|
97
- | `grid_create_project` | `grid new` | Scaffold an app or agent project; optionally seed a web service. |
98
- | `grid_view_logs` | `grid logs` | Snapshot of recent logs. Does not stream. Read-only. (`grid_logs` is a kept deprecated alias.) |
99
- | `grid_share` | `grid visibility set` | Set visibility, default link. |
100
- | `grid_whoami` | `grid whoami` | Show the signed-in user and active grid. Read-only. |
116
+ | `grid_create_project` | `grid new` | Scaffold a new CloudGrid app or agent folder (cloudgrid.yaml + a web service), optionally pre-declaring resources. |
117
+ | `grid_view_logs` | `grid logs` | Tail recent logs for an entity. |
118
+ | `grid_share` | `grid visibility set` | Set an entity's visibility and print its URL. |
119
+ | `grid_feedback` | `grid feedback list` | List recent feedback events for the active grid. |
120
+ | `grid_whoami` | `grid whoami` | Show the signed-in user and active grid. |
101
121
  | `grid_switch_grid` | `grid use` | Switch the active grid. |
102
- | `grid_logout` | `grid logout` | Sign out and clear local credentials. Destructive. |
103
- | `grid_status` | `grid status` | Grid dashboard or entity detail. Read-only. |
104
- | `grid_info` | `grid info` | Entity metadata. Read-only. |
105
- | `grid_get` | `grid get grids\|entities\|spaces` | List grids, entities, or spaces. Read-only. |
106
- | `grid_describe_grid` | `grid describe grid <slug>` | Grid detail. Read-only. |
107
- | `grid_edit_existing_app` | `grid pickup <name>` | Download an entity's source and bind the folder. |
108
- | `grid_rename` | `grid rename` | Rename an entity's display name. |
109
- | `grid_take_offline` | `grid unplug` | Take an entity off the grid. Destructive; requires confirm. |
110
- | `grid_delete` | `grid delete entity` | Archive an inspiration. Destructive; requires confirm. |
111
- | `grid_rollback_deploy` | `grid rollback` | Rollback to a previous version. |
112
- | `grid_list_versions` | `grid versions` | List published versions. Read-only. |
113
- | `grid_set_env` | `grid env` | Get, set, or list environment variables. |
114
- | `grid_set_secret` | `grid secrets` | Set or list secret names. Never returns secret values. |
115
- | `grid_scaffold` | `grid scaffold` | Generate starter files. |
116
- | `grid_diagnose` | `grid doctor` | Run local diagnostics. Read-only. |
117
- | `grid_get_url` | `grid open --print` | Return the public URL. Does not open a browser. Read-only. |
122
+ | `grid_logout` | `grid logout` | Sign out and clear local credentials. |
123
+ | `grid_status` | `grid status` | Org dashboard, entity detail, or deploy snapshot. |
124
+ | `grid_info` | `grid info` | Show metadata for a CloudGrid entity. |
125
+ | `grid_get` | `grid get <grids|entities|spaces> --json` | List CloudGrid grids, entities, or spaces. |
126
+ | `grid_describe_grid` | `grid describe grid <slug> --json` | Show a grid's detail: role, members, spaces, tier, wildcard-TLS state. |
127
+ | `grid_edit_existing_app` | `grid pull` | Continue/edit an EXISTING entity locally: download its source + cloudgrid.yaml and link the folder so your next `grid... |
128
+ | `grid_rename` | `grid rename` | Rename a CloudGrid entity's display name (slug stays the same). |
129
+ | `grid_take_offline` | `grid unplug` | Take an entity off the grid. |
130
+ | `grid_delete` | `grid delete entity` | Archive a CloudGrid inspiration. |
131
+ | `grid_rollback_deploy` | `grid rollback` | Rollback an entity to a previous version. |
132
+ | `grid_list_versions` | `grid versions` | List published versions for an entity. |
133
+ | `grid_set_env` | `grid env` | Manage environment variables for an entity. |
134
+ | `grid_set_secret` | `grid secrets` | Set or list secret names for an entity. |
135
+ | `grid_scaffold` | `grid scaffold` | Scaffold service folders declared in cloudgrid.yaml (idempotent). |
136
+ | `grid_diagnose` | `grid doctor` | Run CloudGrid diagnostics on the local environment. |
137
+ | `grid_get_url` | `grid open --print` | Return the public URL for an entity. |
138
+
139
+ <!-- /gen:tools -->
118
140
 
119
141
  `grid_share` and `grid_visibility` overlap on purpose: `grid_share`
120
142
  wraps the CLI and defaults to `link`; `grid_visibility` is direct API, takes an
121
- explicit scope, and defaults its target to the session's last drop -- it is the one
143
+ explicit scope, and defaults its target to the session's last drop — it is the one
122
144
  the web edition gets.
123
145
 
124
- 37 tool names are advertised in total: 35 canonical + 2 kept deprecated
125
- aliases (`grid_fetch`, `grid_logs`). The 0.20.8 alias diet dropped the other 16
126
- legacy aliases. Per-tool internals (signature, sketch, behavior):
127
- [docs/TOOL-INTERNALS.md](docs/TOOL-INTERNALS.md).
128
-
129
146
  All tools carry MCP annotations (`readOnlyHint`, `destructiveHint`,
130
147
  `openWorldHint`) for clients that support them.
131
148
 
@@ -135,7 +152,7 @@ A smoke test spawns the server with a real MCP client, lists the tools, and call
135
152
  the read-only `grid_feedback` tool end to end:
136
153
 
137
154
  ```
138
- cd mcp-server
155
+ cd mcp
139
156
  npm install
140
157
  npm run smoke
141
158
  ```
@@ -146,5 +163,5 @@ It needs a logged-in CLI on `$PATH`.
146
163
 
147
164
  - Shells out with `execFile` and an argument array, so there is no shell and no
148
165
  injection surface.
149
- - `grid_logs` never uses `--follow`; a streaming call would never return.
166
+ - `grid_view_logs` never uses `--follow`; a streaming call would never return.
150
167
  - Stateless. Each call is one CLI invocation.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudgrid-io/mcp",
3
- "version": "0.20.48",
3
+ "version": "0.20.50",
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": {
package/src/auth.js CHANGED
@@ -25,6 +25,13 @@ const PUBLIC_API_BASE = (
25
25
  process.env.CLOUDGRID_PUBLIC_API_URL || "https://api.cloudgrid.io"
26
26
  ).replace(/\/+$/, "");
27
27
 
28
+ // The public URL of the connected (OAuth) MCP host, used in error guidance that
29
+ // suggests the hosted connector as an alternative. Derived from MCP_PUBLIC_URL
30
+ // (set on the deployment) with a fallback.
31
+ const CONNECTED_MCP_HOST =
32
+ process.env.MCP_PUBLIC_URL?.replace(/\/+$/, "") ||
33
+ "https://mcp-connected.cloudgrid.io";
34
+
28
35
  export function cloudgridHome() {
29
36
  return process.env.CLOUDGRID_HOME || join(homedir(), ".cloudgrid");
30
37
  }
@@ -52,7 +59,7 @@ const CERT_ERROR_GUIDANCE =
52
59
  "- Install the root CA into your OS trust store (macOS Keychain, Windows " +
53
60
  "Certificate Manager, or /usr/local/share/ca-certificates on Linux), or\n" +
54
61
  "- Set NODE_EXTRA_CA_CERTS=/path/to/root-ca.pem before launching the MCP, or\n" +
55
- "- Use the hosted MCP connector: https://mcp-connected.cloudgrid.io/mcp";
62
+ `- Use the hosted MCP connector: ${CONNECTED_MCP_HOST}/mcp`;
56
63
 
57
64
  function throwCertError() {
58
65
  const err = new Error(CERT_ERROR_GUIDANCE);
@@ -82,7 +89,7 @@ export async function pollStatusOnce(code) {
82
89
  throw new Error(
83
90
  `Can't reach api.cloudgrid.io (${cause}). ` +
84
91
  `If you are behind a proxy, set HTTPS_PROXY; or use the hosted connector ` +
85
- `https://mcp-connected.cloudgrid.io/mcp.`,
92
+ `${CONNECTED_MCP_HOST}/mcp.`,
86
93
  );
87
94
  }
88
95
  if (res.status === 404) {
@@ -90,7 +90,7 @@ grid grid
90
90
  | `logs [name]` | Stream entity logs |
91
91
  | `open [name]` | Open the entity URL in the browser |
92
92
  | `info [name]` / `builds [name]` | Entity metadata; recent deploys |
93
- | `visibility set <slug> <mode>` | private \| authenticated \| org \| link |
93
+ | `visibility set <slug> <mode>` | private \| grid \| link — or the axes: `--inside <private\|spaces\|grid> --outside <none\|link\|public>` (+ `--require-signin`, `--space <slug>`) |
94
94
  | `rollback` / `versions` | Roll back to a prior deploy; list/tag minted versions |
95
95
  | `env` / `secrets` | Manage runtime env vars and secrets (secret values are never printed) |
96
96
  | `grid` / `list` | List what's on your hub / org |
@@ -136,8 +136,13 @@ npx -y @cloudgrid-io/mcp
136
136
  ```
137
137
 
138
138
  It wraps the same `grid` CLI and uses the same credentials, so no extra login
139
- is needed. Remote-capable clients can point at `https://mcp.cloudgrid.io/mcp`
140
- instead. See [USAGE.md](USAGE.md) for per-client snippets.
139
+ is needed. Remote-capable clients can point at one of two hosted endpoints:
140
+
141
+ - `https://mcp.cloudgrid.io/mcp` — anonymous-first; no account needed to try it.
142
+ - `https://mcp-connected.cloudgrid.io/mcp` — connected; native OAuth sign-in
143
+ when you add it (for claude.ai org connectors and clients that support OAuth).
144
+
145
+ See [USAGE.md](USAGE.md) for per-client snippets.
141
146
 
142
147
  ### Claude Desktop — one-click install
143
148
 
@@ -7,7 +7,7 @@ Operating rules:
7
7
  3b. No grid yet (first-time signed-in user): grid_plug returns needs_grid_create when the account has no grid, and a plug can fail with 403 NO_ACTIVE_ORG. Do NOT send the user to the console — suggest a short slug (3-40 lowercase letters/digits/hyphens, starting with a letter), confirm it with the user (it is permanent and appears in URLs), call grid_create_grid, then re-call grid_plug with grid: <slug>.
8
8
  3c. A failed runtime build never loses the work: the source was uploaded before the build ran and lives on the entity. Read the build log (grid_check_deploy) and fix; if it cannot be fixed from this session (hosted, no filesystem), hand the user their files — grid_get_app_source returns a source zip link (source_download_url), and `npx -y @cloudgrid-io/cli@latest pull <grid>/<slug>` downloads the project AND links the folder so the next plug updates the SAME entity locally. Offer one of these instead of abandoning the build or starting over.
9
9
  4. Load specifics on demand. Use grid_get_template({kind, name}) to pull the exact workflow, template, or example you need (kind ∈ workflow|template|example|rule|troubleshooting|doc).
10
- 5. Always return the live share URL at the end — that is the deliverable. On a NEW plug, then ASK the user who should be able to open it — private (only you), a space, your grid, anyone with the link, or anyone signed in — and set their choice with grid_visibility. Never pick the visibility silently. On a re-plug/edit of an existing entity, leave its current visibility as-is unless the user asks to change it.
10
+ 5. Always return the live share URL at the end — that is the deliverable. On a NEW plug, then ASK the user who should be able to open it — private (only you), your grid, or anyone with the link (optionally sign-in required, or search-indexed for public findability; selected spaces via inside: spaces) — and set their choice with grid_visibility. Never pick the visibility silently. On a re-plug/edit of an existing entity, leave its current visibility as-is unless the user asks to change it. Note the model: visibility is two axes — inside the grid (private | spaces | grid) and outside it (none | link | public); the old 'authenticated' mode is retired (it equals a sign-in-required link) and 'org' is gone (use grid).
11
11
  6. Brainstorm first (lightly) for a real app, then minimize questions. For a substantial runtime app, take ONE lightweight beat before generating/plugging: confirm the goal + 3-5 core features in a sentence, check for a matching template/recipe (capability-map), and infer the data/runtime needs and STATE them ("I'll add a database so entries are saved"). Keep it to a line or two; never ask a non-technical user infra questions they can't answer. A simple single page skips this and builds immediately. Otherwise use sensible defaults and build; don't front-load setup questions.
12
12
  7. If a signed-in publish fails with a server error, do not fall back to anonymous publishing (it burns the anonymous quota and downgrades ownership); surface the error, use the CLI fallback if offered, or ask the user.
13
13
  8. When signed in and the user has more than one grid, do not assume a target. For a NEW app, establish the grid UP FRONT — grid_start returns the grids; if there is more than one, ask "which grid should this go on?" before you start building, and carry that slug into grid_plug. As a backstop, grid_plug hard-gates this: an authed create with >1 grid and no chosen grid returns needs_grid (the picker) instead of plugging; a create with no auth and no anon returns needs_auth (sign-in vs anonymous) instead of silently publishing anonymously. Relay either ask to the user; never pick silently.
@@ -40,11 +40,17 @@ The command prints the outlet URL. Hand that URL to the user.
40
40
 
41
41
  `grid visibility set <slug> <mode>` accepts:
42
42
 
43
- - `link` — anyone with the URL can open it. Use this for sharing.
43
+ - `link` — anyone with the URL can open it. Use this for sharing. Add
44
+ `--require-signin` for signed-in accounts only, `--indexed` to be findable by
45
+ search engines.
44
46
  - `private` — only the owner.
45
- - `authenticated` — any logged-in user.
46
- - `org` — members of the org.
47
- - `space` — members of the entity's space.
47
+ - `grid` — everyone in the grid.
48
+
49
+ Finer control uses the two axes instead of a mode:
50
+ `grid visibility set <slug> --inside <private|spaces|grid> --outside <none|link|public> [--space <slug>...] [--require-signin]`
51
+ (`--outside public` = anyone, search-indexed. `authenticated` is retired — it
52
+ equals `--inside private --outside link --require-signin`; `org` is gone — use
53
+ `grid`.)
48
54
 
49
55
  Default to `link` when the user says "share" or "make it public". Confirm before
50
56
  using a wider mode than they asked for.
@@ -29,12 +29,20 @@ export const ANON_HTML_MAX_BYTES = 2_000_000;
29
29
  export const AUTHED_HTML_MAX_BYTES = 25_000_000;
30
30
  export const CONSOLE_URL = "https://console.cloudgrid.io/";
31
31
 
32
+ // Display labels for visibility values (two-axis model). The OPTIONS list is what
33
+ // the post-plug ask offers; the LABELS map also keeps legacy keys (org,
34
+ // authenticated, space) so an entity's CURRENT stored value still renders — do
35
+ // not offer those as choices.
36
+ export const VISIBILITY_OPTIONS = ["private", "grid", "link"];
32
37
  export const VISIBILITY_LABELS = {
33
38
  private: "Only you",
34
- org: "Your grid",
35
- authenticated: "Anyone signed in",
36
- space: "A space",
39
+ grid: "Everyone in your grid",
37
40
  link: "Anyone with the link",
41
+ // display-only (legacy stored values / axis renderings):
42
+ org: "Everyone in your grid",
43
+ authenticated: "Anyone with the link, sign-in required",
44
+ space: "Selected spaces",
45
+ public: "Anyone, findable by search",
38
46
  };
39
47
 
40
48
  // ── Widget resources (ChatGPT Apps SDK, web edition only) ────────────────────
@@ -13,6 +13,7 @@ import {
13
13
  AUTHED_HTML_MAX_BYTES,
14
14
  CONSOLE_URL,
15
15
  VISIBILITY_LABELS,
16
+ VISIBILITY_OPTIONS,
16
17
  APPS_WIDGETS_ENABLED,
17
18
  GRID_PICKER_URI,
18
19
  PLUG_UPLOAD_TIMEOUT_MS,
@@ -316,8 +317,8 @@ const GUEST_ORG_SLUG = "guest";
316
317
  // `<slug>--<grid>.cloudgrid.io`), so it must never be the primary source:
317
318
  // - inspiration (HTML drops): path-based at the org apex
318
319
  // https://<grid>.cloudgrid.io/<slug>
319
- // - runtime (app/agent): flat-arch double-dash host
320
- // https://<slug>--<grid>.cloudgrid.io
320
+ // - runtime (app/agent): subdomain
321
+ // https://<slug>.<grid>.cloudgrid.io
321
322
  // Anonymous drops are grid-less in the response (`grid: null`); they live under
322
323
  // the Guest Org, so the apex slug is the constant `guest`.
323
324
  function composePlugUrl(data) {
@@ -326,11 +327,7 @@ function composePlugUrl(data) {
326
327
  const grid = data?.grid || GUEST_ORG_SLUG;
327
328
  const kind = data?.detection?.kind;
328
329
  if (kind === "app" || kind === "agent") {
329
- // Flat-arch host: runtime entities serve at `<slug>--<grid>.cloudgrid.io`
330
- // (double-dash), NOT the legacy nested `<slug>.<grid>` form — the latter
331
- // 404s on every current grid. Still fallback-only; the server's `data.url`
332
- // remains the primary source (resolvePlugUrl).
333
- return `https://${slug}--${grid}.cloudgrid.io`;
330
+ return `https://${slug}.${grid}.cloudgrid.io`;
334
331
  }
335
332
  // inspiration (and any unknown/static kind) — path-based at the org apex.
336
333
  return `https://${grid}.cloudgrid.io/${slug}`;
@@ -1243,6 +1240,15 @@ export async function runPlug(ctx, input, deps = {}) {
1243
1240
  "may accompany a .zip `path` — it becomes the index.html over the archive's assets.",
1244
1241
  );
1245
1242
  }
1243
+ // M3: a create/re-plug with NO source at all is a caller mistake — fail it
1244
+ // here with the source list instead of letting it wander into an obscure
1245
+ // downstream error.
1246
+ if (!hasHtml && !hasArtifacts && !hasPath) {
1247
+ throw new Error(
1248
+ "No source to plug. Pass exactly one of: `html` (a single inline HTML document), " +
1249
+ "`artifact_files` (multiple inline files), or `path` (a local file/folder/zip, local edition only).",
1250
+ );
1251
+ }
1246
1252
  if (ctx.edition === "web" && hasPath) {
1247
1253
  throw new Error(
1248
1254
  "The hosted server cannot read local files — pass the source inline via `html` or `artifact_files`.",
@@ -1750,14 +1756,14 @@ export async function runPlug(ctx, input, deps = {}) {
1750
1756
  const current = typeof data.visibility === "string" ? data.visibility : null;
1751
1757
  structured.console_url = CONSOLE_URL;
1752
1758
  if (current) structured.current_visibility = current;
1753
- structured.visibility_options = Object.entries(VISIBILITY_LABELS).map(([v, l]) => ({ value: v, label: l }));
1759
+ structured.visibility_options = VISIBILITY_OPTIONS.map((v) => ({ value: v, label: VISIBILITY_LABELS[v] }));
1754
1760
  lines.push(`Manage all your apps in your grid: ${CONSOLE_URL}`);
1755
1761
  lines.push(
1756
1762
  `Now ASK the user who should be able to open this${current ? ` (currently ${VISIBILITY_LABELS[current] ?? current})` : ""}, then set their choice with grid_visibility — do not decide it for them. Options: ${
1757
- Object.entries(VISIBILITY_LABELS)
1758
- .map(([v, l]) => `${v} (${l})`)
1763
+ VISIBILITY_OPTIONS
1764
+ .map((v) => `${v} (${VISIBILITY_LABELS[v]})`)
1759
1765
  .join("; ")
1760
- }.`,
1766
+ }. Finer control: a sign-in-required link (require_signin), search-indexed (indexed), or selected spaces (inside: spaces).`,
1761
1767
  );
1762
1768
  }
1763
1769
  return { text: lines.join("\n"), structured };
@@ -1848,7 +1854,7 @@ export async function runPickup(ctx, { id, into_org_slug, name, source_version_i
1848
1854
  // direct API — works on the hosted edition where the CLI-wrapping share tool is
1849
1855
  // unavailable. Kind-aware routing (see below). Defaults to the drop made in this
1850
1856
  // session, so "make it private" needs no ids.
1851
- export async function runVisibility(ctx, { target, visibility, kind, org }) {
1857
+ export async function runVisibility(ctx, { target, visibility, inside, outside, require_signin, spaces, indexed, kind, org }) {
1852
1858
  const token = await ctx.getToken();
1853
1859
  if (!token) {
1854
1860
  throw new Error("Changing visibility needs an owner. Run grid_login first.");
@@ -1865,32 +1871,78 @@ export async function runVisibility(ctx, { target, visibility, kind, org }) {
1865
1871
  headers["X-CloudGrid-Org"] = orgSlug;
1866
1872
  }
1867
1873
 
1868
- // Two visibility surfaces with two vocabularies:
1869
- // - inspirations: PATCH /api/v2/inspirations/:id { private|space|authenticated|org|link }
1870
- // - runtimes: PATCH /api/v2/entities/:id/visibility { private|authenticated|grid|link }
1871
- // Whole-grid visibility is the legacy `org` on inspirations and the canonical
1872
- // `grid` on runtimes. Normalize the caller's value to `grid`, then map per
1873
- // route. `space` exists only on inspirations.
1874
- const vis = visibility === "org" ? "grid" : visibility;
1875
- const runtimeValue = vis; // private | authenticated | grid | link
1876
- const inspirationValue = vis === "grid" ? "org" : vis; // private | space | authenticated | org | link
1874
+ // ── Two-axis visibility model ──────────────────────────────────────
1875
+ // A scope is two independent axes:
1876
+ // inside (share_scope): private | spaces | grid — who in the grid sees it
1877
+ // outside (external_access): none | link | public — reach beyond the grid
1878
+ // require_signin: link only — the link needs a signed-in CloudGrid account
1879
+ // Legacy positional modes (private|grid|link) still ride the { visibility }
1880
+ // body; `authenticated` is RETIRED as a first-class mode and maps to the axis
1881
+ // body it equals (private + link + require_signin); `org` is rejected;
1882
+ // `public` as a mode is an alias of `link` (the axis value `public` is the
1883
+ // indexed one); `space` maps to inside: spaces and needs the `spaces` list.
1884
+ const normSpaces = [...new Set((spaces || []).map((s) => String(s).trim().toLowerCase()).filter(Boolean))];
1885
+ const useAxes = inside !== undefined || outside !== undefined;
1886
+ let body;
1887
+ let requested; // for the result text when the response is sparse
1888
+ if (useAxes) {
1889
+ if (visibility) throw new Error("Pass either `visibility` or the `inside`/`outside` axes, not both.");
1890
+ if (!inside || !outside) {
1891
+ throw new Error("Both axes are required: inside (private|spaces|grid) and outside (none|link|public).");
1892
+ }
1893
+ if (inside === "spaces" && normSpaces.length === 0) {
1894
+ throw new Error("inside: spaces needs at least one space slug in `spaces`.");
1895
+ }
1896
+ if (inside !== "spaces" && normSpaces.length > 0) {
1897
+ throw new Error("`spaces` only applies with inside: spaces.");
1898
+ }
1899
+ if (require_signin === true && outside !== "link") {
1900
+ throw new Error("require_signin only applies when outside is link.");
1901
+ }
1902
+ body = {
1903
+ share_scope: inside,
1904
+ external_access: outside,
1905
+ ...(require_signin === true ? { require_signin: true } : {}),
1906
+ ...(inside === "spaces" ? { visibility_spaces: normSpaces } : {}),
1907
+ };
1908
+ requested = `inside: ${inside}${inside === "spaces" ? ` (${normSpaces.join(", ")})` : ""}, outside: ${outside}${require_signin === true ? " (sign-in required)" : ""}`;
1909
+ } else {
1910
+ let mode = String(visibility || "").trim().toLowerCase();
1911
+ if (!mode) throw new Error("Pass a visibility mode (private | grid | link), or the inside/outside axes.");
1912
+ if (mode === "org") {
1913
+ throw new Error("'org' visibility is deprecated. Use 'grid' (everyone in the grid).");
1914
+ }
1915
+ if (mode === "public") mode = "link"; // alias; `indexed: true` governs search-indexing
1916
+ if (mode === "authenticated") {
1917
+ // Retired as a first-class mode — the axis body it equals.
1918
+ body = { share_scope: "private", external_access: "link", require_signin: true };
1919
+ requested = "link with sign-in required (the retired 'authenticated')";
1920
+ } else if (mode === "space") {
1921
+ if (normSpaces.length === 0) {
1922
+ throw new Error("'space' visibility needs the `spaces` list (which space slugs can see it) — or use inside: spaces with outside: none.");
1923
+ }
1924
+ body = { share_scope: "spaces", external_access: "none", visibility_spaces: normSpaces };
1925
+ requested = `selected spaces (${normSpaces.join(", ")})`;
1926
+ } else if (mode === "private" || mode === "grid" || mode === "link") {
1927
+ body = { visibility: mode };
1928
+ if (mode === "grid" && normSpaces.length > 0) body.visibility_spaces = normSpaces;
1929
+ if (mode === "link") body.link_indexed = indexed === true;
1930
+ requested = mode + (mode === "link" && indexed === true ? " (search-indexed)" : "");
1931
+ } else {
1932
+ throw new Error(`Unknown visibility '${visibility}'. Use private | grid | link (public = link alias), or the inside/outside axes.`);
1933
+ }
1934
+ }
1877
1935
 
1878
1936
  const k = kind || ctx.state.lastDrop?.kind || null;
1879
1937
  const isRuntimeKind = k === "app" || k === "agent";
1880
1938
 
1881
- if (vis === "space" && isRuntimeKind) {
1882
- throw new Error(
1883
- "'space' visibility isn't available for runtime apps/agents. Use grid, authenticated, private, or link.",
1884
- );
1885
- }
1886
-
1887
- const patch = async (pathName, value) => {
1939
+ const patch = async (pathName) => {
1888
1940
  let res;
1889
1941
  try {
1890
1942
  res = await fetch(`${API_BASE}${pathName}`, {
1891
1943
  method: "PATCH",
1892
1944
  headers,
1893
- body: JSON.stringify({ visibility: value }),
1945
+ body: JSON.stringify(body),
1894
1946
  });
1895
1947
  } catch (err) {
1896
1948
  throw new Error(`Could not reach CloudGrid at ${API_BASE}: ${err.message}`);
@@ -1901,33 +1953,53 @@ export async function runVisibility(ctx, { target, visibility, kind, org }) {
1901
1953
  return { res, data, raw };
1902
1954
  };
1903
1955
 
1956
+ // BOTH realms now take the same realm-scoped visibility PATCH with the
1957
+ // same body vocabulary: /api/v2/entities/:id/visibility (runtimes) and
1958
+ // /api/v2/inspirations/:id/visibility (inspirations).
1904
1959
  const runtimePath = `/api/v2/entities/${encodeURIComponent(id)}/visibility`;
1905
- const inspirationPath = `/api/v2/inspirations/${encodeURIComponent(id)}`;
1960
+ const inspirationPath = `/api/v2/inspirations/${encodeURIComponent(id)}/visibility`;
1906
1961
 
1907
- // Route order. `space` is inspiration-only; a known runtime kind goes straight
1908
- // to the entities route; a known inspiration goes straight to the inspiration
1909
- // route; an unknown kind tries the runtime route first and falls back to the
1910
- // inspiration route on a not-found (mirrors runPickup).
1962
+ // Route order: a known runtime kind goes straight to the entities route; a
1963
+ // known inspiration to the inspiration route; unknown tries runtime first and
1964
+ // falls back on a not-found (mirrors runPickup).
1911
1965
  let order;
1912
- if (vis === "space") order = ["inspiration"];
1913
- else if (isRuntimeKind) order = ["runtime"];
1966
+ if (isRuntimeKind) order = ["runtime"];
1914
1967
  else if (k === "inspiration") order = ["inspiration"];
1915
1968
  else order = ["runtime", "inspiration"];
1916
1969
 
1917
1970
  let last;
1918
1971
  for (let i = 0; i < order.length; i++) {
1919
1972
  const route = order[i];
1920
- last = route === "runtime"
1921
- ? await patch(runtimePath, runtimeValue)
1922
- : await patch(inspirationPath, inspirationValue);
1973
+ last = await patch(route === "runtime" ? runtimePath : inspirationPath);
1923
1974
  if (last.res.ok) {
1924
- const lines = [`Visibility is now ${vis}.`];
1925
- if (last.data?.url) lines.push(last.data.url);
1975
+ const d = last.data || {};
1976
+ // Prefer the server's stored axes (stored axes are authoritative on
1977
+ // the wire); fall back to what we asked for.
1978
+ const lines = [];
1979
+ if (d.share_scope || d.external_access) {
1980
+ const insideLine =
1981
+ d.share_scope === "private" ? "only the owner" :
1982
+ d.share_scope === "spaces" ? `selected spaces${Array.isArray(d.visibility_spaces) && d.visibility_spaces.length ? ` (${d.visibility_spaces.join(", ")})` : ""}` :
1983
+ d.share_scope === "grid" ? "everyone in the grid" : d.share_scope;
1984
+ const outsideLine =
1985
+ d.external_access === "none" ? "no one outside the grid" :
1986
+ d.external_access === "link" ? `anyone with the link${d.require_signin ? " (signed-in accounts only)" : ""}${d.link_indexed ? ", search-indexed" : ""}` :
1987
+ d.external_access === "public" ? "anyone, findable by search engines" : d.external_access;
1988
+ lines.push(`Visibility set — inside the grid: ${insideLine}; outside: ${outsideLine}.`);
1989
+ } else {
1990
+ lines.push(`Visibility is now ${d.visibility || requested}.`);
1991
+ }
1992
+ if (d.url) lines.push(d.url);
1926
1993
  return {
1927
1994
  text: lines.join("\n"),
1928
1995
  structured: {
1929
- visibility: vis,
1930
- ...(last.data?.url ? { url: last.data.url } : {}),
1996
+ visibility: d.visibility || (useAxes ? undefined : String(visibility || "").trim().toLowerCase() || undefined),
1997
+ ...(d.share_scope ? { share_scope: d.share_scope } : {}),
1998
+ ...(d.external_access ? { external_access: d.external_access } : {}),
1999
+ ...(d.require_signin !== undefined ? { require_signin: d.require_signin === true } : {}),
2000
+ ...(Array.isArray(d.visibility_spaces) ? { visibility_spaces: d.visibility_spaces } : {}),
2001
+ ...(d.link_indexed !== undefined ? { link_indexed: d.link_indexed === true } : {}),
2002
+ ...(d.url ? { url: d.url } : {}),
1931
2003
  },
1932
2004
  };
1933
2005
  }
@@ -398,7 +398,10 @@ export function registerTools(server, ctx) {
398
398
  })).optional().describe("Alias of grids (legacy picker alias)."),
399
399
  via: z.string().optional().describe("Recovery marker: 'cli-fallback' when a signed-in create was published through the bundled CloudGrid CLI."),
400
400
  },
401
- annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
401
+ // destructiveHint (M3): a re-plug with target_entity_id REPLACES what is
402
+ // live at a public URL in place. Versions + grid rollback exist, but the
403
+ // honest MCP annotation for overwrite-live-state is destructive.
404
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
402
405
  ...(ctx.edition === "web" && APPS_WIDGETS_ENABLED ? {
403
406
  _meta: {
404
407
  ui: { resourceUri: LIVE_RESULT_URI, csp: WIDGET_CSP },
@@ -659,15 +662,25 @@ export function registerTools(server, ctx) {
659
662
  reg(
660
663
  "grid_visibility",
661
664
  {
662
- description: "Change who can see a CloudGrid inspiration OR runtime app/agent: private, authenticated, grid (everyone in the grid), or link (anyone with the URL). `space` is inspiration-only. Use when the user wants to make something private, restrict who sees it, or open it up — including right after a drop, with no target id needed. Kind-aware: routes runtime apps/agents to the entities visibility surface and inspirations to the inspiration surface automatically. Defaults to the drop made in this session. Requires sign-in. Calls the API directly.",
665
+ description: "Change who can see a CloudGrid inspiration OR runtime app/agent. Simple modes: private (only the user), grid (everyone in their grid), link (anyone with the URL — add indexed: true to be findable by search engines). Finer control via the TWO AXES instead: inside (who in the grid: private | spaces | grid) and outside (reach beyond it: none | link | public), with require_signin for a members-only link and `spaces` for selected spaces. 'authenticated' is retired (it maps to a sign-in-required link); 'org' is gone — use grid. Use when the user wants to make something private, restrict who sees it, or open it up — including right after a drop, with no target id needed. Kind-aware routing across both surfaces. Defaults to the drop made in this session. Requires sign-in. Calls the API directly.",
663
666
  inputSchema: {
664
- visibility: z.enum(["private", "space", "authenticated", "grid", "org", "link"]).describe("The new scope. Use `grid` for whole-grid visibility (`org` is the deprecated alias). `space` is inspiration-only."),
667
+ visibility: z.enum(["private", "grid", "link", "public", "authenticated", "space"]).optional().describe("Simple mode: private | grid | link ('public' is an alias of link — pass indexed: true for search-findable; 'authenticated' is the retired alias for a sign-in-required link; 'space' needs the `spaces` list). Pass either this OR the inside/outside axes."),
668
+ inside: z.enum(["private", "spaces", "grid"]).optional().describe("Axis 1 — who in the grid can see it: private (only the user), spaces (selected spaces — pass `spaces`), or grid (everyone in the grid). Use together with `outside`, instead of `visibility`."),
669
+ outside: z.enum(["none", "link", "public"]).optional().describe("Axis 2 — reach beyond the grid: none, link (anyone with the link; add require_signin: true for signed-in accounts only), or public (anyone, and findable by search engines)."),
670
+ require_signin: z.boolean().optional().describe("With outside: link (or visibility: link) — the link requires a signed-in CloudGrid account."),
671
+ spaces: z.array(z.string()).optional().describe("Space slugs, for inside: spaces (or the legacy space/grid modes)."),
672
+ indexed: z.boolean().optional().describe("With visibility: link — make the page search-engine indexable."),
665
673
  target: z.string().optional().describe("Entity id. Defaults to this session's last drop."),
666
674
  kind: z.enum(["inspiration", "app", "agent"]).optional().describe("Entity kind. Omit to auto-detect from this session's last drop (falls back to trying the runtime surface, then the inspiration surface)."),
667
675
  org: z.string().optional().describe("Grid of the entity. Defaults to the active grid."),
668
676
  },
669
677
  outputSchema: {
670
- visibility: z.string().describe("The visibility that was set."),
678
+ visibility: z.string().optional().describe("The legacy mode that was set, when a simple mode was used."),
679
+ share_scope: z.string().optional().describe("Stored axis: who in the grid (private | spaces | grid)."),
680
+ external_access: z.string().optional().describe("Stored axis: reach beyond the grid (none | link | public)."),
681
+ require_signin: z.boolean().optional().describe("Whether the link requires sign-in."),
682
+ visibility_spaces: z.array(z.string()).optional().describe("The space slugs, when share_scope is spaces."),
683
+ link_indexed: z.boolean().optional().describe("Whether the link is search-indexed."),
671
684
  url: z.string().optional().describe("URL of the entity, if returned."),
672
685
  },
673
686
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },