@cloudgrid-io/mcp 0.3.3 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/tools.js +53 -18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudgrid-io/mcp",
3
- "version": "0.3.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
@@ -107,6 +107,26 @@ async function fetchUserOrgs(token) {
107
107
  }
108
108
  }
109
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
+
110
130
  // ── Direct-API tools (both editions) ───────────────────────────────────────────
111
131
  function looksLikeFullHtml(s) {
112
132
  const head = s.replace(/^/, "").trimStart().slice(0, 256).toLowerCase();
@@ -240,9 +260,14 @@ async function runDrop(ctx, { html, path: filePath, filename, anonymous, org, fr
240
260
  ...(data.expires_at ? { expires_at: data.expires_at } : {}),
241
261
  };
242
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
+ }
243
268
  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.`);
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.`);
246
271
  structured.console_url = CONSOLE_URL;
247
272
  structured.current_visibility = vis;
248
273
  structured.visibility_options = Object.entries(VISIBILITY_LABELS).map(([v, l]) => ({ value: v, label: l }));
@@ -263,9 +288,13 @@ async function runDrop(ctx, { html, path: filePath, filename, anonymous, org, fr
263
288
  ...(data.expires_at ? { expires_at: data.expires_at } : {}),
264
289
  };
265
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
+ }
266
295
  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.`);
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.`);
269
298
  structured.console_url = CONSOLE_URL;
270
299
  structured.current_visibility = vis;
271
300
  structured.visibility_options = Object.entries(VISIBILITY_LABELS).map(([v, l]) => ({ value: v, label: l }));
@@ -423,7 +452,7 @@ export function registerTools(server, ctx) {
423
452
  path: z.string().optional().describe("Path to a local file to upload instead of inline HTML."),
424
453
  filename: z.string().optional().describe("Filename to present. Defaults to index.html for inline HTML."),
425
454
  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."),
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."),
427
456
  fresh: z
428
457
  .boolean()
429
458
  .optional()
@@ -463,20 +492,26 @@ export function registerTools(server, ctx) {
463
492
  structured: { needs_sign_in: true, login_url: url },
464
493
  });
465
494
  }
466
- // Org disambiguation: if signed in, no org arg, list the user's orgs.
467
- if (!input?.org) {
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
+ {
468
499
  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 };
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
+ }
480
515
  }
481
516
  }
482
517
  }