@cloudgrid-io/mcp 0.20.28 → 0.20.30

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudgrid-io/mcp",
3
- "version": "0.20.28",
3
+ "version": "0.20.30",
4
4
  "description": "MCP access to CloudGrid — the live runtime environment where the user's apps run WITH the infrastructure they need out of the box: managed MongoDB, Redis (cache/queues/pub-sub), a pgvector store, persistent disk, and built-in AI with no API keys, in any language or stack, no Docker or Kubernetes to manage. Brainstorm, build, plug, and improve apps, games, dashboards, and tools; instant single-page publishes and full runtime-app deploys with a live URL — for developers and non-technical users. Three editions: local stdio (full toolset), hosted web (light, CLI-free), and a public docs-search server.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -53,12 +53,14 @@
53
53
  "test:fork": "node test/fork.test.mjs",
54
54
  "test:set-sharing": "node test/set-sharing.test.mjs",
55
55
  "test:check-deploy": "node test/check-deploy.test.mjs",
56
- "test:staleness": "node test/staleness.test.mjs"
56
+ "test:staleness": "node test/staleness.test.mjs",
57
+ "test:proxy": "node test/proxy.test.mjs"
57
58
  },
58
59
  "dependencies": {
59
60
  "@modelcontextprotocol/sdk": "^1.0.0",
60
61
  "express": "^4.22.2",
61
62
  "fflate": "^0.8.3",
63
+ "undici": "^8.7.0",
62
64
  "zod": "^3.23.0"
63
65
  },
64
66
  "license": "Apache-2.0",
