@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 +64 -47
- package/package.json +1 -1
- package/src/auth.js +9 -2
- package/src/corpus/cli-reference.md +1 -1
- package/src/corpus/install.md +7 -2
- package/src/corpus/playbook.md +1 -1
- package/src/corpus/skill-share.md +10 -4
- package/src/tools/constants.js +11 -3
- package/src/tools/deploy.js +115 -43
- package/src/tools/register.js +17 -4
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
|
|
10
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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 |
|
|
91
|
+
| Tool | Wraps | Summary |
|
|
75
92
|
|---|---|---|
|
|
76
|
-
| `
|
|
77
|
-
| `
|
|
78
|
-
| `
|
|
79
|
-
| `
|
|
80
|
-
| `
|
|
81
|
-
| `
|
|
82
|
-
| `
|
|
83
|
-
| `
|
|
84
|
-
| `
|
|
85
|
-
| `
|
|
86
|
-
| `
|
|
87
|
-
| `
|
|
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 |
|
|
114
|
+
| Tool | Wraps | Summary |
|
|
96
115
|
|---|---|---|
|
|
97
|
-
| `grid_create_project` | `grid new` | Scaffold
|
|
98
|
-
| `grid_view_logs` | `grid logs` |
|
|
99
|
-
| `grid_share` | `grid visibility set` | Set visibility
|
|
100
|
-
| `
|
|
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.
|
|
103
|
-
| `grid_status` | `grid status` |
|
|
104
|
-
| `grid_info` | `grid info` |
|
|
105
|
-
| `grid_get` | `grid get grids
|
|
106
|
-
| `grid_describe_grid` | `grid describe grid <slug
|
|
107
|
-
| `grid_edit_existing_app` | `grid
|
|
108
|
-
| `grid_rename` | `grid rename` | Rename
|
|
109
|
-
| `grid_take_offline` | `grid unplug` | Take an entity off the grid.
|
|
110
|
-
| `grid_delete` | `grid delete entity` | Archive
|
|
111
|
-
| `grid_rollback_deploy` | `grid rollback` | Rollback to a previous version. |
|
|
112
|
-
| `grid_list_versions` | `grid versions` | List published versions
|
|
113
|
-
| `grid_set_env` | `grid env` |
|
|
114
|
-
| `grid_set_secret` | `grid secrets` | Set or list secret names
|
|
115
|
-
| `grid_scaffold` | `grid scaffold` |
|
|
116
|
-
| `grid_diagnose` | `grid doctor` | Run
|
|
117
|
-
| `grid_get_url` | `grid open --print` | Return the public URL
|
|
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
|
|
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
|
|
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
|
-
- `
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 \|
|
|
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 |
|
package/src/corpus/install.md
CHANGED
|
@@ -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
|
|
140
|
-
|
|
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
|
|
package/src/corpus/playbook.md
CHANGED
|
@@ -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),
|
|
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
|
-
- `
|
|
46
|
-
|
|
47
|
-
|
|
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.
|
package/src/tools/constants.js
CHANGED
|
@@ -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
|
-
|
|
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) ────────────────────
|
package/src/tools/deploy.js
CHANGED
|
@@ -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):
|
|
320
|
-
// https://<slug
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
1758
|
-
.map((
|
|
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
|
|
1869
|
-
//
|
|
1870
|
-
//
|
|
1871
|
-
//
|
|
1872
|
-
//
|
|
1873
|
-
//
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
1908
|
-
// to the
|
|
1909
|
-
//
|
|
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 (
|
|
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
|
|
1925
|
-
|
|
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:
|
|
1930
|
-
...(
|
|
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
|
}
|
package/src/tools/register.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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", "
|
|
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
|
|
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 },
|