@cloudgrid-io/mcp 0.3.2 → 0.3.4
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 -1
- package/src/tools.js +179 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudgrid-io/mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "MCP server for CloudGrid. Two editions: a local stdio server (full toolset) and a hosted web server (light, CLI-free toolset) over MCP Streamable HTTP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/tools.js
CHANGED
|
@@ -23,6 +23,14 @@ export const API_BASE = (process.env.CLOUDGRID_API_URL || "https://api.cloudgrid
|
|
|
23
23
|
);
|
|
24
24
|
|
|
25
25
|
const ANON_HTML_MAX_BYTES = 2_000_000;
|
|
26
|
+
const CONSOLE_URL = "https://console.cloudgrid.io/";
|
|
27
|
+
const VISIBILITY_LABELS = {
|
|
28
|
+
private: "Only you",
|
|
29
|
+
org: "Your org",
|
|
30
|
+
authenticated: "Anyone signed in",
|
|
31
|
+
space: "A space",
|
|
32
|
+
link: "Anyone with the link",
|
|
33
|
+
};
|
|
26
34
|
|
|
27
35
|
function ok(text) {
|
|
28
36
|
return { content: [{ type: "text", text }] };
|
|
@@ -77,6 +85,48 @@ function tryOpenBrowser(url) {
|
|
|
77
85
|
}
|
|
78
86
|
}
|
|
79
87
|
|
|
88
|
+
// ── Org listing (bearer-authed, web edition) ──────────────────────────────────
|
|
89
|
+
// Fetches the signed-in user's orgs via GET /api/v2/orgs. The JWT does not
|
|
90
|
+
// carry orgs (claims: sub, email, name, iat, exp), so the API is the canonical
|
|
91
|
+
// source. Returns [{slug, name, role}].
|
|
92
|
+
async function fetchUserOrgs(token) {
|
|
93
|
+
try {
|
|
94
|
+
const res = await fetch(`${API_BASE}/api/v2/orgs`, {
|
|
95
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
96
|
+
});
|
|
97
|
+
if (!res.ok) return [];
|
|
98
|
+
const data = await res.json();
|
|
99
|
+
const orgs = Array.isArray(data?.orgs) ? data.orgs : Array.isArray(data) ? data : [];
|
|
100
|
+
return orgs.map((o) => ({
|
|
101
|
+
slug: o.slug ?? "",
|
|
102
|
+
name: o.name ?? o.slug ?? "",
|
|
103
|
+
role: o.role ?? "member",
|
|
104
|
+
}));
|
|
105
|
+
} catch {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// After an authenticated web drop, upgrade visibility to "link" so the artifact
|
|
111
|
+
// is shareable and its preview renders without a sign-in wall. Best-effort — a
|
|
112
|
+
// failure here does not fail the drop; the user can always call cloudgrid_visibility.
|
|
113
|
+
async function upgradeVisibilityToLink(ctx, entityId, orgSlug) {
|
|
114
|
+
const token = await ctx.getToken();
|
|
115
|
+
if (!token || !entityId) return false;
|
|
116
|
+
try {
|
|
117
|
+
const hdrs = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
|
|
118
|
+
if (orgSlug) hdrs["X-CloudGrid-Org"] = orgSlug;
|
|
119
|
+
const res = await fetch(`${API_BASE}/api/v2/inspirations/${encodeURIComponent(entityId)}`, {
|
|
120
|
+
method: "PATCH",
|
|
121
|
+
headers: hdrs,
|
|
122
|
+
body: JSON.stringify({ visibility: "link" }),
|
|
123
|
+
});
|
|
124
|
+
return res.ok;
|
|
125
|
+
} catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
80
130
|
// ── Direct-API tools (both editions) ───────────────────────────────────────────
|
|
81
131
|
function looksLikeFullHtml(s) {
|
|
82
132
|
const head = s.replace(/^/, "").trimStart().slice(0, 256).toLowerCase();
|
|
@@ -198,34 +248,60 @@ async function runDrop(ctx, { html, path: filePath, filename, anonymous, org, fr
|
|
|
198
248
|
if (res.status === 200) {
|
|
199
249
|
// Updated in place: same URL, new version, views/reactions intact.
|
|
200
250
|
const url = (data.url ?? ctx.state.lastDrop?.url ?? "").trim();
|
|
201
|
-
const lines =
|
|
202
|
-
|
|
251
|
+
const lines = ctx.edition === "web"
|
|
252
|
+
? [`Your app is live: ${url}`]
|
|
253
|
+
: [`Updated in place — same link: ${url}`];
|
|
254
|
+
if (ctx.edition !== "web" && data.owned_by === "authenticated") lines.push("Owned by you.");
|
|
203
255
|
if (data.expires_at) lines.push(`Expires ${data.expires_at}.`);
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
...(data.owned_by ? { owned_by: data.owned_by } : {}),
|
|
210
|
-
...(data.expires_at ? { expires_at: data.expires_at } : {}),
|
|
211
|
-
},
|
|
256
|
+
const structured = {
|
|
257
|
+
url,
|
|
258
|
+
status: "updated",
|
|
259
|
+
...(data.owned_by ? { owned_by: data.owned_by } : {}),
|
|
260
|
+
...(data.expires_at ? { expires_at: data.expires_at } : {}),
|
|
212
261
|
};
|
|
262
|
+
if (ctx.edition === "web") {
|
|
263
|
+
// Default authed web drops to "link" visibility so the URL is shareable
|
|
264
|
+
// and the console thumbnail renders without a sign-in wall.
|
|
265
|
+
if (data.visibility !== "link" && data.entity_id) {
|
|
266
|
+
await upgradeVisibilityToLink(ctx, data.entity_id, orgSlug);
|
|
267
|
+
}
|
|
268
|
+
lines.push(`See and manage all your apps in your grid: ${CONSOLE_URL}`);
|
|
269
|
+
const vis = "link";
|
|
270
|
+
lines.push(`Visible to: ${VISIBILITY_LABELS[vis]}. Want to restrict access? I can set it to only you or your org.`);
|
|
271
|
+
structured.console_url = CONSOLE_URL;
|
|
272
|
+
structured.current_visibility = vis;
|
|
273
|
+
structured.visibility_options = Object.entries(VISIBILITY_LABELS).map(([v, l]) => ({ value: v, label: l }));
|
|
274
|
+
}
|
|
275
|
+
return { text: lines.join("\n"), structured };
|
|
213
276
|
}
|
|
214
277
|
|
|
215
278
|
if (data.owned_by === "authenticated") {
|
|
216
279
|
ctx.state.lastAnonClaim = null;
|
|
217
|
-
const lines =
|
|
280
|
+
const lines = ctx.edition === "web"
|
|
281
|
+
? [`Your app is live: ${data.url}`]
|
|
282
|
+
: [`Published to your org: ${data.url}`, "Owned by you."];
|
|
218
283
|
if (data.expires_at) lines.push(`Expires ${data.expires_at}.`);
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
status: "created",
|
|
225
|
-
owned_by: "authenticated",
|
|
226
|
-
...(data.expires_at ? { expires_at: data.expires_at } : {}),
|
|
227
|
-
},
|
|
284
|
+
const structured = {
|
|
285
|
+
url: data.url,
|
|
286
|
+
status: "created",
|
|
287
|
+
owned_by: "authenticated",
|
|
288
|
+
...(data.expires_at ? { expires_at: data.expires_at } : {}),
|
|
228
289
|
};
|
|
290
|
+
if (ctx.edition === "web") {
|
|
291
|
+
// Default authed web drops to "link" visibility (same as above).
|
|
292
|
+
if (data.visibility !== "link" && data.entity_id) {
|
|
293
|
+
await upgradeVisibilityToLink(ctx, data.entity_id, orgSlug);
|
|
294
|
+
}
|
|
295
|
+
lines.push(`See and manage all your apps in your grid: ${CONSOLE_URL}`);
|
|
296
|
+
const vis = "link";
|
|
297
|
+
lines.push(`Visible to: ${VISIBILITY_LABELS[vis]}. Want to restrict access? I can set it to only you or your org.`);
|
|
298
|
+
structured.console_url = CONSOLE_URL;
|
|
299
|
+
structured.current_visibility = vis;
|
|
300
|
+
structured.visibility_options = Object.entries(VISIBILITY_LABELS).map(([v, l]) => ({ value: v, label: l }));
|
|
301
|
+
} else {
|
|
302
|
+
lines.push("Drop again in this session to update it in place (same link); pass fresh to start a new one.");
|
|
303
|
+
}
|
|
304
|
+
return { text: lines.join("\n"), structured };
|
|
229
305
|
}
|
|
230
306
|
|
|
231
307
|
// 201 — created new (first drop, fresh: true, or the server fell back to create).
|
|
@@ -240,7 +316,7 @@ async function runDrop(ctx, { html, path: filePath, filename, anonymous, org, fr
|
|
|
240
316
|
ctx.state.lastAnonClaim = null;
|
|
241
317
|
}
|
|
242
318
|
}
|
|
243
|
-
const lines = [`Live: ${data.url}`];
|
|
319
|
+
const lines = [ctx.edition === "web" ? `Your app is live: ${data.url}` : `Live: ${data.url}`];
|
|
244
320
|
if (data.expires_at) lines.push(`Expires ${data.expires_at} — anonymous drops last 7 days.`);
|
|
245
321
|
if (data.claim_url) lines.push("Sign in, then run cloudgrid_claim to keep it past 7 days.");
|
|
246
322
|
lines.push("Drop again in this session to update it in place (same link); pass fresh to start a new one.");
|
|
@@ -376,22 +452,69 @@ export function registerTools(server, ctx) {
|
|
|
376
452
|
path: z.string().optional().describe("Path to a local file to upload instead of inline HTML."),
|
|
377
453
|
filename: z.string().optional().describe("Filename to present. Defaults to index.html for inline HTML."),
|
|
378
454
|
anonymous: z.boolean().optional().describe("Force an anonymous drop even if the user is signed in."),
|
|
379
|
-
org: z.string().optional().describe("
|
|
455
|
+
org: z.string().optional().describe("Leave unset; the tool will ask the user which org to publish into. Only set this after the user picks from the list the tool returns."),
|
|
380
456
|
fresh: z
|
|
381
457
|
.boolean()
|
|
382
458
|
.optional()
|
|
383
459
|
.describe("Force a new drop even if you already dropped in this session (default: update in place)."),
|
|
384
460
|
},
|
|
385
461
|
outputSchema: {
|
|
386
|
-
url: z.string().describe("The public URL of the drop."),
|
|
387
|
-
status: z.enum(["created", "updated", "unchanged"]).describe("What happened to the drop."),
|
|
462
|
+
url: z.string().optional().describe("The public URL of the drop."),
|
|
463
|
+
status: z.enum(["created", "updated", "unchanged"]).optional().describe("What happened to the drop."),
|
|
388
464
|
owned_by: z.string().optional().describe("Ownership class, e.g. 'authenticated'."),
|
|
389
465
|
expires_at: z.string().optional().describe("Expiry timestamp, if any."),
|
|
466
|
+
console_url: z.string().optional().describe("URL to manage all apps in the grid."),
|
|
467
|
+
current_visibility: z.string().optional().describe("Current visibility of the drop."),
|
|
468
|
+
visibility_options: z.array(z.object({
|
|
469
|
+
value: z.string().describe("Visibility value to pass to cloudgrid_visibility."),
|
|
470
|
+
label: z.string().describe("Human-readable label."),
|
|
471
|
+
})).optional().describe("Available visibility levels."),
|
|
472
|
+
needs_org: z.boolean().optional().describe("True when the user must choose an org before dropping."),
|
|
473
|
+
orgs: z.array(z.object({
|
|
474
|
+
slug: z.string().describe("Org slug to pass as the org parameter."),
|
|
475
|
+
name: z.string().describe("Human-readable org name."),
|
|
476
|
+
role: z.string().describe("User's role in the org."),
|
|
477
|
+
})).optional().describe("The user's orgs, when org choice is needed."),
|
|
478
|
+
needs_sign_in: z.boolean().optional().describe("True when sign-in is needed before dropping."),
|
|
479
|
+
login_url: z.string().optional().describe("Sign-in URL when authentication is needed."),
|
|
390
480
|
},
|
|
391
481
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
392
482
|
},
|
|
393
483
|
async (input) => {
|
|
394
484
|
try {
|
|
485
|
+
// Web edition: sign-in guidance when unauthenticated.
|
|
486
|
+
if (ctx.edition === "web" && input?.anonymous !== true) {
|
|
487
|
+
const token = await ctx.getToken();
|
|
488
|
+
if (!token) {
|
|
489
|
+
const url = buildLoginUrl(newLoginCode());
|
|
490
|
+
return okResult({
|
|
491
|
+
text: `Sign in to publish to your org.\n${url}`,
|
|
492
|
+
structured: { needs_sign_in: true, login_url: url },
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
// Org disambiguation: always validate the org against the user's real
|
|
496
|
+
// orgs. If the LLM guessed an org slug that doesn't match, ignore it
|
|
497
|
+
// and ask — this is why the >1-org ask didn't fire in the first test.
|
|
498
|
+
{
|
|
499
|
+
const orgs = await fetchUserOrgs(token);
|
|
500
|
+
const suppliedOrg = input?.org;
|
|
501
|
+
const validOrg = suppliedOrg && orgs.some((o) => o.slug === suppliedOrg);
|
|
502
|
+
if (!validOrg) {
|
|
503
|
+
if (orgs.length > 1) {
|
|
504
|
+
const lines = ["Which org should this be published to?"];
|
|
505
|
+
for (const o of orgs) lines.push(` ${o.slug} — ${o.name} (${o.role})`);
|
|
506
|
+
lines.push("Pass the org slug in the org parameter to publish.");
|
|
507
|
+
return okResult({
|
|
508
|
+
text: lines.join("\n"),
|
|
509
|
+
structured: { needs_org: true, orgs },
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
if (orgs.length === 1) {
|
|
513
|
+
input = { ...(input || {}), org: orgs[0].slug };
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
395
518
|
return okResult(await runDrop(ctx, input || {}));
|
|
396
519
|
} catch (err) {
|
|
397
520
|
return fail(err.message);
|
|
@@ -502,7 +625,7 @@ export function registerTools(server, ctx) {
|
|
|
502
625
|
server.registerTool(
|
|
503
626
|
"cloudgrid_visibility",
|
|
504
627
|
{
|
|
505
|
-
description: "Change who can see a CloudGrid inspiration: private, space, authenticated, org, or link (anyone with the URL). Use when the user wants to make a drop private, restrict who sees it, or open it up. Defaults to the drop made in this session. Requires sign-in. Calls the API directly.",
|
|
628
|
+
description: "Change who can see a CloudGrid inspiration: private, space, authenticated, org, or link (anyone with the URL). Use when the user wants to make a drop private, restrict who sees it, or open it up — including right after a drop, with no target id needed. Defaults to the drop made in this session. Requires sign-in. Calls the API directly.",
|
|
506
629
|
inputSchema: {
|
|
507
630
|
visibility: z.enum(["private", "space", "authenticated", "org", "link"]).describe("The new scope."),
|
|
508
631
|
target: z.string().optional().describe("Entity id. Defaults to this session's last drop."),
|
|
@@ -523,6 +646,37 @@ export function registerTools(server, ctx) {
|
|
|
523
646
|
},
|
|
524
647
|
);
|
|
525
648
|
|
|
649
|
+
// Org listing — web edition only (local edition uses cloudgrid_whoami).
|
|
650
|
+
if (ctx.edition === "web") {
|
|
651
|
+
server.registerTool(
|
|
652
|
+
"cloudgrid_orgs",
|
|
653
|
+
{
|
|
654
|
+
description: "List the signed-in user's organizations. Returns each org's slug, name, and the user's role. Use to discover which org to publish to. Requires sign-in.",
|
|
655
|
+
inputSchema: {},
|
|
656
|
+
outputSchema: {
|
|
657
|
+
orgs: z.array(z.object({
|
|
658
|
+
slug: z.string().describe("Org slug."),
|
|
659
|
+
name: z.string().describe("Human-readable org name."),
|
|
660
|
+
role: z.string().describe("User's role in the org."),
|
|
661
|
+
})).describe("The user's org memberships."),
|
|
662
|
+
},
|
|
663
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
664
|
+
},
|
|
665
|
+
async () => {
|
|
666
|
+
const token = await ctx.getToken();
|
|
667
|
+
if (!token) {
|
|
668
|
+
return fail("You are not signed in. Run cloudgrid_login first.");
|
|
669
|
+
}
|
|
670
|
+
const orgs = await fetchUserOrgs(token);
|
|
671
|
+
if (orgs.length === 0) {
|
|
672
|
+
return okResult({ text: "No organizations found.", structured: { orgs: [] } });
|
|
673
|
+
}
|
|
674
|
+
const lines = orgs.map((o) => `${o.slug} — ${o.name} (${o.role})`);
|
|
675
|
+
return okResult({ text: lines.join("\n"), structured: { orgs } });
|
|
676
|
+
},
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
|
|
526
680
|
if (ctx.edition !== "local") return; // web edition stops here — no CLI tools
|
|
527
681
|
|
|
528
682
|
// ── CLI-wrapping tools (local edition only) ───────────────────────────────
|