@cloudgrid-io/mcp 0.16.0 → 0.16.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 +3 -2
- package/src/tools.js +86 -29
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudgrid-io/mcp",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.2",
|
|
4
4
|
"description": "MCP server for CloudGrid. Three editions: a local stdio server (full toolset), a hosted web server (light, CLI-free toolset), and a public docs server (search_cloudgrid_documentation) over MCP Streamable HTTP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
"smoke:docs": "node test/smoke-docs.mjs",
|
|
40
40
|
"start:docs": "node src/docs.js",
|
|
41
41
|
"snapshot:corpus": "node scripts/snapshot-corpus.mjs",
|
|
42
|
-
"build:mcpb": "node scripts/build-mcpb.mjs"
|
|
42
|
+
"build:mcpb": "node scripts/build-mcpb.mjs",
|
|
43
|
+
"test:widgets": "node test/apps-widgets.test.mjs"
|
|
43
44
|
},
|
|
44
45
|
"dependencies": {
|
|
45
46
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
package/src/tools.js
CHANGED
|
@@ -45,6 +45,14 @@ const AUTHED_HTML_MAX_BYTES = 25_000_000;
|
|
|
45
45
|
const CONSOLE_URL = "https://console.cloudgrid.io/";
|
|
46
46
|
|
|
47
47
|
// ── Widget resources (ChatGPT Apps SDK, web edition only) ────────────────────
|
|
48
|
+
// The Apps-SDK UI widgets (openai/outputTemplate → a ui:// html resource) render
|
|
49
|
+
// as a broken black frame in ChatGPT today, hiding the plain-text result. Gate
|
|
50
|
+
// them behind an env flag, DEFAULT OFF, so the drop/plug result is text-first
|
|
51
|
+
// (the live URL is already the first line of the text content) and the widget is
|
|
52
|
+
// optional. Flip MCP_APPS_WIDGETS=1 in the platform manifest to re-enable once
|
|
53
|
+
// the widget HTML is verified to render. The resources stay registered either
|
|
54
|
+
// way (harmless when no tool references them via outputTemplate).
|
|
55
|
+
const APPS_WIDGETS_ENABLED = process.env.MCP_APPS_WIDGETS === "1";
|
|
48
56
|
const LIVE_RESULT_URI = "ui://cloudgrid/live-result.html";
|
|
49
57
|
// URI/resource-name/filename stay `org-picker` — that's the stable contract the
|
|
50
58
|
// web card is registered under; only the JS identifier moves toward grid.
|
|
@@ -753,7 +761,7 @@ export async function resolveGridOrAsk(ctx, { token, suppliedGrid, edition }, de
|
|
|
753
761
|
// `needs_grid` is the new field; `needs_org`/`orgs` are kept as aliases
|
|
754
762
|
// so the existing org-picker.html web widget (reads data.orgs) still works.
|
|
755
763
|
structured: { needs_grid: true, needs_org: true, grids: annotated, orgs: annotated },
|
|
756
|
-
...(edition === "web" ? { meta: { "openai/outputTemplate": GRID_PICKER_URI } } : {}),
|
|
764
|
+
...(edition === "web" && APPS_WIDGETS_ENABLED ? { meta: { "openai/outputTemplate": GRID_PICKER_URI } } : {}),
|
|
757
765
|
},
|
|
758
766
|
};
|
|
759
767
|
}
|
|
@@ -2322,11 +2330,66 @@ function isCloudgridHost(hostname) {
|
|
|
2322
2330
|
return h === "cloudgrid.io" || h.endsWith(".cloudgrid.io");
|
|
2323
2331
|
}
|
|
2324
2332
|
|
|
2333
|
+
// Shape an HTML string into the gridctl_source result (shared by the API-read
|
|
2334
|
+
// and the public-fetch paths). Caps the body at SOURCE_MAX_BYTES.
|
|
2335
|
+
function shapeSourceResult(sourceUrl, entityId, htmlStr) {
|
|
2336
|
+
const buf = Buffer.from(htmlStr, "utf-8");
|
|
2337
|
+
const totalBytes = buf.length;
|
|
2338
|
+
const truncated = totalBytes > SOURCE_MAX_BYTES;
|
|
2339
|
+
const html = truncated ? buf.subarray(0, SOURCE_MAX_BYTES).toString("utf-8") : htmlStr;
|
|
2340
|
+
const lines = [
|
|
2341
|
+
`Current source for ${sourceUrl} (${totalBytes} bytes) — edit this and re-plug with target_entity_id to update the same URL:`,
|
|
2342
|
+
];
|
|
2343
|
+
if (truncated) {
|
|
2344
|
+
lines.push(
|
|
2345
|
+
"(too large to return in full; re-plug needs the complete document — consider that this drop may be multi-file)",
|
|
2346
|
+
);
|
|
2347
|
+
}
|
|
2348
|
+
lines.push("", html);
|
|
2349
|
+
return {
|
|
2350
|
+
text: lines.join("\n"),
|
|
2351
|
+
structured: { url: sourceUrl, entity_id: entityId ?? null, bytes: totalBytes, truncated, html },
|
|
2352
|
+
};
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
// Read an inspiration's HTML via the API (server-side storage read) instead of
|
|
2356
|
+
// fetching the public *.cloudgrid.io URL. Critical on the hosted edition: the
|
|
2357
|
+
// MCP pod can reach the API (it POSTs /api/v2/plug) but CANNOT egress to the
|
|
2358
|
+
// public ingress ("fetch failed"). getInspirationSource resolves directly by
|
|
2359
|
+
// entity_id when the path segment is a UUID (no org context needed), else by
|
|
2360
|
+
// slug + the X-CloudGrid-Grid active-org header. Returns the html string, or
|
|
2361
|
+
// null if the API can't serve it (→ caller falls back to the direct fetch).
|
|
2362
|
+
async function readInspirationSourceViaApi(ctx, { entityId, slug, grid }) {
|
|
2363
|
+
let token = null;
|
|
2364
|
+
try { token = await ctx.getToken(); } catch { /* anonymous is fine for public */ }
|
|
2365
|
+
const baseHeaders = token ? { Authorization: `Bearer ${token}` } : {};
|
|
2366
|
+
const attempts = [];
|
|
2367
|
+
if (entityId) attempts.push({ seg: entityId, grid: null }); // UUID → resolves by id, no org ctx
|
|
2368
|
+
if (slug) attempts.push({ seg: slug, grid: grid || null }); // slug → needs org (grid) context
|
|
2369
|
+
for (const a of attempts) {
|
|
2370
|
+
try {
|
|
2371
|
+
const headers = { ...baseHeaders };
|
|
2372
|
+
if (a.grid) headers["X-CloudGrid-Grid"] = a.grid;
|
|
2373
|
+
const res = await fetch(
|
|
2374
|
+
`${API_BASE}/api/v2/inspirations/${encodeURIComponent(a.seg)}/source`,
|
|
2375
|
+
{ method: "GET", headers, signal: AbortSignal.timeout(15_000) },
|
|
2376
|
+
);
|
|
2377
|
+
if (!res.ok) continue;
|
|
2378
|
+
const data = await res.json().catch(() => null);
|
|
2379
|
+
if (data && typeof data.html === "string") return data.html;
|
|
2380
|
+
} catch {
|
|
2381
|
+
// try the next attempt, then the public-fetch fallback
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
return null;
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2325
2387
|
// Fetch a drop's/inspiration's current deployed HTML inline as text so an agent
|
|
2326
2388
|
// that lost the content can edit it and re-plug in place. Resolves the fetch URL
|
|
2327
|
-
// (explicit url → session lastDrop → composed grid+slug),
|
|
2328
|
-
//
|
|
2329
|
-
//
|
|
2389
|
+
// (explicit url → session lastDrop → composed grid+slug), then reads the HTML
|
|
2390
|
+
// via the API (reachable) BEFORE falling back to a direct `*.cloudgrid.io` fetch
|
|
2391
|
+
// (SSRF-guarded, capped at SOURCE_MAX_BYTES). Uses the global `fetch` seam so
|
|
2392
|
+
// tests can mock it.
|
|
2330
2393
|
export async function runSource(ctx, { entity_id, url, grid, slug } = {}) {
|
|
2331
2394
|
const last = ctx.state.lastDrop;
|
|
2332
2395
|
// Resolution order: explicit url → session lastDrop.url (if entity_id matches
|
|
@@ -2359,6 +2422,23 @@ export async function runSource(ctx, { entity_id, url, grid, slug } = {}) {
|
|
|
2359
2422
|
}
|
|
2360
2423
|
|
|
2361
2424
|
const resolvedUrl = parsed.toString();
|
|
2425
|
+
const eid = entity_id ?? (last && (!entity_id || last.entity_id === entity_id) ? last.entity_id : null);
|
|
2426
|
+
|
|
2427
|
+
// ── API-first (reachable) ────────────────────────────────────────────────
|
|
2428
|
+
// Read the HTML from the API server-side rather than fetching the public URL.
|
|
2429
|
+
// The hosted MCP pod can reach the API but NOT the public *.cloudgrid.io
|
|
2430
|
+
// ingress ("fetch failed") — this is the fix for hosted edit-in-place. Derive
|
|
2431
|
+
// the grid/slug hint from the inspiration path URL (`grid.cloudgrid.io/slug`).
|
|
2432
|
+
const host = parsed.hostname;
|
|
2433
|
+
const gridHint = grid ?? (host.endsWith(".cloudgrid.io") && !host.includes("--") && host.split(".").length === 3
|
|
2434
|
+
? host.split(".")[0] : null);
|
|
2435
|
+
const slugHint = slug ?? (parsed.pathname.replace(/^\/+/, "").split("/")[0] || null);
|
|
2436
|
+
const apiHtml = await readInspirationSourceViaApi(ctx, { entityId: eid, slug: slugHint, grid: gridHint });
|
|
2437
|
+
if (apiHtml != null) return shapeSourceResult(resolvedUrl, eid, apiHtml);
|
|
2438
|
+
|
|
2439
|
+
// ── Fallback: direct public fetch ────────────────────────────────────────
|
|
2440
|
+
// Local edition has normal egress; last resort on hosted (e.g. a runtime app
|
|
2441
|
+
// whose source the inspiration route can't serve).
|
|
2362
2442
|
let res;
|
|
2363
2443
|
try {
|
|
2364
2444
|
res = await fetch(resolvedUrl, {
|
|
@@ -2390,30 +2470,7 @@ export async function runSource(ctx, { entity_id, url, grid, slug } = {}) {
|
|
|
2390
2470
|
}
|
|
2391
2471
|
|
|
2392
2472
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
2393
|
-
|
|
2394
|
-
const truncated = totalBytes > SOURCE_MAX_BYTES;
|
|
2395
|
-
const html = (truncated ? buf.subarray(0, SOURCE_MAX_BYTES) : buf).toString("utf-8");
|
|
2396
|
-
|
|
2397
|
-
const lines = [
|
|
2398
|
-
`Current source for ${resolvedUrl} (${totalBytes} bytes) — edit this and re-plug with target_entity_id to update the same URL:`,
|
|
2399
|
-
];
|
|
2400
|
-
if (truncated) {
|
|
2401
|
-
lines.push(
|
|
2402
|
-
"(too large to return in full; re-plug needs the complete document — consider that this drop may be multi-file)",
|
|
2403
|
-
);
|
|
2404
|
-
}
|
|
2405
|
-
lines.push("", html);
|
|
2406
|
-
|
|
2407
|
-
return {
|
|
2408
|
-
text: lines.join("\n"),
|
|
2409
|
-
structured: {
|
|
2410
|
-
url: resolvedUrl,
|
|
2411
|
-
entity_id: entity_id ?? last?.entity_id ?? null,
|
|
2412
|
-
bytes: totalBytes,
|
|
2413
|
-
truncated,
|
|
2414
|
-
html,
|
|
2415
|
-
},
|
|
2416
|
-
};
|
|
2473
|
+
return shapeSourceResult(resolvedUrl, eid, buf.toString("utf-8"));
|
|
2417
2474
|
}
|
|
2418
2475
|
|
|
2419
2476
|
// ── Registration ───────────────────────────────────────────────────────────────
|
|
@@ -2531,7 +2588,7 @@ export function registerTools(server, ctx) {
|
|
|
2531
2588
|
login_url: z.string().optional().describe("Sign-in URL when authentication is needed."),
|
|
2532
2589
|
},
|
|
2533
2590
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
2534
|
-
...(ctx.edition === "web" ? {
|
|
2591
|
+
...(ctx.edition === "web" && APPS_WIDGETS_ENABLED ? {
|
|
2535
2592
|
_meta: {
|
|
2536
2593
|
ui: { resourceUri: LIVE_RESULT_URI, csp: WIDGET_CSP },
|
|
2537
2594
|
"openai/outputTemplate": LIVE_RESULT_URI,
|