package/src/auth.js CHANGED
@@ -45,7 +45,17 @@ export function buildLoginUrl(code) {
45
45
  // A 404 means the session for this code does not exist yet — the user has not
46
46
  // opened the sign-in URL — so it is reported as 'not_started', not an error.
47
47
  export async function pollStatusOnce(code) {
48
- const res = await fetch(`${API_BASE}/auth/status?code=${encodeURIComponent(code)}`);
48
+ let res;
49
+ try {
50
+ res = await fetch(`${API_BASE}/auth/status?code=${encodeURIComponent(code)}`);
51
+ } catch (err) {
52
+ const cause = err?.cause?.code || err?.cause?.message || err?.cause || err.message;
53
+ throw new Error(
54
+ `Can't reach api.cloudgrid.io (${cause}). ` +
55
+ `If you are behind a proxy, set HTTPS_PROXY; or use the hosted connector ` +
56
+ `https://mcp-connected.cloudgrid.io/mcp.`,
57
+ );
58
+ }
49
59
  if (res.status === 404) {
50
60
  return { status: "not_started" };
51
61
  }
package/src/index.js CHANGED
@@ -5,6 +5,9 @@
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 { installProxy } from "./proxy.js";
9
+ installProxy();
10
+
8
11
  import { readFileSync } from "node:fs";
9
12
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
13
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
package/src/proxy.js ADDED
@@ -0,0 +1,63 @@
1
+ // Corporate proxy support for Node's global fetch (undici).
2
+ //
3
+ // Node's built-in fetch ignores HTTPS_PROXY / HTTP_PROXY. This module reads
4
+ // the standard proxy env vars and, when set, installs an undici ProxyAgent as
5
+ // the global dispatcher so every subsequent fetch() honors the proxy.
6
+ //
7
+ // Fully guarded: if undici is unavailable or the env is unset, this is a
8
+ // silent no-op. Never throws at startup.
9
+
10
+ import { createRequire } from "node:module";
11
+
12
+ export function resolveProxy() {
13
+ const proxyUrl =
14
+ process.env.HTTPS_PROXY ||
15
+ process.env.https_proxy ||
16
+ process.env.HTTP_PROXY ||
17
+ process.env.http_proxy ||
18
+ null;
19
+
20
+ if (!proxyUrl) return null;
21
+
22
+ const raw =
23
+ process.env.NO_PROXY ||
24
+ process.env.no_proxy ||
25
+ "";
26
+
27
+ const noProxy = raw
28
+ .split(",")
29
+ .map((s) => s.trim())
30
+ .filter(Boolean);
31
+
32
+ return { proxyUrl, noProxy };
33
+ }
34
+
35
+ export function shouldBypassProxy(hostname, noProxyList) {
36
+ if (!noProxyList || noProxyList.length === 0) return false;
37
+ for (const entry of noProxyList) {
38
+ if (entry === "*") return true;
39
+ if (hostname === entry) return true;
40
+ if (entry.startsWith(".") && hostname.endsWith(entry)) return true;
41
+ }
42
+ return false;
43
+ }
44
+
45
+ export function installProxy() {
46
+ try {
47
+ const cfg = resolveProxy();
48
+ if (!cfg) return;
49
+
50
+ const require = createRequire(import.meta.url);
51
+ const { ProxyAgent, setGlobalDispatcher } = require("undici");
52
+
53
+ setGlobalDispatcher(
54
+ new ProxyAgent({
55
+ uri: cfg.proxyUrl,
56
+ noProxy: cfg.noProxy.join(","),
57
+ }),
58
+ );
59
+ } catch {
60
+ // Silent: undici unavailable or ProxyAgent unsupported — fall through to
61
+ // default (unproxied) fetch. Never break startup.
62
+ }
63
+ }
@@ -1424,9 +1424,10 @@ export async function runPlug(ctx, input, deps = {}) {
1424
1424
  // budget. Fast builds come back "live" in this same tool call; a build that
1425
1425
  // outlives the budget returns "building" with explicit do-not-claim-live copy;
1426
1426
  // a failed build surfaces the platform's user-language error instead of a URL.
1427
- if ((data.status === "building" || data.poll_url) && data.poll_url && !useAnonWire) {
1427
+ if ((data.status === "building" || data.poll_url) && !useAnonWire && (data.poll_url || data.entity_id)) {
1428
1428
  const verdict = await pollDeployTrace(ctx, {
1429
1429
  pollUrl: data.poll_url,
1430
+ entityId: data.entity_id,
1430
1431
  grid: data.grid,
1431
1432
  budgetMs: ctx.deployPollBudgetMs ?? DEPLOY_POLL_BUDGET_MS,
1432
1433
  intervalMs: ctx.deployPollIntervalMs ?? DEPLOY_POLL_INTERVAL_MS,
@@ -1773,6 +1774,34 @@ const sleepMs = (ms) => new Promise((r) => setTimeout(r, ms));
1773
1774
  // GET one deploy-trace snapshot. Returns { status, error } — status is the
1774
1775
  // server's word ("queued"/"building"/"success"/"failed"), error is the
1775
1776
  // user-language message when failed. Throws only on a non-OK HTTP response.
1777
+ // Resolve a pollable deploy target. The hosted inline-create 202 returns
1778
+ // poll_url:null and trace_id:null (the trace is minted async), so when we only
1779
+ // have an entity_id we look up its latest deploy — GET /entities/:id/deploys
1780
+ // exposes a real trace_id even when the plug response omitted it — and poll the
1781
+ // normal /deploys/<trace_id>. Returns a poll path, or null when there is no
1782
+ // trace yet (the caller retries within its budget). Never throws.
1783
+ async function resolvePollUrl(ctx, { pollUrl, entityId, grid }) {
1784
+ if (pollUrl) return pollUrl;
1785
+ if (!entityId) return null;
1786
+ try {
1787
+ const token = await ctx.getToken();
1788
+ const headers = {};
1789
+ if (token) headers.Authorization = `Bearer ${token}`;
1790
+ const orgSlug = grid || (await ctx.getActiveGrid());
1791
+ if (orgSlug) {
1792
+ headers["X-CloudGrid-Grid"] = orgSlug;
1793
+ headers["X-CloudGrid-Org"] = orgSlug;
1794
+ }
1795
+ const res = await fetch(`${API_BASE}/api/v2/entities/${encodeURIComponent(entityId)}/deploys?limit=1`, { headers });
1796
+ if (!res.ok) return null;
1797
+ const data = await res.json();
1798
+ const traceId = data?.deploys?.[0]?.trace_id;
1799
+ return traceId ? `/deploys/${traceId}` : null;
1800
+ } catch {
1801
+ return null;
1802
+ }
1803
+ }
1804
+
1776
1805
  async function fetchDeployTrace(ctx, { pollUrl, grid }) {
1777
1806
  const token = await ctx.getToken();
1778
1807
  const headers = {};
@@ -1799,12 +1828,16 @@ async function fetchDeployTrace(ctx, { pollUrl, grid }) {
1799
1828
  // Poll until terminal or the budget runs out. Never throws — an unreachable
1800
1829
  // poll endpoint degrades to { status: "unknown" } and the caller keeps the
1801
1830
  // do-not-claim-live wording (the safe direction).
1802
- export async function pollDeployTrace(ctx, { pollUrl, grid, budgetMs = DEPLOY_POLL_BUDGET_MS, intervalMs = DEPLOY_POLL_INTERVAL_MS }) {
1831
+ export async function pollDeployTrace(ctx, { pollUrl, entityId, grid, budgetMs = DEPLOY_POLL_BUDGET_MS, intervalMs = DEPLOY_POLL_INTERVAL_MS }) {
1803
1832
  const start = Date.now();
1804
1833
  let last = { status: "unknown", error: null };
1805
1834
  for (;;) {
1806
1835
  try {
1807
- last = await fetchDeployTrace(ctx, { pollUrl, grid });
1836
+ // Resolve each iteration: on the hosted inline-create path poll_url is
1837
+ // null and the trace may not exist for the first second or two, so keep
1838
+ // trying to resolve it from the entity until it appears.
1839
+ const target = await resolvePollUrl(ctx, { pollUrl, entityId, grid });
1840
+ if (target) last = await fetchDeployTrace(ctx, { pollUrl: target, grid });
1808
1841
  } catch {
1809
1842
  // transient/unauthorized/network — keep last, retry within budget
1810
1843
  }
@@ -1819,13 +1852,21 @@ export async function pollDeployTrace(ctx, { pollUrl, grid, budgetMs = DEPLOY_PO
1819
1852
  // were missing — they had no way to confirm a runtime build came live and
1820
1853
  // blind-polled the public URL into 502s.
1821
1854
  export async function runCheckDeploy(ctx, { poll_url, grid } = {}) {
1822
- const pollUrl = poll_url || ctx.state.lastDrop?.poll_url;
1823
- if (!pollUrl) {
1855
+ const gridSlug = grid || ctx.state.lastDrop?.grid;
1856
+ const entityId = ctx.state.lastDrop?.entity_id;
1857
+ // poll_url may be null on the hosted inline-create path — fall back to the
1858
+ // session's entity_id and resolve the trace from /entities/:id/deploys.
1859
+ const target = await resolvePollUrl(ctx, {
1860
+ pollUrl: poll_url || ctx.state.lastDrop?.poll_url,
1861
+ entityId,
1862
+ grid: gridSlug,
1863
+ });
1864
+ if (!target) {
1824
1865
  throw new Error(
1825
- "No build to check. Pass poll_url from a grid_deploy result, or deploy something first in this session. (Instant inspiration deploys are live on return and have no poll_url.)",
1866
+ "No build to check. Pass poll_url from a grid_deploy result, or deploy something first in this session. (Instant inspiration deploys are live on return and have no build to poll.)",
1826
1867
  );
1827
1868
  }
1828
- const verdict = await fetchDeployTrace(ctx, { pollUrl, grid: grid || ctx.state.lastDrop?.grid });
1869
+ const verdict = await fetchDeployTrace(ctx, { pollUrl: target, grid: gridSlug });
1829
1870
  const url = ctx.state.lastDrop?.url;
1830
1871
  if (verdict.status === "success") {
1831
1872
  return {
package/src/web.js CHANGED
@@ -14,6 +14,9 @@
14
14
  //
15
15
  // Run: PORT=8080 node src/web.js Health: GET /healthz
16
16
 
17
+ import { installProxy } from "./proxy.js";
18
+ installProxy();
19
+
17
20
  import { readFileSync } from "node:fs";
18
21
  import { randomUUID } from "node:crypto";
19
22
  import express from "express";