@cloudgrid-io/mcp 0.4.4 → 0.4.6
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 +3 -2
- package/src/tools.js +100 -41
- 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.6",
|
|
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": {
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"smoke:web": "node test/smoke-web.mjs",
|
|
23
23
|
"smoke:redrop": "node test/smoke-redrop.mjs",
|
|
24
24
|
"smoke:oauth": "node test/smoke-oauth.mjs",
|
|
25
|
-
"test:auth": "node test/auth.test.mjs"
|
|
25
|
+
"test:auth": "node test/auth.test.mjs",
|
|
26
|
+
"build:mcpb": "node scripts/build-mcpb.mjs"
|
|
26
27
|
},
|
|
27
28
|
"dependencies": {
|
|
28
29
|
"@cloudgrid-io/cli": "^0.9.20",
|
package/src/tools.js
CHANGED
|
@@ -166,7 +166,7 @@ function tryOpenBrowser(url) {
|
|
|
166
166
|
// ── Org listing (bearer-authed, web edition) ──────────────────────────────────
|
|
167
167
|
// Fetches the signed-in user's orgs via GET /api/v2/orgs. The JWT does not
|
|
168
168
|
// carry orgs (claims: sub, email, name, iat, exp), so the API is the canonical
|
|
169
|
-
// source. Returns [{slug, name, role}].
|
|
169
|
+
// source. Returns [{slug, name, role, render_ready}].
|
|
170
170
|
async function fetchUserOrgs(token) {
|
|
171
171
|
try {
|
|
172
172
|
const res = await fetch(`${API_BASE}/api/v2/orgs`, {
|
|
@@ -179,6 +179,7 @@ async function fetchUserOrgs(token) {
|
|
|
179
179
|
slug: o.slug ?? "",
|
|
180
180
|
name: o.name ?? o.slug ?? "",
|
|
181
181
|
role: o.role ?? "member",
|
|
182
|
+
render_ready: o.render_ready ?? true, // default true for older APIs
|
|
182
183
|
}));
|
|
183
184
|
} catch {
|
|
184
185
|
return [];
|
|
@@ -601,6 +602,7 @@ export function registerTools(server, ctx) {
|
|
|
601
602
|
slug: z.string().describe("Org slug to pass as the org parameter."),
|
|
602
603
|
name: z.string().describe("Human-readable org name."),
|
|
603
604
|
role: z.string().describe("User's role in the org."),
|
|
605
|
+
is_active: z.boolean().optional().describe("True if this is the user's currently active org."),
|
|
604
606
|
})).optional().describe("The user's orgs, when org choice is needed."),
|
|
605
607
|
needs_sign_in: z.boolean().optional().describe("True when sign-in is needed before dropping."),
|
|
606
608
|
login_url: z.string().optional().describe("Sign-in URL when authentication is needed."),
|
|
@@ -634,28 +636,64 @@ export function registerTools(server, ctx) {
|
|
|
634
636
|
structured: { needs_sign_in: true, login_url: url },
|
|
635
637
|
});
|
|
636
638
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Stateless org disambiguation — both editions, when authenticated.
|
|
642
|
+
// No dependency on prior-call state so it works even when the client
|
|
643
|
+
// reconnects on every tool call (ChatGPT Apps SDK behaviour).
|
|
644
|
+
if (input?.anonymous !== true) {
|
|
645
|
+
const token = await ctx.getToken();
|
|
646
|
+
if (token) {
|
|
641
647
|
const orgs = await fetchUserOrgs(token);
|
|
648
|
+
const activeOrg = await ctx.getActiveOrg();
|
|
642
649
|
const suppliedOrg = input?.org;
|
|
643
|
-
const
|
|
644
|
-
if (
|
|
645
|
-
// Supplied org matches
|
|
650
|
+
const matchedOrg = suppliedOrg && orgs.find((o) => o.slug === suppliedOrg);
|
|
651
|
+
if (matchedOrg) {
|
|
652
|
+
// Supplied org matches — proceed with it. The agent should have
|
|
653
|
+
// already checked render_ready via cloudgrid_orgs and warned the
|
|
654
|
+
// user if this org isn't set up yet. We don't block here.
|
|
646
655
|
input = { ...(input || {}), org: suppliedOrg };
|
|
647
656
|
} else if (orgs.length > 1) {
|
|
648
657
|
// No valid org supplied and multiple orgs — ask once.
|
|
658
|
+
// Mark the active org so the agent can offer it as the default.
|
|
659
|
+
const annotated = orgs.map((o) => ({
|
|
660
|
+
...o,
|
|
661
|
+
is_active: o.slug === activeOrg,
|
|
662
|
+
}));
|
|
663
|
+
// Sort: active org first, then ready orgs, then not-ready orgs.
|
|
664
|
+
annotated.sort((a, b) => {
|
|
665
|
+
if (a.is_active !== b.is_active) return b.is_active ? 1 : -1;
|
|
666
|
+
if (a.render_ready !== b.render_ready) return b.render_ready ? 1 : -1;
|
|
667
|
+
return 0;
|
|
668
|
+
});
|
|
649
669
|
const lines = ["Which org should this be published to?"];
|
|
650
|
-
for (const o of
|
|
670
|
+
for (const o of annotated) {
|
|
671
|
+
const tags = [];
|
|
672
|
+
if (o.is_active) tags.push("your active org");
|
|
673
|
+
if (!o.render_ready) tags.push("not set up yet");
|
|
674
|
+
const suffix = tags.length ? ` (${tags.join(", ")})` : "";
|
|
675
|
+
lines.push(` ${o.slug} — ${o.name} (${o.role})${suffix}`);
|
|
676
|
+
}
|
|
651
677
|
lines.push("Pass the org slug in the org parameter to publish.");
|
|
678
|
+
const readyCount = annotated.filter((o) => o.render_ready).length;
|
|
679
|
+
if (readyCount === 0) {
|
|
680
|
+
lines.push("Note: none of your orgs are fully set up yet. You can use anonymous: true as a fallback.");
|
|
681
|
+
}
|
|
652
682
|
return okResult({
|
|
653
683
|
text: lines.join("\n"),
|
|
654
|
-
structured: { needs_org: true, orgs },
|
|
655
|
-
meta: { "openai/outputTemplate": ORG_PICKER_URI },
|
|
684
|
+
structured: { needs_org: true, orgs: annotated },
|
|
685
|
+
...(ctx.edition === "web" ? { meta: { "openai/outputTemplate": ORG_PICKER_URI } } : {}),
|
|
656
686
|
});
|
|
657
687
|
} else if (orgs.length === 1) {
|
|
658
|
-
|
|
688
|
+
if (!orgs[0].render_ready) {
|
|
689
|
+
// Single org but not set up — tell the agent so it can warn
|
|
690
|
+
// the user and offer anonymous drop as fallback.
|
|
691
|
+
const annotated = [{ ...orgs[0], is_active: orgs[0].slug === activeOrg, render_ready: false }];
|
|
692
|
+
return okResult({
|
|
693
|
+
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.`,
|
|
694
|
+
structured: { needs_org: true, org_not_ready: true, orgs: annotated },
|
|
695
|
+
});
|
|
696
|
+
}
|
|
659
697
|
input = { ...(input || {}), org: orgs[0].slug };
|
|
660
698
|
}
|
|
661
699
|
}
|
|
@@ -791,36 +829,57 @@ export function registerTools(server, ctx) {
|
|
|
791
829
|
},
|
|
792
830
|
);
|
|
793
831
|
|
|
794
|
-
// Org listing —
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
},
|
|
808
|
-
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
809
|
-
},
|
|
810
|
-
async () => {
|
|
811
|
-
const token = await ctx.getToken();
|
|
812
|
-
if (!token) {
|
|
813
|
-
return fail("You are not signed in. Run cloudgrid_login first.");
|
|
814
|
-
}
|
|
815
|
-
const orgs = await fetchUserOrgs(token);
|
|
816
|
-
if (orgs.length === 0) {
|
|
817
|
-
return okResult({ text: "No organizations found.", structured: { orgs: [] } });
|
|
818
|
-
}
|
|
819
|
-
const lines = orgs.map((o) => `${o.slug} — ${o.name} (${o.role})`);
|
|
820
|
-
return okResult({ text: lines.join("\n"), structured: { orgs } });
|
|
832
|
+
// Org listing — both editions.
|
|
833
|
+
server.registerTool(
|
|
834
|
+
"cloudgrid_orgs",
|
|
835
|
+
{
|
|
836
|
+
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.",
|
|
837
|
+
inputSchema: {},
|
|
838
|
+
outputSchema: {
|
|
839
|
+
orgs: z.array(z.object({
|
|
840
|
+
slug: z.string().describe("Org slug."),
|
|
841
|
+
name: z.string().describe("Human-readable org name."),
|
|
842
|
+
role: z.string().describe("User's role in the org."),
|
|
843
|
+
is_active: z.boolean().optional().describe("True if this is the user's currently active org."),
|
|
844
|
+
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."),
|
|
845
|
+
})).describe("The user's org memberships."),
|
|
821
846
|
},
|
|
822
|
-
|
|
823
|
-
|
|
847
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
848
|
+
},
|
|
849
|
+
async () => {
|
|
850
|
+
const token = await ctx.getToken();
|
|
851
|
+
if (!token) {
|
|
852
|
+
return fail("You are not signed in. Run cloudgrid_login first.");
|
|
853
|
+
}
|
|
854
|
+
const orgs = await fetchUserOrgs(token);
|
|
855
|
+
if (orgs.length === 0) {
|
|
856
|
+
return okResult({ text: "No organizations found.", structured: { orgs: [] } });
|
|
857
|
+
}
|
|
858
|
+
const activeOrg = await ctx.getActiveOrg();
|
|
859
|
+
const annotated = orgs.map((o) => ({
|
|
860
|
+
...o,
|
|
861
|
+
is_active: o.slug === activeOrg,
|
|
862
|
+
}));
|
|
863
|
+
// Sort: active org first, then ready orgs, then not-ready orgs.
|
|
864
|
+
annotated.sort((a, b) => {
|
|
865
|
+
if (a.is_active !== b.is_active) return b.is_active ? 1 : -1;
|
|
866
|
+
if (a.render_ready !== b.render_ready) return b.render_ready ? 1 : -1;
|
|
867
|
+
return 0;
|
|
868
|
+
});
|
|
869
|
+
const lines = annotated.map((o) => {
|
|
870
|
+
const tags = [];
|
|
871
|
+
if (o.is_active) tags.push("your active org");
|
|
872
|
+
if (!o.render_ready) tags.push("not set up yet");
|
|
873
|
+
const suffix = tags.length ? ` (${tags.join(", ")})` : "";
|
|
874
|
+
return `${o.slug} — ${o.name} (${o.role})${suffix}`;
|
|
875
|
+
});
|
|
876
|
+
const readyCount = annotated.filter((o) => o.render_ready).length;
|
|
877
|
+
if (readyCount === 0 && annotated.length > 0) {
|
|
878
|
+
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.");
|
|
879
|
+
}
|
|
880
|
+
return okResult({ text: lines.join("\n"), structured: { orgs: annotated } });
|
|
881
|
+
},
|
|
882
|
+
);
|
|
824
883
|
|
|
825
884
|
if (ctx.edition !== "local") return; // web edition stops here — no CLI tools
|
|
826
885
|
|
|
@@ -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
|
}
|