@cloudgrid-io/mcp 0.3.1 → 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 +260 -53
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 }] };
|
|
@@ -30,6 +38,9 @@ function ok(text) {
|
|
|
30
38
|
function fail(text) {
|
|
31
39
|
return { content: [{ type: "text", text }], isError: true };
|
|
32
40
|
}
|
|
41
|
+
function okResult({ text, structured }) {
|
|
42
|
+
return { content: [{ type: "text", text }], structuredContent: structured };
|
|
43
|
+
}
|
|
33
44
|
|
|
34
45
|
// ── CLI wrapping (local edition only) ──────────────────────────────────────────
|
|
35
46
|
async function runCloudgrid(args) {
|
|
@@ -74,6 +85,28 @@ function tryOpenBrowser(url) {
|
|
|
74
85
|
}
|
|
75
86
|
}
|
|
76
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
|
+
|
|
77
110
|
// ── Direct-API tools (both editions) ───────────────────────────────────────────
|
|
78
111
|
function looksLikeFullHtml(s) {
|
|
79
112
|
const head = s.replace(/^/, "").trimStart().slice(0, 256).toLowerCase();
|
|
@@ -185,23 +218,61 @@ async function runDrop(ctx, { html, path: filePath, filename, anonymous, org, fr
|
|
|
185
218
|
|
|
186
219
|
if (res.status === 202) {
|
|
187
220
|
// Idempotent no-op — the bytes matched the live version exactly.
|
|
188
|
-
|
|
221
|
+
const url = (data.url ?? ctx.state.lastDrop?.url ?? "").trim();
|
|
222
|
+
return {
|
|
223
|
+
text: `No change — this exact content is already live: ${url}`,
|
|
224
|
+
structured: { url, status: "unchanged" },
|
|
225
|
+
};
|
|
189
226
|
}
|
|
190
227
|
|
|
191
228
|
if (res.status === 200) {
|
|
192
229
|
// Updated in place: same URL, new version, views/reactions intact.
|
|
193
|
-
const
|
|
194
|
-
|
|
230
|
+
const url = (data.url ?? ctx.state.lastDrop?.url ?? "").trim();
|
|
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.");
|
|
195
235
|
if (data.expires_at) lines.push(`Expires ${data.expires_at}.`);
|
|
196
|
-
|
|
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 } : {}),
|
|
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 };
|
|
197
251
|
}
|
|
198
252
|
|
|
199
253
|
if (data.owned_by === "authenticated") {
|
|
200
254
|
ctx.state.lastAnonClaim = null;
|
|
201
|
-
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."];
|
|
202
258
|
if (data.expires_at) lines.push(`Expires ${data.expires_at}.`);
|
|
203
|
-
|
|
204
|
-
|
|
259
|
+
const structured = {
|
|
260
|
+
url: data.url,
|
|
261
|
+
status: "created",
|
|
262
|
+
owned_by: "authenticated",
|
|
263
|
+
...(data.expires_at ? { expires_at: data.expires_at } : {}),
|
|
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 };
|
|
205
276
|
}
|
|
206
277
|
|
|
207
278
|
// 201 — created new (first drop, fresh: true, or the server fell back to create).
|
|
@@ -216,11 +287,18 @@ async function runDrop(ctx, { html, path: filePath, filename, anonymous, org, fr
|
|
|
216
287
|
ctx.state.lastAnonClaim = null;
|
|
217
288
|
}
|
|
218
289
|
}
|
|
219
|
-
const lines = [`Live: ${data.url}`];
|
|
290
|
+
const lines = [ctx.edition === "web" ? `Your app is live: ${data.url}` : `Live: ${data.url}`];
|
|
220
291
|
if (data.expires_at) lines.push(`Expires ${data.expires_at} — anonymous drops last 7 days.`);
|
|
221
292
|
if (data.claim_url) lines.push("Sign in, then run cloudgrid_claim to keep it past 7 days.");
|
|
222
293
|
lines.push("Drop again in this session to update it in place (same link); pass fresh to start a new one.");
|
|
223
|
-
return
|
|
294
|
+
return {
|
|
295
|
+
text: lines.join("\n"),
|
|
296
|
+
structured: {
|
|
297
|
+
url: data.url,
|
|
298
|
+
status: "created",
|
|
299
|
+
...(data.expires_at ? { expires_at: data.expires_at } : {}),
|
|
300
|
+
},
|
|
301
|
+
};
|
|
224
302
|
}
|
|
225
303
|
|
|
226
304
|
async function runClaim(ctx, { claim_token, claim_url }) {
|
|
@@ -264,13 +342,24 @@ async function runClaim(ctx, { claim_token, claim_url }) {
|
|
|
264
342
|
throw new Error(`Claim failed (HTTP ${res.status}): ${msg}`);
|
|
265
343
|
}
|
|
266
344
|
const claimed = Array.isArray(data?.claimed) ? data.claimed : [];
|
|
267
|
-
if (claimed.length === 0)
|
|
345
|
+
if (claimed.length === 0) {
|
|
346
|
+
return {
|
|
347
|
+
text: "Nothing to claim — it may already be claimed or expired.",
|
|
348
|
+
structured: { claimed: 0, urls: [] },
|
|
349
|
+
};
|
|
350
|
+
}
|
|
268
351
|
ctx.state.lastAnonClaim = null;
|
|
269
352
|
const lines = [`Claimed ${claimed.length}, now yours:`];
|
|
270
353
|
for (const c of claimed) {
|
|
271
354
|
lines.push(`${c.url}${c.new_expires_at ? ` (expires ${c.new_expires_at})` : ""}`);
|
|
272
355
|
}
|
|
273
|
-
return
|
|
356
|
+
return {
|
|
357
|
+
text: lines.join("\n"),
|
|
358
|
+
structured: {
|
|
359
|
+
claimed: claimed.length,
|
|
360
|
+
urls: claimed.map((c) => c.url),
|
|
361
|
+
},
|
|
362
|
+
};
|
|
274
363
|
}
|
|
275
364
|
|
|
276
365
|
|
|
@@ -309,7 +398,13 @@ async function runVisibility(ctx, { target, visibility, org }) {
|
|
|
309
398
|
}
|
|
310
399
|
const lines = [`Visibility is now ${visibility}.`];
|
|
311
400
|
if (data?.url) lines.push(data.url);
|
|
312
|
-
return
|
|
401
|
+
return {
|
|
402
|
+
text: lines.join("\n"),
|
|
403
|
+
structured: {
|
|
404
|
+
visibility,
|
|
405
|
+
...(data?.url ? { url: data.url } : {}),
|
|
406
|
+
},
|
|
407
|
+
};
|
|
313
408
|
}
|
|
314
409
|
|
|
315
410
|
// ── Registration ───────────────────────────────────────────────────────────────
|
|
@@ -319,24 +414,73 @@ export function registerTools(server, ctx) {
|
|
|
319
414
|
// ── Direct-API tools (both editions) ──────────────────────────────────────
|
|
320
415
|
|
|
321
416
|
// Drop — both editions.
|
|
322
|
-
server.
|
|
417
|
+
server.registerTool(
|
|
323
418
|
"cloudgrid_drop",
|
|
324
|
-
"Publish an HTML page or file to CloudGrid and get a public shareable URL. Use when the user wants to share, publish, send, or 'deploy' an artifact, or wants a link to send a friend. Re-drops in the same session update the existing drop in place — same link, new version; pass fresh: true to force a new one. If signed in, it publishes into the user's org as an owned inspiration (30-day expiry); if not, it drops anonymously (7-day expiry, claimable later). Calls the API directly.",
|
|
325
419
|
{
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
.
|
|
333
|
-
|
|
334
|
-
|
|
420
|
+
description: "Publish an HTML page or file to CloudGrid and get a public shareable URL. Use when the user wants to share, publish, send, or 'deploy' an artifact, or wants a link to send a friend. Re-drops in the same session update the existing drop in place — same link, new version; pass fresh: true to force a new one. If signed in, it publishes into the user's org as an owned inspiration (30-day expiry); if not, it drops anonymously (7-day expiry, claimable later). Calls the API directly.",
|
|
421
|
+
inputSchema: {
|
|
422
|
+
html: z.string().optional().describe("Inline HTML to publish. A fragment is wrapped into a full document."),
|
|
423
|
+
path: z.string().optional().describe("Path to a local file to upload instead of inline HTML."),
|
|
424
|
+
filename: z.string().optional().describe("Filename to present. Defaults to index.html for inline HTML."),
|
|
425
|
+
anonymous: z.boolean().optional().describe("Force an anonymous drop even if the user is signed in."),
|
|
426
|
+
org: z.string().optional().describe("Org slug to publish into when signed in. Defaults to the active org."),
|
|
427
|
+
fresh: z
|
|
428
|
+
.boolean()
|
|
429
|
+
.optional()
|
|
430
|
+
.describe("Force a new drop even if you already dropped in this session (default: update in place)."),
|
|
431
|
+
},
|
|
432
|
+
outputSchema: {
|
|
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."),
|
|
435
|
+
owned_by: z.string().optional().describe("Ownership class, e.g. 'authenticated'."),
|
|
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."),
|
|
451
|
+
},
|
|
452
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
335
453
|
},
|
|
336
|
-
{ readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
337
454
|
async (input) => {
|
|
338
455
|
try {
|
|
339
|
-
|
|
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
|
+
}
|
|
483
|
+
return okResult(await runDrop(ctx, input || {}));
|
|
340
484
|
} catch (err) {
|
|
341
485
|
return fail(err.message);
|
|
342
486
|
}
|
|
@@ -344,17 +488,23 @@ export function registerTools(server, ctx) {
|
|
|
344
488
|
);
|
|
345
489
|
|
|
346
490
|
// Claim — both editions.
|
|
347
|
-
server.
|
|
491
|
+
server.registerTool(
|
|
348
492
|
"cloudgrid_claim",
|
|
349
|
-
"Claim an anonymous drop into the signed-in account, so it becomes owned and stops expiring in 7 days. Use after the user signs in to keep something they dropped anonymously. The public URL does not change. Requires sign-in (cloudgrid_login). Calls the API directly.",
|
|
350
493
|
{
|
|
351
|
-
|
|
352
|
-
|
|
494
|
+
description: "Claim an anonymous drop into the signed-in account, so it becomes owned and stops expiring in 7 days. Use after the user signs in to keep something they dropped anonymously. The public URL does not change. Requires sign-in (cloudgrid_login). Calls the API directly.",
|
|
495
|
+
inputSchema: {
|
|
496
|
+
claim_token: z.string().optional().describe("The claim token from an anonymous drop."),
|
|
497
|
+
claim_url: z.string().optional().describe("The claim_url from an anonymous drop; the token is read from it."),
|
|
498
|
+
},
|
|
499
|
+
outputSchema: {
|
|
500
|
+
claimed: z.number().describe("Number of drops claimed."),
|
|
501
|
+
urls: z.array(z.string()).describe("URLs of the claimed drops."),
|
|
502
|
+
},
|
|
503
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
353
504
|
},
|
|
354
|
-
{ readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
355
505
|
async (input) => {
|
|
356
506
|
try {
|
|
357
|
-
return
|
|
507
|
+
return okResult(await runClaim(ctx, input || {}));
|
|
358
508
|
} catch (err) {
|
|
359
509
|
return fail(err.message);
|
|
360
510
|
}
|
|
@@ -363,30 +513,44 @@ export function registerTools(server, ctx) {
|
|
|
363
513
|
|
|
364
514
|
// Login — both editions. Local opens a browser and saves to the credentials
|
|
365
515
|
// file; web returns the URL and saves to the session.
|
|
366
|
-
server.
|
|
516
|
+
server.registerTool(
|
|
367
517
|
"cloudgrid_login",
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
518
|
+
{
|
|
519
|
+
description: "Start a CLI-free CloudGrid sign-in. Use when the user wants to log in, sign in, or authenticate, or to claim an anonymous drop. Returns a URL to open in the browser; then call cloudgrid_login_status to finish. Uses CloudGrid's existing OAuth.",
|
|
520
|
+
inputSchema: {},
|
|
521
|
+
outputSchema: {
|
|
522
|
+
login_url: z.string().describe("URL to open in a browser to complete sign-in."),
|
|
523
|
+
},
|
|
524
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
525
|
+
},
|
|
371
526
|
async () => {
|
|
372
527
|
const code = newLoginCode();
|
|
373
528
|
ctx.state.pendingLoginCode = code;
|
|
374
529
|
const url = buildLoginUrl(code);
|
|
375
530
|
if (ctx.canOpenBrowser) tryOpenBrowser(url);
|
|
376
|
-
return
|
|
377
|
-
|
|
531
|
+
return {
|
|
532
|
+
content: [{ type: "text", text:
|
|
533
|
+
`To sign in, open this URL in your browser and finish with Google:\n${url}\n\n` +
|
|
378
534
|
`After you complete it, run cloudgrid_login_status to finish signing in.`,
|
|
379
|
-
|
|
535
|
+
}],
|
|
536
|
+
structuredContent: { login_url: url },
|
|
537
|
+
};
|
|
380
538
|
},
|
|
381
539
|
);
|
|
382
540
|
|
|
383
|
-
server.
|
|
541
|
+
server.registerTool(
|
|
384
542
|
"cloudgrid_login_status",
|
|
385
|
-
"Finish a sign-in started by cloudgrid_login. Polls once: if you have completed the browser sign-in, it saves your session; otherwise it tells you to finish and try again.",
|
|
386
543
|
{
|
|
387
|
-
|
|
544
|
+
description: "Finish a sign-in started by cloudgrid_login. Polls once: if you have completed the browser sign-in, it saves your session; otherwise it tells you to finish and try again.",
|
|
545
|
+
inputSchema: {
|
|
546
|
+
code: z.string().optional().describe("The sign-in code. Defaults to the most recent cloudgrid_login."),
|
|
547
|
+
},
|
|
548
|
+
outputSchema: {
|
|
549
|
+
status: z.enum(["authenticated", "pending"]).describe("Current sign-in state."),
|
|
550
|
+
email: z.string().optional().describe("Signed-in email, when authenticated."),
|
|
551
|
+
},
|
|
552
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
388
553
|
},
|
|
389
|
-
{ readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
390
554
|
async (input) => {
|
|
391
555
|
const code = input?.code || ctx.state.pendingLoginCode;
|
|
392
556
|
if (!code) return fail("No sign-in is in progress. Run cloudgrid_login first.");
|
|
@@ -405,36 +569,79 @@ export function registerTools(server, ctx) {
|
|
|
405
569
|
}
|
|
406
570
|
ctx.state.pendingLoginCode = null;
|
|
407
571
|
const who = info?.email ? ` as ${info.email}` : "";
|
|
408
|
-
return
|
|
572
|
+
return {
|
|
573
|
+
content: [{ type: "text", text: `Signed in${who}. ${ctx.savedLocationNote()}` }],
|
|
574
|
+
structuredContent: { status: "authenticated", ...(info?.email ? { email: info.email } : {}) },
|
|
575
|
+
};
|
|
409
576
|
}
|
|
410
577
|
if (status.status === "pending" || status.status === "not_started") {
|
|
411
|
-
return
|
|
412
|
-
|
|
578
|
+
return {
|
|
579
|
+
content: [{ type: "text", text:
|
|
580
|
+
"Still waiting for you to finish signing in. Open the URL from cloudgrid_login " +
|
|
413
581
|
"in your browser, complete it with Google, then run cloudgrid_login_status again.",
|
|
414
|
-
|
|
582
|
+
}],
|
|
583
|
+
structuredContent: { status: "pending" },
|
|
584
|
+
};
|
|
415
585
|
}
|
|
416
586
|
return fail("The sign-in window expired (5 minutes). Run cloudgrid_login to start again.");
|
|
417
587
|
},
|
|
418
588
|
);
|
|
419
589
|
|
|
420
|
-
server.
|
|
590
|
+
server.registerTool(
|
|
421
591
|
"cloudgrid_visibility",
|
|
422
|
-
"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.",
|
|
423
592
|
{
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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.",
|
|
594
|
+
inputSchema: {
|
|
595
|
+
visibility: z.enum(["private", "space", "authenticated", "org", "link"]).describe("The new scope."),
|
|
596
|
+
target: z.string().optional().describe("Entity id. Defaults to this session's last drop."),
|
|
597
|
+
org: z.string().optional().describe("Org of the entity. Defaults to the active org."),
|
|
598
|
+
},
|
|
599
|
+
outputSchema: {
|
|
600
|
+
visibility: z.string().describe("The visibility that was set."),
|
|
601
|
+
url: z.string().optional().describe("URL of the entity, if returned."),
|
|
602
|
+
},
|
|
603
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
427
604
|
},
|
|
428
|
-
{ readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
429
605
|
async (input) => {
|
|
430
606
|
try {
|
|
431
|
-
return
|
|
607
|
+
return okResult(await runVisibility(ctx, input || {}));
|
|
432
608
|
} catch (err) {
|
|
433
609
|
return fail(err.message);
|
|
434
610
|
}
|
|
435
611
|
},
|
|
436
612
|
);
|
|
437
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
|
+
|
|
438
645
|
if (ctx.edition !== "local") return; // web edition stops here — no CLI tools
|
|
439
646
|
|
|
440
647
|
// ── CLI-wrapping tools (local edition only) ───────────────────────────────
|