@cloudgrid-io/mcp 0.3.1 → 0.3.2

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 +137 -49
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudgrid-io/mcp",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
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
@@ -30,6 +30,9 @@ function ok(text) {
30
30
  function fail(text) {
31
31
  return { content: [{ type: "text", text }], isError: true };
32
32
  }
33
+ function okResult({ text, structured }) {
34
+ return { content: [{ type: "text", text }], structuredContent: structured };
35
+ }
33
36
 
34
37
  // ── CLI wrapping (local edition only) ──────────────────────────────────────────
35
38
  async function runCloudgrid(args) {
@@ -185,15 +188,28 @@ async function runDrop(ctx, { html, path: filePath, filename, anonymous, org, fr
185
188
 
186
189
  if (res.status === 202) {
187
190
  // Idempotent no-op — the bytes matched the live version exactly.
188
- return `No change — this exact content is already live: ${data.url ?? ctx.state.lastDrop?.url ?? ""}`.trim();
191
+ const url = (data.url ?? ctx.state.lastDrop?.url ?? "").trim();
192
+ return {
193
+ text: `No change — this exact content is already live: ${url}`,
194
+ structured: { url, status: "unchanged" },
195
+ };
189
196
  }
190
197
 
191
198
  if (res.status === 200) {
192
199
  // Updated in place: same URL, new version, views/reactions intact.
193
- const lines = [`Updated in place — same link: ${data.url ?? ctx.state.lastDrop?.url ?? ""}`.trim()];
200
+ const url = (data.url ?? ctx.state.lastDrop?.url ?? "").trim();
201
+ const lines = [`Updated in place — same link: ${url}`];
194
202
  if (data.owned_by === "authenticated") lines.push("Owned by you.");
195
203
  if (data.expires_at) lines.push(`Expires ${data.expires_at}.`);
196
- return lines.join("\n");
204
+ return {
205
+ text: lines.join("\n"),
206
+ structured: {
207
+ url,
208
+ status: "updated",
209
+ ...(data.owned_by ? { owned_by: data.owned_by } : {}),
210
+ ...(data.expires_at ? { expires_at: data.expires_at } : {}),
211
+ },
212
+ };
197
213
  }
198
214
 
199
215
  if (data.owned_by === "authenticated") {
@@ -201,7 +217,15 @@ async function runDrop(ctx, { html, path: filePath, filename, anonymous, org, fr
201
217
  const lines = [`Published to your org: ${data.url}`, "Owned by you."];
202
218
  if (data.expires_at) lines.push(`Expires ${data.expires_at}.`);
203
219
  lines.push("Drop again in this session to update it in place (same link); pass fresh to start a new one.");
204
- return lines.join("\n");
220
+ return {
221
+ text: lines.join("\n"),
222
+ structured: {
223
+ url: data.url,
224
+ status: "created",
225
+ owned_by: "authenticated",
226
+ ...(data.expires_at ? { expires_at: data.expires_at } : {}),
227
+ },
228
+ };
205
229
  }
206
230
 
207
231
  // 201 — created new (first drop, fresh: true, or the server fell back to create).
@@ -220,7 +244,14 @@ async function runDrop(ctx, { html, path: filePath, filename, anonymous, org, fr
220
244
  if (data.expires_at) lines.push(`Expires ${data.expires_at} — anonymous drops last 7 days.`);
221
245
  if (data.claim_url) lines.push("Sign in, then run cloudgrid_claim to keep it past 7 days.");
222
246
  lines.push("Drop again in this session to update it in place (same link); pass fresh to start a new one.");
223
- return lines.join("\n");
247
+ return {
248
+ text: lines.join("\n"),
249
+ structured: {
250
+ url: data.url,
251
+ status: "created",
252
+ ...(data.expires_at ? { expires_at: data.expires_at } : {}),
253
+ },
254
+ };
224
255
  }
225
256
 
226
257
  async function runClaim(ctx, { claim_token, claim_url }) {
@@ -264,13 +295,24 @@ async function runClaim(ctx, { claim_token, claim_url }) {
264
295
  throw new Error(`Claim failed (HTTP ${res.status}): ${msg}`);
265
296
  }
266
297
  const claimed = Array.isArray(data?.claimed) ? data.claimed : [];
267
- if (claimed.length === 0) return "Nothing to claim — it may already be claimed or expired.";
298
+ if (claimed.length === 0) {
299
+ return {
300
+ text: "Nothing to claim — it may already be claimed or expired.",
301
+ structured: { claimed: 0, urls: [] },
302
+ };
303
+ }
268
304
  ctx.state.lastAnonClaim = null;
269
305
  const lines = [`Claimed ${claimed.length}, now yours:`];
270
306
  for (const c of claimed) {
271
307
  lines.push(`${c.url}${c.new_expires_at ? ` (expires ${c.new_expires_at})` : ""}`);
272
308
  }
273
- return lines.join("\n");
309
+ return {
310
+ text: lines.join("\n"),
311
+ structured: {
312
+ claimed: claimed.length,
313
+ urls: claimed.map((c) => c.url),
314
+ },
315
+ };
274
316
  }
275
317
 
276
318
 
@@ -309,7 +351,13 @@ async function runVisibility(ctx, { target, visibility, org }) {
309
351
  }
310
352
  const lines = [`Visibility is now ${visibility}.`];
311
353
  if (data?.url) lines.push(data.url);
312
- return lines.join("\n");
354
+ return {
355
+ text: lines.join("\n"),
356
+ structured: {
357
+ visibility,
358
+ ...(data?.url ? { url: data.url } : {}),
359
+ },
360
+ };
313
361
  }
314
362
 
315
363
  // ── Registration ───────────────────────────────────────────────────────────────
@@ -319,24 +367,32 @@ export function registerTools(server, ctx) {
319
367
  // ── Direct-API tools (both editions) ──────────────────────────────────────
320
368
 
321
369
  // Drop — both editions.
322
- server.tool(
370
+ server.registerTool(
323
371
  "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
372
  {
326
- html: z.string().optional().describe("Inline HTML to publish. A fragment is wrapped into a full document."),
327
- path: z.string().optional().describe("Path to a local file to upload instead of inline HTML."),
328
- filename: z.string().optional().describe("Filename to present. Defaults to index.html for inline HTML."),
329
- anonymous: z.boolean().optional().describe("Force an anonymous drop even if the user is signed in."),
330
- org: z.string().optional().describe("Org slug to publish into when signed in. Defaults to the active org."),
331
- fresh: z
332
- .boolean()
333
- .optional()
334
- .describe("Force a new drop even if you already dropped in this session (default: update in place)."),
373
+ 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.",
374
+ inputSchema: {
375
+ html: z.string().optional().describe("Inline HTML to publish. A fragment is wrapped into a full document."),
376
+ path: z.string().optional().describe("Path to a local file to upload instead of inline HTML."),
377
+ filename: z.string().optional().describe("Filename to present. Defaults to index.html for inline HTML."),
378
+ anonymous: z.boolean().optional().describe("Force an anonymous drop even if the user is signed in."),
379
+ org: z.string().optional().describe("Org slug to publish into when signed in. Defaults to the active org."),
380
+ fresh: z
381
+ .boolean()
382
+ .optional()
383
+ .describe("Force a new drop even if you already dropped in this session (default: update in place)."),
384
+ },
385
+ 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."),
388
+ owned_by: z.string().optional().describe("Ownership class, e.g. 'authenticated'."),
389
+ expires_at: z.string().optional().describe("Expiry timestamp, if any."),
390
+ },
391
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
335
392
  },
336
- { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
337
393
  async (input) => {
338
394
  try {
339
- return ok(await runDrop(ctx, input || {}));
395
+ return okResult(await runDrop(ctx, input || {}));
340
396
  } catch (err) {
341
397
  return fail(err.message);
342
398
  }
@@ -344,17 +400,23 @@ export function registerTools(server, ctx) {
344
400
  );
345
401
 
346
402
  // Claim — both editions.
347
- server.tool(
403
+ server.registerTool(
348
404
  "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
405
  {
351
- claim_token: z.string().optional().describe("The claim token from an anonymous drop."),
352
- claim_url: z.string().optional().describe("The claim_url from an anonymous drop; the token is read from it."),
406
+ 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.",
407
+ inputSchema: {
408
+ claim_token: z.string().optional().describe("The claim token from an anonymous drop."),
409
+ claim_url: z.string().optional().describe("The claim_url from an anonymous drop; the token is read from it."),
410
+ },
411
+ outputSchema: {
412
+ claimed: z.number().describe("Number of drops claimed."),
413
+ urls: z.array(z.string()).describe("URLs of the claimed drops."),
414
+ },
415
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
353
416
  },
354
- { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
355
417
  async (input) => {
356
418
  try {
357
- return ok(await runClaim(ctx, input || {}));
419
+ return okResult(await runClaim(ctx, input || {}));
358
420
  } catch (err) {
359
421
  return fail(err.message);
360
422
  }
@@ -363,30 +425,44 @@ export function registerTools(server, ctx) {
363
425
 
364
426
  // Login — both editions. Local opens a browser and saves to the credentials
365
427
  // file; web returns the URL and saves to the session.
366
- server.tool(
428
+ server.registerTool(
367
429
  "cloudgrid_login",
368
- "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.",
369
- {},
370
- { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
430
+ {
431
+ 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.",
432
+ inputSchema: {},
433
+ outputSchema: {
434
+ login_url: z.string().describe("URL to open in a browser to complete sign-in."),
435
+ },
436
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
437
+ },
371
438
  async () => {
372
439
  const code = newLoginCode();
373
440
  ctx.state.pendingLoginCode = code;
374
441
  const url = buildLoginUrl(code);
375
442
  if (ctx.canOpenBrowser) tryOpenBrowser(url);
376
- return ok(
377
- `To sign in, open this URL in your browser and finish with Google:\n${url}\n\n` +
443
+ return {
444
+ content: [{ type: "text", text:
445
+ `To sign in, open this URL in your browser and finish with Google:\n${url}\n\n` +
378
446
  `After you complete it, run cloudgrid_login_status to finish signing in.`,
379
- );
447
+ }],
448
+ structuredContent: { login_url: url },
449
+ };
380
450
  },
381
451
  );
382
452
 
383
- server.tool(
453
+ server.registerTool(
384
454
  "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
455
  {
387
- code: z.string().optional().describe("The sign-in code. Defaults to the most recent cloudgrid_login."),
456
+ 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.",
457
+ inputSchema: {
458
+ code: z.string().optional().describe("The sign-in code. Defaults to the most recent cloudgrid_login."),
459
+ },
460
+ outputSchema: {
461
+ status: z.enum(["authenticated", "pending"]).describe("Current sign-in state."),
462
+ email: z.string().optional().describe("Signed-in email, when authenticated."),
463
+ },
464
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
388
465
  },
389
- { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
390
466
  async (input) => {
391
467
  const code = input?.code || ctx.state.pendingLoginCode;
392
468
  if (!code) return fail("No sign-in is in progress. Run cloudgrid_login first.");
@@ -405,30 +481,42 @@ export function registerTools(server, ctx) {
405
481
  }
406
482
  ctx.state.pendingLoginCode = null;
407
483
  const who = info?.email ? ` as ${info.email}` : "";
408
- return ok(`Signed in${who}. ${ctx.savedLocationNote()}`);
484
+ return {
485
+ content: [{ type: "text", text: `Signed in${who}. ${ctx.savedLocationNote()}` }],
486
+ structuredContent: { status: "authenticated", ...(info?.email ? { email: info.email } : {}) },
487
+ };
409
488
  }
410
489
  if (status.status === "pending" || status.status === "not_started") {
411
- return ok(
412
- "Still waiting for you to finish signing in. Open the URL from cloudgrid_login " +
490
+ return {
491
+ content: [{ type: "text", text:
492
+ "Still waiting for you to finish signing in. Open the URL from cloudgrid_login " +
413
493
  "in your browser, complete it with Google, then run cloudgrid_login_status again.",
414
- );
494
+ }],
495
+ structuredContent: { status: "pending" },
496
+ };
415
497
  }
416
498
  return fail("The sign-in window expired (5 minutes). Run cloudgrid_login to start again.");
417
499
  },
418
500
  );
419
501
 
420
- server.tool(
502
+ server.registerTool(
421
503
  "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
504
  {
424
- visibility: z.enum(["private", "space", "authenticated", "org", "link"]).describe("The new scope."),
425
- target: z.string().optional().describe("Entity id. Defaults to this session's last drop."),
426
- org: z.string().optional().describe("Org of the entity. Defaults to the active org."),
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.",
506
+ inputSchema: {
507
+ visibility: z.enum(["private", "space", "authenticated", "org", "link"]).describe("The new scope."),
508
+ target: z.string().optional().describe("Entity id. Defaults to this session's last drop."),
509
+ org: z.string().optional().describe("Org of the entity. Defaults to the active org."),
510
+ },
511
+ outputSchema: {
512
+ visibility: z.string().describe("The visibility that was set."),
513
+ url: z.string().optional().describe("URL of the entity, if returned."),
514
+ },
515
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
427
516
  },
428
- { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
429
517
  async (input) => {
430
518
  try {
431
- return ok(await runVisibility(ctx, input || {}));
519
+ return okResult(await runVisibility(ctx, input || {}));
432
520
  } catch (err) {
433
521
  return fail(err.message);
434
522
  }