@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 +4 -3
- package/src/auth.js +29 -0
- package/src/corpus/workflows/ai-app.md +11 -3
- package/src/index.js +2 -2
- package/src/system-ca.js +129 -0
- package/src/tools/register.js +7 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudgrid-io/mcp",
|
|
3
|
-
"version": "0.20.
|
|
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": "^
|
|
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
|
|
45
|
-
`
|
|
46
|
-
it from the manifest (honoring its `name:`) and
|
|
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 {
|
|
9
|
-
|
|
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";
|
package/src/system-ca.js
ADDED
|
@@ -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
|
+
}
|
package/src/tools/register.js
CHANGED
|
@@ -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."),
|