@cloudgrid-io/mcp 0.20.30 → 0.20.32

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.30",
3
+ "version": "0.20.32",
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": {
@@ -54,13 +54,14 @@
54
54
  "test:set-sharing": "node test/set-sharing.test.mjs",
55
55
  "test:check-deploy": "node test/check-deploy.test.mjs",
56
56
  "test:staleness": "node test/staleness.test.mjs",
57
- "test:proxy": "node test/proxy.test.mjs"
57
+ "test:proxy": "node test/proxy.test.mjs",
58
+ "test:system-ca": "node test/system-ca.test.mjs"
58
59
  },
59
60
  "dependencies": {
60
61
  "@modelcontextprotocol/sdk": "^1.0.0",
61
62
  "express": "^4.22.2",
62
63
  "fflate": "^0.8.3",
63
- "undici": "^8.7.0",
64
+ "undici": "^6.27.0",
64
65
  "zod": "^3.23.0"
65
66
  },
66
67
  "license": "Apache-2.0",
package/src/auth.js CHANGED
@@ -13,6 +13,7 @@ import { randomUUID } from "node:crypto";
13
13
  import { mkdir, writeFile, chmod, readFile } from "node:fs/promises";
14
14
  import { homedir } from "node:os";
15
15
  import { join } from "node:path";
16
+ import { isCertError } from "./system-ca.js";
16
17
 
17
18
  // Base for server-to-API calls. On a hosted deployment this is the in-cluster
18
19
  // service address (CLOUDGRID_API_URL), which is fast but NOT reachable by a browser.
@@ -41,6 +42,33 @@ export function buildLoginUrl(code) {
41
42
  return `${PUBLIC_API_BASE}/auth/login?code=${encodeURIComponent(code)}`;
42
43
  }
43
44
 
45
+ const CERT_ERROR_GUIDANCE =
46
+ "TLS certificate validation failed. This machine has a TLS-inspecting " +
47
+ "proxy or a custom root CA that is not in the OS trust store. The MCP " +
48
+ "trusts the system certificate store automatically, so if you still see " +
49
+ "this error the intercepting CA's root certificate is not installed in " +
50
+ "your OS trust store.\n\n" +
51
+ "To resolve this:\n" +
52
+ "- Install the root CA into your OS trust store (macOS Keychain, Windows " +
53
+ "Certificate Manager, or /usr/local/share/ca-certificates on Linux), or\n" +
54
+ "- Set NODE_EXTRA_CA_CERTS=/path/to/root-ca.pem before launching the MCP, or\n" +
55
+ "- Use the hosted MCP connector: https://mcp-connected.cloudgrid.io/mcp";
56
+
57
+ function throwCertError() {
58
+ const err = new Error(CERT_ERROR_GUIDANCE);
59
+ err.certError = true;
60
+ throw err;
61
+ }
62
+
63
+ export async function checkApiConnectivity() {
64
+ try {
65
+ await fetch(`${API_BASE}/auth/status?code=connectivity-probe`);
66
+ } catch (err) {
67
+ if (isCertError(err)) throwCertError();
68
+ throw err;
69
+ }
70
+ }
71
+
44
72
  // One poll. Returns { status: 'not_started' | 'pending' | 'authenticated' | 'expired', jwt? }.
45
73
  // A 404 means the session for this code does not exist yet — the user has not
46
74
  // opened the sign-in URL — so it is reported as 'not_started', not an error.
