@cloudgrid-io/mcp 0.16.1 → 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 +1 -1
- package/src/tools.js +76 -27
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": {
|
package/src/tools.js
CHANGED
|
@@ -2330,11 +2330,66 @@ function isCloudgridHost(hostname) {
|
|
|
2330
2330
|
return h === "cloudgrid.io" || h.endsWith(".cloudgrid.io");
|
|
2331
2331
|
}
|
|
2332
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
|
+
|
|
2333
2387
|
// Fetch a drop's/inspiration's current deployed HTML inline as text so an agent
|
|
2334
2388
|
// that lost the content can edit it and re-plug in place. Resolves the fetch URL
|
|
2335
|
-
// (explicit url → session lastDrop → composed grid+slug),
|
|
2336
|
-
//
|
|
2337
|
-
//
|
|
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.
|
|
2338
2393
|
export async function runSource(ctx, { entity_id, url, grid, slug } = {}) {
|
|
2339
2394
|
const last = ctx.state.lastDrop;
|
|
2340
2395
|
// Resolution order: explicit url → session lastDrop.url (if entity_id matches
|
|
@@ -2367,6 +2422,23 @@ export async function runSource(ctx, { entity_id, url, grid, slug } = {}) {
|
|
|
2367
2422
|
}
|
|
2368
2423
|
|
|
2369
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).
|
|
2370
2442
|
let res;
|
|
2371
2443
|
try {
|
|
2372
2444
|
res = await fetch(resolvedUrl, {
|
|
@@ -2398,30 +2470,7 @@ export async function runSource(ctx, { entity_id, url, grid, slug } = {}) {
|
|
|
2398
2470
|
}
|
|
2399
2471
|
|
|
2400
2472
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
2401
|
-
|
|
2402
|
-
const truncated = totalBytes > SOURCE_MAX_BYTES;
|
|
2403
|
-
const html = (truncated ? buf.subarray(0, SOURCE_MAX_BYTES) : buf).toString("utf-8");
|
|
2404
|
-
|
|
2405
|
-
const lines = [
|
|
2406
|
-
`Current source for ${resolvedUrl} (${totalBytes} bytes) — edit this and re-plug with target_entity_id to update the same URL:`,
|
|
2407
|
-
];
|
|
2408
|
-
if (truncated) {
|
|
2409
|
-
lines.push(
|
|
2410
|
-
"(too large to return in full; re-plug needs the complete document — consider that this drop may be multi-file)",
|
|
2411
|
-
);
|
|
2412
|
-
}
|
|
2413
|
-
lines.push("", html);
|
|
2414
|
-
|
|
2415
|
-
return {
|
|
2416
|
-
text: lines.join("\n"),
|
|
2417
|
-
structured: {
|
|
2418
|
-
url: resolvedUrl,
|
|
2419
|
-
entity_id: entity_id ?? last?.entity_id ?? null,
|
|
2420
|
-
bytes: totalBytes,
|
|
2421
|
-
truncated,
|
|
2422
|
-
html,
|
|
2423
|
-
},
|
|
2424
|
-
};
|
|
2473
|
+
return shapeSourceResult(resolvedUrl, eid, buf.toString("utf-8"));
|
|
2425
2474
|
}
|
|
2426
2475
|
|
|
2427
2476
|
// ── Registration ───────────────────────────────────────────────────────────────
|