@cloudgrid-io/mcp 0.3.0 → 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.
- package/package.json +1 -1
- package/src/index.js +4 -1
- package/src/tools.js +137 -49
- package/src/web.js +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudgrid-io/mcp",
|
|
3
|
-
"version": "0.3.
|
|
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/index.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// Desktop). Full toolset, including the CLI-wrapping tools. Identity comes from
|
|
6
6
|
// the shared ~/.cloudgrid/credentials file, so it interoperates with the CLI.
|
|
7
7
|
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
8
9
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
10
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
11
|
import { registerTools } from "./tools.js";
|
|
@@ -15,6 +16,8 @@ import {
|
|
|
15
16
|
credentialsPath,
|
|
16
17
|
} from "./auth.js";
|
|
17
18
|
|
|
19
|
+
const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url)));
|
|
20
|
+
|
|
18
21
|
const ctx = {
|
|
19
22
|
edition: "local",
|
|
20
23
|
state: { pendingLoginCode: null, lastAnonClaim: null, lastDrop: null, anonCookie: null },
|
|
@@ -25,7 +28,7 @@ const ctx = {
|
|
|
25
28
|
savedLocationNote: () => `Credentials saved to ${credentialsPath()}.`,
|
|
26
29
|
};
|
|
27
30
|
|
|
28
|
-
const server = new McpServer({ name: "cloudgrid-mcp", version
|
|
31
|
+
const server = new McpServer({ name: "cloudgrid-mcp", version });
|
|
29
32
|
registerTools(server, ctx);
|
|
30
33
|
|
|
31
34
|
const transport = new StdioServerTransport();
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
.
|
|
333
|
-
|
|
334
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
352
|
-
|
|
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
|
|
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.
|
|
428
|
+
server.registerTool(
|
|
367
429
|
"cloudgrid_login",
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
|
377
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
412
|
-
|
|
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.
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
|
519
|
+
return okResult(await runVisibility(ctx, input || {}));
|
|
432
520
|
} catch (err) {
|
|
433
521
|
return fail(err.message);
|
|
434
522
|
}
|
package/src/web.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
//
|
|
15
15
|
// Run: PORT=8080 node src/web.js Health: GET /healthz
|
|
16
16
|
|
|
17
|
+
import { readFileSync } from "node:fs";
|
|
17
18
|
import { randomUUID } from "node:crypto";
|
|
18
19
|
import express from "express";
|
|
19
20
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -22,6 +23,8 @@ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
|
22
23
|
import { registerTools, decodeJwt } from "./tools.js";
|
|
23
24
|
import { mountOAuth, bearerChallenge } from "./oauth.js";
|
|
24
25
|
|
|
26
|
+
const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url)));
|
|
27
|
+
|
|
25
28
|
const PORT = Number(process.env.PORT || 8080);
|
|
26
29
|
|
|
27
30
|
// This server's public origin — used in OAuth metadata and the interstitial.
|
|
@@ -128,7 +131,7 @@ app.post("/mcp", async (req, res) => {
|
|
|
128
131
|
delete sessionAuth[transport.sessionId];
|
|
129
132
|
}
|
|
130
133
|
};
|
|
131
|
-
const server = new McpServer({ name: "cloudgrid-mcp-web", version
|
|
134
|
+
const server = new McpServer({ name: "cloudgrid-mcp-web", version });
|
|
132
135
|
registerTools(server, makeWebContext(newSessionId));
|
|
133
136
|
await server.connect(transport);
|
|
134
137
|
await transport.handleRequest(req, res, req.body);
|