@@ -49,6 +77,7 @@ export async function pollStatusOnce(code) {
49
77
  try {
50
78
  res = await fetch(`${API_BASE}/auth/status?code=${encodeURIComponent(code)}`);
51
79
  } catch (err) {
80
+ if (isCertError(err)) throwCertError();
52
81
  const cause = err?.cause?.code || err?.cause?.message || err?.cause || err.message;
53
82
  throw new Error(
54
83
  `Can't reach api.cloudgrid.io (${cause}). ` +
@@ -41,9 +41,17 @@ Persistent apps are owned entities.
41
41
 
42
42
  ## 3. Scaffold
43
43
 
44
- `grid_create_project` an app `<name>`. It scaffolds the project folder and writes a `cloudgrid.yaml` with an EMPTY
45
- `services: {}`. No server entity exists yet — the first `grid plug` auto-creates
46
- it from the manifest (honoring its `name:`) and writes `.cloudgrid/link.json`.
44
+ `grid_create_project` an app `<name>`. It scaffolds the project folder, a
45
+ `cloudgrid.yaml`, and a starter `web` service. No server entity exists yet — the
46
+ first `grid plug` auto-creates it from the manifest (honoring its `name:`) and
47
+ writes `.cloudgrid/link.json`.
48
+
49
+ The starter files are throwaway — the `ai-app` template (step 4) is the source
50
+ of truth. **The template is plain JavaScript.** The `nextjs` scaffold, on the
51
+ other hand, writes a TypeScript starter (`tsconfig.json`, `app/layout.tsx`,
52
+ `app/page.tsx`). Before applying the template, delete the scaffolded
53
+ `tsconfig.json` and `app/*.tsx`, then write the template's `.js` files. Never
54
+ leave both `.tsx` and `.js` for the same route — that breaks `next build`.
47
55
 
48
56
  You then do two things: (a) write the app under **`services/web/`**, and (b) fill
49
57
  in `cloudgrid.yaml` to the shape below (`services.web` type `nextjs` + `needs: {
package/src/index.js CHANGED
@@ -5,8 +5,8 @@
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();
8
+ import { installTrustStore } from "./system-ca.js";
9
+ installTrustStore();
10
10
 
11
11
  import { readFileSync } from "node:fs";
12
12
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -0,0 +1,129 @@
1
+ // System CA trust for Node's global fetch.
2
+ //
3
+ // Node's built-in fetch uses Node's bundled Mozilla CA list and ignores the OS
4
+ // trust store. On machines with a TLS-inspecting proxy or a custom/corporate
5
+ // root CA (installed in the OS keychain but not in Node's bundle), every HTTPS
6
+ // call fails with UNABLE_TO_GET_ISSUER_CERT_LOCALLY. This module builds a
7
+ // unified CA bundle (Node defaults + OS store + NODE_EXTRA_CA_CERTS) and
8
+ // installs it process-wide via tls.setDefaultCACertificates (Node >=22.15).
9
+ //
10
+ // On older Node (< 22.15) it is a graceful no-op — NODE_EXTRA_CA_CERTS is
11
+ // still honored natively when set at launch, and the login preflight surfaces
12
+ // the hosted-connector fallback on cert errors.
13
+ //
14
+ // Fully guarded: on any failure this is a silent no-op. Never throws at startup.
15
+
16
+ import tls from "node:tls";
17
+ import { readFileSync } from "node:fs";
18
+ import { createRequire } from "node:module";
19
+ import { resolveProxy } from "./proxy.js";
20
+
21
+ const CERT_ERROR_CODES = new Set([
22
+ "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
23
+ "SELF_SIGNED_CERT_IN_CHAIN",
24
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
25
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
26
+ "CERT_HAS_EXPIRED",
27
+ "ERR_TLS_CERT_ALTNAME_INVALID",
28
+ "CERT_UNTRUSTED",
29
+ ]);
30
+
31
+ export function isCertError(err) {
32
+ if (!err) return false;
33
+ if (typeof err.code === "string" && CERT_ERROR_CODES.has(err.code)) return true;
34
+ if (err.cause) return isCertError(err.cause);
35
+ return false;
36
+ }
37
+
38
+ function parsePemFile(path) {
39
+ try {
40
+ const raw = readFileSync(path, "utf8");
41
+ const certs = [];
42
+ const re = /-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g;
43
+ let m;
44
+ while ((m = re.exec(raw)) !== null) certs.push(m[0]);
45
+ return certs;
46
+ } catch {
47
+ return [];
48
+ }
49
+ }
50
+
51
+ export function buildCaBundle() {
52
+ const seen = new Set();
53
+ const certs = [];
54
+
55
+ function add(list) {
56
+ if (!Array.isArray(list)) return;
57
+ for (const c of list) {
58
+ if (c && !seen.has(c)) {
59
+ seen.add(c);
60
+ certs.push(c);
61
+ }
62
+ }
63
+ }
64
+
65
+ try {
66
+ if (typeof tls.getCACertificates === "function") {
67
+ add(tls.getCACertificates("default"));
68
+ } else {
69
+ add(tls.rootCertificates);
70
+ }
71
+ } catch { /* guard */ }
72
+
73
+ try {
74
+ if (typeof tls.getCACertificates === "function") {
75
+ add(tls.getCACertificates("system"));
76
+ }
77
+ } catch { /* guard */ }
78
+
79
+ try {
80
+ if (typeof tls.getCACertificates === "function") {
81
+ add(tls.getCACertificates("extra"));
82
+ }
83
+ } catch { /* guard */ }
84
+
85
+ try {
86
+ if (process.env.NODE_EXTRA_CA_CERTS) {
87
+ add(parsePemFile(process.env.NODE_EXTRA_CA_CERTS));
88
+ }
89
+ } catch { /* guard */ }
90
+
91
+ return certs.length > 0 ? certs : null;
92
+ }
93
+
94
+ export function installTrustStore() {
95
+ try {
96
+ const ca = buildCaBundle();
97
+
98
+ // Primary mechanism (Node >=22.15): set the process-wide default CA list.
99
+ // This covers Node's built-in fetch and all TLS connections — no userland
100
+ // undici dispatcher needed for the non-proxy case.
101
+ if (ca && typeof tls.setDefaultCACertificates === "function") {
102
+ tls.setDefaultCACertificates(ca);
103
+ }
104
+
105
+ // Proxy composition: Node's built-in fetch ignores HTTPS_PROXY, so when a
106
+ // proxy is configured we still need undici's ProxyAgent as the global
107
+ // dispatcher. The ProxyAgent carries the CA bundle in its TLS options.
108
+ // Guarded: if undici fails to load (unexpected, since we pin a compatible
109
+ // version, but guard anyway), proxy is a silent no-op.
110
+ const cfg = resolveProxy();
111
+ if (cfg) {
112
+ try {
113
+ const require = createRequire(import.meta.url);
114
+ const { setGlobalDispatcher, ProxyAgent } = require("undici");
115
+ setGlobalDispatcher(
116
+ new ProxyAgent({
117
+ uri: cfg.proxyUrl,
118
+ noProxy: cfg.noProxy.join(","),
119
+ ...(ca ? { requestTls: { ca }, proxyTls: { ca } } : {}),
120
+ }),
121
+ );
122
+ } catch {
123
+ // undici unavailable or API mismatch — proxy degrades silently.
124
+ }
125
+ }
126
+ } catch {
127
+ // Never break startup.
128
+ }
129
+ }
@@ -3,7 +3,7 @@
3
3
  // Extracted verbatim from src/tools.js (refactor: split tools.js into modules).
4
4
 
5
5
  import { z } from "zod";
6
- import { newLoginCode, buildLoginUrl, pollStatusOnce } from "../auth.js";
6
+ import { newLoginCode, buildLoginUrl, pollStatusOnce, checkApiConnectivity } from "../auth.js";
7
7
  import { PLAYBOOK, fetchCorpus, listWorkflows } from "../playbook.js";
8
8
  import {
9
9
  APPS_WIDGETS_ENABLED,
@@ -557,6 +557,11 @@ export function registerTools(server, ctx) {
557
557
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
558
558
  },
559
559
  async () => {
560
+ try {
561
+ await checkApiConnectivity();
562
+ } catch (err) {
563
+ if (err.certError) return fail(err.message);
564
+ }
560
565
  const code = newLoginCode();
561
566
  ctx.state.pendingLoginCode = code;
562
567
  const url = buildLoginUrl(code);
@@ -934,7 +939,7 @@ export function registerTools(server, ctx) {
934
939
 
935
940
  regTool(
936
941
  "grid_create_project",
937
- "Scaffold a new CloudGrid app or agent folder (cloudgrid.yaml + a web service), optionally pre-declaring resources. Wraps `grid new` (scaffolds locally; no server entity until you grid_deploy).",
942
+ "Scaffold a new CloudGrid app or agent folder (cloudgrid.yaml + a web service), optionally pre-declaring resources. Wraps `grid new` (scaffolds locally; no server entity until you grid_deploy). Language note: the nextjs starter is TypeScript (writes tsconfig.json + app/*.tsx), but CloudGrid templates are plain JavaScript. When you fill a template, treat the template's files as the source of truth — delete the scaffolded tsconfig.json and app/*.tsx, then write the template's .js files. Never leave both .tsx and .js for the same route.",
938
943
  {
939
944
  kind: z.enum(["app", "agent"]).describe("Entity kind. 'agent' scaffolds an agent: block."),
940
945
  name: z.string().describe("Slug: 3-40 lowercase alphanumerics and hyphens."),