@cloudgrid-io/mcp 0.3.2 → 0.3.3
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 +143 -24
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudgrid-io/mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
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,28 @@ 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
|
+
|
|
80
110
|
// ── Direct-API tools (both editions) ───────────────────────────────────────────
|
|
81
111
|
function looksLikeFullHtml(s) {
|
|
82
112
|
const head = s.replace(/^/, "").trimStart().slice(0, 256).toLowerCase();
|
|
@@ -198,34 +228,51 @@ async function runDrop(ctx, { html, path: filePath, filename, anonymous, org, fr
|
|
|
198
228
|
if (res.status === 200) {
|
|
199
229
|
// Updated in place: same URL, new version, views/reactions intact.
|
|
200
230
|
const url = (data.url ?? ctx.state.lastDrop?.url ?? "").trim();
|
|
201
|
-
const lines =
|
|
202
|
-
|
|
231
|
+
const lines = ctx.edition === "web"
|
|
232
|
+
? [`Your app is live: ${url}`]
|
|
233
|
+
: [`Updated in place — same link: ${url}`];
|
|
234
|
+
if (ctx.edition !== "web" && data.owned_by === "authenticated") lines.push("Owned by you.");
|
|
203
235
|
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
|
-
},
|
|
236
|
+
const structured = {
|
|
237
|
+
url,
|
|
238
|
+
status: "updated",
|
|
239
|
+
...(data.owned_by ? { owned_by: data.owned_by } : {}),
|
|
240
|
+
...(data.expires_at ? { expires_at: data.expires_at } : {}),
|
|
212
241
|
};
|
|
242
|
+
if (ctx.edition === "web") {
|
|
243
|
+
lines.push(`See and manage all your apps in your grid: ${CONSOLE_URL}`);
|
|
244
|
+
const vis = data.visibility || "link";
|
|
245
|
+
lines.push(`Visible to: ${VISIBILITY_LABELS[vis] || vis}. Want to change who can see it? I can set it to only you, your org, or anyone with the link.`);
|
|
246
|
+
structured.console_url = CONSOLE_URL;
|
|
247
|
+
structured.current_visibility = vis;
|
|
248
|
+
structured.visibility_options = Object.entries(VISIBILITY_LABELS).map(([v, l]) => ({ value: v, label: l }));
|
|
249
|
+
}
|
|
250
|
+
return { text: lines.join("\n"), structured };
|
|
213
251
|
}
|
|
214
252
|
|
|
215
253
|
if (data.owned_by === "authenticated") {
|
|
216
254
|
ctx.state.lastAnonClaim = null;
|
|
217
|
-
const lines =
|
|
255
|
+
const lines = ctx.edition === "web"
|
|
256
|
+
? [`Your app is live: ${data.url}`]
|
|
257
|
+
: [`Published to your org: ${data.url}`, "Owned by you."];
|
|
218
258
|
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
|
-
},
|
|
259
|
+
const structured = {
|
|
260
|
+
url: data.url,
|
|
261
|
+
status: "created",
|
|
262
|
+
owned_by: "authenticated",
|
|
263
|
+
...(data.expires_at ? { expires_at: data.expires_at } : {}),
|
|
228
264
|
};
|
|
265
|
+
if (ctx.edition === "web") {
|
|
266
|
+
lines.push(`See and manage all your apps in your grid: ${CONSOLE_URL}`);
|
|
267
|
+
const vis = data.visibility || "link";
|
|
268
|
+
lines.push(`Visible to: ${VISIBILITY_LABELS[vis] || vis}. Want to change who can see it? I can set it to only you, your org, or anyone with the link.`);
|
|
269
|
+
structured.console_url = CONSOLE_URL;
|
|
270
|
+
structured.current_visibility = vis;
|
|
271
|
+
structured.visibility_options = Object.entries(VISIBILITY_LABELS).map(([v, l]) => ({ value: v, label: l }));
|
|
272
|
+
} else {
|
|
273
|
+
lines.push("Drop again in this session to update it in place (same link); pass fresh to start a new one.");
|
|
274
|
+
}
|
|
275
|
+
return { text: lines.join("\n"), structured };
|
|
229
276
|
}
|
|
230
277
|
|
|
231
278
|
// 201 — created new (first drop, fresh: true, or the server fell back to create).
|
|
@@ -240,7 +287,7 @@ async function runDrop(ctx, { html, path: filePath, filename, anonymous, org, fr
|
|
|
240
287
|
ctx.state.lastAnonClaim = null;
|
|
241
288
|
}
|
|
242
289
|
}
|
|
243
|
-
const lines = [`Live: ${data.url}`];
|
|
290
|
+
const lines = [ctx.edition === "web" ? `Your app is live: ${data.url}` : `Live: ${data.url}`];
|
|
244
291
|
if (data.expires_at) lines.push(`Expires ${data.expires_at} — anonymous drops last 7 days.`);
|
|
245
292
|
if (data.claim_url) lines.push("Sign in, then run cloudgrid_claim to keep it past 7 days.");
|
|
246
293
|
lines.push("Drop again in this session to update it in place (same link); pass fresh to start a new one.");
|
|
@@ -383,15 +430,56 @@ export function registerTools(server, ctx) {
|
|
|
383
430
|
.describe("Force a new drop even if you already dropped in this session (default: update in place)."),
|
|
384
431
|
},
|
|
385
432
|
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."),
|
|
433
|
+
url: z.string().optional().describe("The public URL of the drop."),
|
|
434
|
+
status: z.enum(["created", "updated", "unchanged"]).optional().describe("What happened to the drop."),
|
|
388
435
|
owned_by: z.string().optional().describe("Ownership class, e.g. 'authenticated'."),
|
|
389
436
|
expires_at: z.string().optional().describe("Expiry timestamp, if any."),
|
|
437
|
+
console_url: z.string().optional().describe("URL to manage all apps in the grid."),
|
|
438
|
+
current_visibility: z.string().optional().describe("Current visibility of the drop."),
|
|
439
|
+
visibility_options: z.array(z.object({
|
|
440
|
+
value: z.string().describe("Visibility value to pass to cloudgrid_visibility."),
|
|
441
|
+
label: z.string().describe("Human-readable label."),
|
|
442
|
+
})).optional().describe("Available visibility levels."),
|
|
443
|
+
needs_org: z.boolean().optional().describe("True when the user must choose an org before dropping."),
|
|
444
|
+
orgs: z.array(z.object({
|
|
445
|
+
slug: z.string().describe("Org slug to pass as the org parameter."),
|
|
446
|
+
name: z.string().describe("Human-readable org name."),
|
|
447
|
+
role: z.string().describe("User's role in the org."),
|
|
448
|
+
})).optional().describe("The user's orgs, when org choice is needed."),
|
|
449
|
+
needs_sign_in: z.boolean().optional().describe("True when sign-in is needed before dropping."),
|
|
450
|
+
login_url: z.string().optional().describe("Sign-in URL when authentication is needed."),
|
|
390
451
|
},
|
|
391
452
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
392
453
|
},
|
|
393
454
|
async (input) => {
|
|
394
455
|
try {
|
|
456
|
+
// Web edition: sign-in guidance when unauthenticated.
|
|
457
|
+
if (ctx.edition === "web" && input?.anonymous !== true) {
|
|
458
|
+
const token = await ctx.getToken();
|
|
459
|
+
if (!token) {
|
|
460
|
+
const url = buildLoginUrl(newLoginCode());
|
|
461
|
+
return okResult({
|
|
462
|
+
text: `Sign in to publish to your org.\n${url}`,
|
|
463
|
+
structured: { needs_sign_in: true, login_url: url },
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
// Org disambiguation: if signed in, no org arg, list the user's orgs.
|
|
467
|
+
if (!input?.org) {
|
|
468
|
+
const orgs = await fetchUserOrgs(token);
|
|
469
|
+
if (orgs.length > 1) {
|
|
470
|
+
const lines = ["Which org should this be published to?"];
|
|
471
|
+
for (const o of orgs) lines.push(` ${o.slug} — ${o.name} (${o.role})`);
|
|
472
|
+
lines.push("Pass the org slug in the org parameter to publish.");
|
|
473
|
+
return okResult({
|
|
474
|
+
text: lines.join("\n"),
|
|
475
|
+
structured: { needs_org: true, orgs },
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
if (orgs.length === 1) {
|
|
479
|
+
input = { ...(input || {}), org: orgs[0].slug };
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
395
483
|
return okResult(await runDrop(ctx, input || {}));
|
|
396
484
|
} catch (err) {
|
|
397
485
|
return fail(err.message);
|
|
@@ -502,7 +590,7 @@ export function registerTools(server, ctx) {
|
|
|
502
590
|
server.registerTool(
|
|
503
591
|
"cloudgrid_visibility",
|
|
504
592
|
{
|
|
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.",
|
|
593
|
+
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
594
|
inputSchema: {
|
|
507
595
|
visibility: z.enum(["private", "space", "authenticated", "org", "link"]).describe("The new scope."),
|
|
508
596
|
target: z.string().optional().describe("Entity id. Defaults to this session's last drop."),
|
|
@@ -523,6 +611,37 @@ export function registerTools(server, ctx) {
|
|
|
523
611
|
},
|
|
524
612
|
);
|
|
525
613
|
|
|
614
|
+
// Org listing — web edition only (local edition uses cloudgrid_whoami).
|
|
615
|
+
if (ctx.edition === "web") {
|
|
616
|
+
server.registerTool(
|
|
617
|
+
"cloudgrid_orgs",
|
|
618
|
+
{
|
|
619
|
+
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.",
|
|
620
|
+
inputSchema: {},
|
|
621
|
+
outputSchema: {
|
|
622
|
+
orgs: z.array(z.object({
|
|
623
|
+
slug: z.string().describe("Org slug."),
|
|
624
|
+
name: z.string().describe("Human-readable org name."),
|
|
625
|
+
role: z.string().describe("User's role in the org."),
|
|
626
|
+
})).describe("The user's org memberships."),
|
|
627
|
+
},
|
|
628
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
629
|
+
},
|
|
630
|
+
async () => {
|
|
631
|
+
const token = await ctx.getToken();
|
|
632
|
+
if (!token) {
|
|
633
|
+
return fail("You are not signed in. Run cloudgrid_login first.");
|
|
634
|
+
}
|
|
635
|
+
const orgs = await fetchUserOrgs(token);
|
|
636
|
+
if (orgs.length === 0) {
|
|
637
|
+
return okResult({ text: "No organizations found.", structured: { orgs: [] } });
|
|
638
|
+
}
|
|
639
|
+
const lines = orgs.map((o) => `${o.slug} — ${o.name} (${o.role})`);
|
|
640
|
+
return okResult({ text: lines.join("\n"), structured: { orgs } });
|
|
641
|
+
},
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
|
|
526
645
|
if (ctx.edition !== "local") return; // web edition stops here — no CLI tools
|
|
527
646
|
|
|
528
647
|
// ── CLI-wrapping tools (local edition only) ───────────────────────────────
|