@cloudgrid-io/mcp 0.4.0 → 0.4.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/tools.js +61 -28
- package/src/widgets/org-picker.html +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudgrid-io/mcp",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.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
|
@@ -150,6 +150,15 @@ function looksLikeFullHtml(s) {
|
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
async function runDrop(ctx, { html, path: filePath, filename, anonymous, org, fresh }) {
|
|
153
|
+
// Defensive: the web edition schema excludes `path`, but if a model still
|
|
154
|
+
// passes one (e.g. from a cached tool description), reject early with a
|
|
155
|
+
// clear explanation.
|
|
156
|
+
if (ctx.edition === "web" && filePath) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
"The hosted server cannot read local files — pass the full document as `html` instead of a `path`.",
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
153
162
|
let bytes;
|
|
154
163
|
let name;
|
|
155
164
|
let type;
|
|
@@ -485,22 +494,35 @@ export function registerTools(server, ctx) {
|
|
|
485
494
|
|
|
486
495
|
// ── Direct-API tools (both editions) ──────────────────────────────────────
|
|
487
496
|
|
|
488
|
-
// Drop — both editions
|
|
489
|
-
server
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
497
|
+
// Drop — both editions, but the input schema is edition-aware: the web
|
|
498
|
+
// edition removes `path` (the cloud server cannot read local files) and
|
|
499
|
+
// strengthens `html` so the model always pastes the full document inline.
|
|
500
|
+
const dropInputSchema = ctx.edition === "web"
|
|
501
|
+
? {
|
|
502
|
+
html: z.string().optional().describe(
|
|
503
|
+
"The COMPLETE HTML document to publish — paste the full file contents here (not a path). " +
|
|
504
|
+
"For a game, include all HTML/CSS/JS inline so it runs standalone. " +
|
|
505
|
+
"A fragment is wrapped into a full document automatically.",
|
|
506
|
+
),
|
|
507
|
+
filename: z.string().optional().describe("Filename to present. Defaults to index.html for inline HTML."),
|
|
508
|
+
anonymous: z.boolean().optional().describe("Force an anonymous drop even if the user is signed in."),
|
|
509
|
+
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."),
|
|
510
|
+
fresh: z.boolean().optional().describe("Force a new drop even if you already dropped in this session (default: update in place)."),
|
|
511
|
+
}
|
|
512
|
+
: {
|
|
494
513
|
html: z.string().optional().describe("Inline HTML to publish. A fragment is wrapped into a full document."),
|
|
495
514
|
path: z.string().optional().describe("Path to a local file to upload instead of inline HTML."),
|
|
496
515
|
filename: z.string().optional().describe("Filename to present. Defaults to index.html for inline HTML."),
|
|
497
516
|
anonymous: z.boolean().optional().describe("Force an anonymous drop even if the user is signed in."),
|
|
498
517
|
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."),
|
|
499
|
-
fresh: z
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
518
|
+
fresh: z.boolean().optional().describe("Force a new drop even if you already dropped in this session (default: update in place)."),
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
server.registerTool(
|
|
522
|
+
"cloudgrid_drop",
|
|
523
|
+
{
|
|
524
|
+
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.",
|
|
525
|
+
inputSchema: dropInputSchema,
|
|
504
526
|
outputSchema: {
|
|
505
527
|
url: z.string().optional().describe("The public URL of the drop."),
|
|
506
528
|
status: z.enum(["created", "updated", "unchanged"]).optional().describe("What happened to the drop."),
|
|
@@ -531,6 +553,15 @@ export function registerTools(server, ctx) {
|
|
|
531
553
|
},
|
|
532
554
|
async (input) => {
|
|
533
555
|
try {
|
|
556
|
+
// Web edition: reject `path` early — the hosted server cannot read
|
|
557
|
+
// local files. The schema already omits it, but a model with a
|
|
558
|
+
// cached tool description might still send one.
|
|
559
|
+
if (ctx.edition === "web" && input?.path) {
|
|
560
|
+
return fail(
|
|
561
|
+
"The hosted server cannot read local files — pass the full document as `html` instead of a `path`.",
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
|
|
534
565
|
// Web edition: sign-in guidance when unauthenticated.
|
|
535
566
|
if (ctx.edition === "web" && input?.anonymous !== true) {
|
|
536
567
|
const token = await ctx.getToken();
|
|
@@ -541,27 +572,29 @@ export function registerTools(server, ctx) {
|
|
|
541
572
|
structured: { needs_sign_in: true, login_url: url },
|
|
542
573
|
});
|
|
543
574
|
}
|
|
544
|
-
//
|
|
545
|
-
//
|
|
546
|
-
//
|
|
575
|
+
// Stateless org disambiguation — no dependency on prior-call state
|
|
576
|
+
// so it works even when the client reconnects on every tool call
|
|
577
|
+
// (ChatGPT Apps SDK behaviour).
|
|
547
578
|
{
|
|
548
579
|
const orgs = await fetchUserOrgs(token);
|
|
549
580
|
const suppliedOrg = input?.org;
|
|
550
581
|
const validOrg = suppliedOrg && orgs.some((o) => o.slug === suppliedOrg);
|
|
551
|
-
if (
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
582
|
+
if (validOrg) {
|
|
583
|
+
// Supplied org matches a real org slug — publish to it.
|
|
584
|
+
input = { ...(input || {}), org: suppliedOrg };
|
|
585
|
+
} else if (orgs.length > 1) {
|
|
586
|
+
// No valid org supplied and multiple orgs — ask once.
|
|
587
|
+
const lines = ["Which org should this be published to?"];
|
|
588
|
+
for (const o of orgs) lines.push(` ${o.slug} — ${o.name} (${o.role})`);
|
|
589
|
+
lines.push("Pass the org slug in the org parameter to publish.");
|
|
590
|
+
return okResult({
|
|
591
|
+
text: lines.join("\n"),
|
|
592
|
+
structured: { needs_org: true, orgs },
|
|
593
|
+
meta: { "openai/outputTemplate": ORG_PICKER_URI },
|
|
594
|
+
});
|
|
595
|
+
} else if (orgs.length === 1) {
|
|
596
|
+
// Single org — publish to it silently.
|
|
597
|
+
input = { ...(input || {}), org: orgs[0].slug };
|
|
565
598
|
}
|
|
566
599
|
}
|
|
567
600
|
}
|
|
@@ -65,7 +65,6 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;b
|
|
|
65
65
|
var input = (window.openai && window.openai.toolInput) || {};
|
|
66
66
|
var args = {};
|
|
67
67
|
if (input.html) args.html = input.html;
|
|
68
|
-
if (input.path) args.path = input.path;
|
|
69
68
|
if (input.filename) args.filename = input.filename;
|
|
70
69
|
if (input.fresh !== undefined) args.fresh = input.fresh;
|
|
71
70
|
args.org = slug;
|