@cloudgrid-io/mcp 0.4.5 → 0.4.7
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 -2
- package/src/tools.js +79 -29
- package/src/widgets/org-picker.html +6 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudgrid-io/mcp",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
4
4
|
"description": "MCP server for CloudGrid. Two editions: a local stdio server (full toolset) and a hosted web server (light, CLI-free toolset) over MCP Streamable HTTP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,7 +26,6 @@
|
|
|
26
26
|
"build:mcpb": "node scripts/build-mcpb.mjs"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@cloudgrid-io/cli": "^0.9.20",
|
|
30
29
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
31
30
|
"express": "^4.22.2",
|
|
32
31
|
"zod": "^3.23.0"
|
package/src/tools.js
CHANGED
|
@@ -90,6 +90,9 @@ function resolveBundledCli() {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
// Pin the CLI version for the lazy npx fallback so behaviour is reproducible.
|
|
94
|
+
const CLI_NPX_PKG = "@cloudgrid-io/cli@^0.9.20";
|
|
95
|
+
|
|
93
96
|
async function runCloudgrid(args, opts = {}) {
|
|
94
97
|
const cwd = resolveCwd(opts.cwd);
|
|
95
98
|
const execOpts = {
|
|
@@ -99,7 +102,15 @@ async function runCloudgrid(args, opts = {}) {
|
|
|
99
102
|
...(cwd ? { cwd } : {}),
|
|
100
103
|
};
|
|
101
104
|
|
|
102
|
-
|
|
105
|
+
const extract = (err) => {
|
|
106
|
+
const detail = [err && err.stdout, err && err.stderr, err && err.message]
|
|
107
|
+
.filter(Boolean)
|
|
108
|
+
.join("\n")
|
|
109
|
+
.trim();
|
|
110
|
+
return new Error(detail || "cloudgrid command failed");
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// 1. Bundled CLI (the .mcpb ships it in its node_modules)
|
|
103
114
|
const bundled = resolveBundledCli();
|
|
104
115
|
if (bundled) {
|
|
105
116
|
try {
|
|
@@ -110,29 +121,32 @@ async function runCloudgrid(args, opts = {}) {
|
|
|
110
121
|
);
|
|
111
122
|
return (stdout || stderr || "").trim() || "Done.";
|
|
112
123
|
} catch (err) {
|
|
113
|
-
|
|
114
|
-
.filter(Boolean)
|
|
115
|
-
.join("\n")
|
|
116
|
-
.trim();
|
|
117
|
-
throw new Error(detail || "cloudgrid command failed");
|
|
124
|
+
throw extract(err);
|
|
118
125
|
}
|
|
119
126
|
}
|
|
120
127
|
|
|
121
|
-
// 2.
|
|
128
|
+
// 2. Global `cloudgrid` on PATH
|
|
122
129
|
try {
|
|
123
130
|
const { stdout, stderr } = await execFileAsync("cloudgrid", args, execOpts);
|
|
124
131
|
return (stdout || stderr || "").trim() || "Done.";
|
|
125
132
|
} catch (err) {
|
|
126
|
-
if (err && err.code === "ENOENT")
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
if (!(err && err.code === "ENOENT")) throw extract(err);
|
|
134
|
+
// Not found — fall through to lazy npx fetch
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 3. Lazy fetch: npx -y @cloudgrid-io/cli@<pinned> <args>
|
|
138
|
+
// One-time download; npx caches it for subsequent invocations.
|
|
139
|
+
console.error("cloudgrid-mcp: fetching the CloudGrid CLI (first use)…");
|
|
140
|
+
const npx = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
141
|
+
try {
|
|
142
|
+
const { stdout, stderr } = await execFileAsync(
|
|
143
|
+
npx,
|
|
144
|
+
["-y", CLI_NPX_PKG, ...args],
|
|
145
|
+
execOpts,
|
|
146
|
+
);
|
|
147
|
+
return (stdout || stderr || "").trim() || "Done.";
|
|
148
|
+
} catch (err) {
|
|
149
|
+
throw extract(err);
|
|
136
150
|
}
|
|
137
151
|
}
|
|
138
152
|
|
|
@@ -166,7 +180,7 @@ function tryOpenBrowser(url) {
|
|
|
166
180
|
// ── Org listing (bearer-authed, web edition) ──────────────────────────────────
|
|
167
181
|
// Fetches the signed-in user's orgs via GET /api/v2/orgs. The JWT does not
|
|
168
182
|
// carry orgs (claims: sub, email, name, iat, exp), so the API is the canonical
|
|
169
|
-
// source. Returns [{slug, name, role}].
|
|
183
|
+
// source. Returns [{slug, name, role, render_ready}].
|
|
170
184
|
async function fetchUserOrgs(token) {
|
|
171
185
|
try {
|
|
172
186
|
const res = await fetch(`${API_BASE}/api/v2/orgs`, {
|
|
@@ -179,6 +193,7 @@ async function fetchUserOrgs(token) {
|
|
|
179
193
|
slug: o.slug ?? "",
|
|
180
194
|
name: o.name ?? o.slug ?? "",
|
|
181
195
|
role: o.role ?? "member",
|
|
196
|
+
render_ready: o.render_ready ?? true, // default true for older APIs
|
|
182
197
|
}));
|
|
183
198
|
} catch {
|
|
184
199
|
return [];
|
|
@@ -646,9 +661,11 @@ export function registerTools(server, ctx) {
|
|
|
646
661
|
const orgs = await fetchUserOrgs(token);
|
|
647
662
|
const activeOrg = await ctx.getActiveOrg();
|
|
648
663
|
const suppliedOrg = input?.org;
|
|
649
|
-
const
|
|
650
|
-
if (
|
|
651
|
-
// Supplied org matches
|
|
664
|
+
const matchedOrg = suppliedOrg && orgs.find((o) => o.slug === suppliedOrg);
|
|
665
|
+
if (matchedOrg) {
|
|
666
|
+
// Supplied org matches — proceed with it. The agent should have
|
|
667
|
+
// already checked render_ready via cloudgrid_orgs and warned the
|
|
668
|
+
// user if this org isn't set up yet. We don't block here.
|
|
652
669
|
input = { ...(input || {}), org: suppliedOrg };
|
|
653
670
|
} else if (orgs.length > 1) {
|
|
654
671
|
// No valid org supplied and multiple orgs — ask once.
|
|
@@ -657,20 +674,40 @@ export function registerTools(server, ctx) {
|
|
|
657
674
|
...o,
|
|
658
675
|
is_active: o.slug === activeOrg,
|
|
659
676
|
}));
|
|
660
|
-
|
|
677
|
+
// Sort: active org first, then ready orgs, then not-ready orgs.
|
|
678
|
+
annotated.sort((a, b) => {
|
|
679
|
+
if (a.is_active !== b.is_active) return b.is_active ? 1 : -1;
|
|
680
|
+
if (a.render_ready !== b.render_ready) return b.render_ready ? 1 : -1;
|
|
681
|
+
return 0;
|
|
682
|
+
});
|
|
661
683
|
const lines = ["Which org should this be published to?"];
|
|
662
684
|
for (const o of annotated) {
|
|
663
|
-
const
|
|
664
|
-
|
|
685
|
+
const tags = [];
|
|
686
|
+
if (o.is_active) tags.push("your active org");
|
|
687
|
+
if (!o.render_ready) tags.push("not set up yet");
|
|
688
|
+
const suffix = tags.length ? ` (${tags.join(", ")})` : "";
|
|
689
|
+
lines.push(` ${o.slug} — ${o.name} (${o.role})${suffix}`);
|
|
665
690
|
}
|
|
666
691
|
lines.push("Pass the org slug in the org parameter to publish.");
|
|
692
|
+
const readyCount = annotated.filter((o) => o.render_ready).length;
|
|
693
|
+
if (readyCount === 0) {
|
|
694
|
+
lines.push("Note: none of your orgs are fully set up yet. You can use anonymous: true as a fallback.");
|
|
695
|
+
}
|
|
667
696
|
return okResult({
|
|
668
697
|
text: lines.join("\n"),
|
|
669
698
|
structured: { needs_org: true, orgs: annotated },
|
|
670
699
|
...(ctx.edition === "web" ? { meta: { "openai/outputTemplate": ORG_PICKER_URI } } : {}),
|
|
671
700
|
});
|
|
672
701
|
} else if (orgs.length === 1) {
|
|
673
|
-
|
|
702
|
+
if (!orgs[0].render_ready) {
|
|
703
|
+
// Single org but not set up — tell the agent so it can warn
|
|
704
|
+
// the user and offer anonymous drop as fallback.
|
|
705
|
+
const annotated = [{ ...orgs[0], is_active: orgs[0].slug === activeOrg, render_ready: false }];
|
|
706
|
+
return okResult({
|
|
707
|
+
text: `Your only org "${orgs[0].slug}" isn't fully set up yet — pages published there may not load. You can drop anonymously (set anonymous: true) or wait until provisioning completes, then re-run.`,
|
|
708
|
+
structured: { needs_org: true, org_not_ready: true, orgs: annotated },
|
|
709
|
+
});
|
|
710
|
+
}
|
|
674
711
|
input = { ...(input || {}), org: orgs[0].slug };
|
|
675
712
|
}
|
|
676
713
|
}
|
|
@@ -810,7 +847,7 @@ export function registerTools(server, ctx) {
|
|
|
810
847
|
server.registerTool(
|
|
811
848
|
"cloudgrid_orgs",
|
|
812
849
|
{
|
|
813
|
-
description: "List the signed-in user's organizations. Returns each org's slug, name, and the user
|
|
850
|
+
description: "List the signed-in user's organizations. Returns each org's slug, name, role, and render_ready status. Orgs where render_ready is false are still provisioning — pages published there may not load yet. Prefer a render_ready org; if the user insists on a not-ready one, warn them that pages may not load and suggest waiting or choosing a ready org instead. Requires sign-in.",
|
|
814
851
|
inputSchema: {},
|
|
815
852
|
outputSchema: {
|
|
816
853
|
orgs: z.array(z.object({
|
|
@@ -818,6 +855,7 @@ export function registerTools(server, ctx) {
|
|
|
818
855
|
name: z.string().describe("Human-readable org name."),
|
|
819
856
|
role: z.string().describe("User's role in the org."),
|
|
820
857
|
is_active: z.boolean().optional().describe("True if this is the user's currently active org."),
|
|
858
|
+
render_ready: z.boolean().describe("True if the org's DNS and TLS are provisioned and pages will load. False means the org is still being set up."),
|
|
821
859
|
})).describe("The user's org memberships."),
|
|
822
860
|
},
|
|
823
861
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
@@ -836,11 +874,23 @@ export function registerTools(server, ctx) {
|
|
|
836
874
|
...o,
|
|
837
875
|
is_active: o.slug === activeOrg,
|
|
838
876
|
}));
|
|
839
|
-
|
|
877
|
+
// Sort: active org first, then ready orgs, then not-ready orgs.
|
|
878
|
+
annotated.sort((a, b) => {
|
|
879
|
+
if (a.is_active !== b.is_active) return b.is_active ? 1 : -1;
|
|
880
|
+
if (a.render_ready !== b.render_ready) return b.render_ready ? 1 : -1;
|
|
881
|
+
return 0;
|
|
882
|
+
});
|
|
840
883
|
const lines = annotated.map((o) => {
|
|
841
|
-
const
|
|
842
|
-
|
|
884
|
+
const tags = [];
|
|
885
|
+
if (o.is_active) tags.push("your active org");
|
|
886
|
+
if (!o.render_ready) tags.push("not set up yet");
|
|
887
|
+
const suffix = tags.length ? ` (${tags.join(", ")})` : "";
|
|
888
|
+
return `${o.slug} — ${o.name} (${o.role})${suffix}`;
|
|
843
889
|
});
|
|
890
|
+
const readyCount = annotated.filter((o) => o.render_ready).length;
|
|
891
|
+
if (readyCount === 0 && annotated.length > 0) {
|
|
892
|
+
lines.push("\nNone of your orgs are fully set up yet. You can use an anonymous drop as a fallback, or wait until provisioning completes.");
|
|
893
|
+
}
|
|
844
894
|
return okResult({ text: lines.join("\n"), structured: { orgs: annotated } });
|
|
845
895
|
},
|
|
846
896
|
);
|
|
@@ -13,8 +13,11 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;b
|
|
|
13
13
|
.org-btn{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-radius:8px;border:1px solid rgba(138,107,255,.3);background:transparent;color:#e0e0e0;cursor:pointer;transition:all .15s;font-size:13px;text-align:left}
|
|
14
14
|
.org-btn:hover{border-color:#8a6bff;background:rgba(138,107,255,.1)}
|
|
15
15
|
.org-btn.loading{opacity:.6;pointer-events:none}
|
|
16
|
+
.org-btn.not-ready{opacity:.65;border-color:rgba(255,200,50,.3)}
|
|
17
|
+
.org-btn.not-ready:hover{border-color:rgba(255,200,50,.6);background:rgba(255,200,50,.06)}
|
|
16
18
|
.org-name{font-weight:500;color:#fff}
|
|
17
19
|
.org-role{font-size:11px;color:#9ca3af}
|
|
20
|
+
.org-warn{font-size:10px;color:#e2b340;margin-left:6px}
|
|
18
21
|
.status{font-size:12px;color:#9ca3af;margin-top:12px}
|
|
19
22
|
</style>
|
|
20
23
|
</head>
|
|
@@ -43,9 +46,10 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;b
|
|
|
43
46
|
'<div class="orgs">';
|
|
44
47
|
for (var i = 0; i < data.orgs.length; i++) {
|
|
45
48
|
var org = data.orgs[i];
|
|
49
|
+
var notReady = org.render_ready === false;
|
|
46
50
|
html +=
|
|
47
|
-
'<button class="org-btn" data-slug="' + esc(org.slug) + '">' +
|
|
48
|
-
'<span class="org-name">' + esc(org.name) + '</span>' +
|
|
51
|
+
'<button class="org-btn' + (notReady ? ' not-ready' : '') + '" data-slug="' + esc(org.slug) + '">' +
|
|
52
|
+
'<span class="org-name">' + esc(org.name) + (notReady ? '<span class="org-warn">not set up yet</span>' : '') + '</span>' +
|
|
49
53
|
'<span class="org-role">' + esc(org.role) + '</span>' +
|
|
50
54
|
'</button>';
|
|
51
55
|
}
|