@cloudgrid-io/mcp 0.7.2 → 0.7.3
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 +2 -1
- package/src/tools.js +143 -60
- package/src/widgets/org-picker.html +10 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudgrid-io/mcp",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
4
4
|
"description": "MCP server for CloudGrid. Three editions: a local stdio server (full toolset), a hosted web server (light, CLI-free toolset), and a public docs server (search_cloudgrid_documentation) over MCP Streamable HTTP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"smoke": "node test/smoke.mjs",
|
|
19
19
|
"test:plug-wire": "node test/plug-wire.test.mjs",
|
|
20
20
|
"test:self-healing": "node test/self-healing.test.mjs",
|
|
21
|
+
"test:grid-picker": "node test/grid-picker.test.mjs",
|
|
21
22
|
"test:drift-guard": "node test/drift-guard.mjs",
|
|
22
23
|
"smoke:drop": "node test/smoke-drop.mjs",
|
|
23
24
|
"smoke:login": "node test/smoke-login.mjs",
|
package/src/tools.js
CHANGED
|
@@ -66,6 +66,7 @@ Operating rules:
|
|
|
66
66
|
4. Always return the live share URL at the end — that is the deliverable.
|
|
67
67
|
5. Minimize questions. Use sensible defaults and build; don't front-load setup questions.
|
|
68
68
|
6. If a signed-in publish fails with a server error, do not fall back to anonymous publishing (it burns the anonymous quota and downgrades ownership); surface the error, use the CLI fallback if offered, or ask the user.
|
|
69
|
+
7. When signed in and the user has more than one grid, do not assume a target — the publish tools will ask; relay the choice to the user and pass the chosen grid.
|
|
69
70
|
|
|
70
71
|
Deploy is edition-dependent: on the hosted MCP call the drop tool with the HTML; on local MCP / CLI write the file and run the plug tool. An HTML page deploys synchronously, so you get a URL right away.`;
|
|
71
72
|
|
|
@@ -667,6 +668,67 @@ async function fetchUserOrgs(token) {
|
|
|
667
668
|
}
|
|
668
669
|
}
|
|
669
670
|
|
|
671
|
+
// ── Shared grid disambiguation (gridctl_drop + gridctl_plug) ──────────────────
|
|
672
|
+
// The stateless "which grid?" ask, used identically by both publish verbs so
|
|
673
|
+
// they never drift. Given the caller's token and a supplied grid, it decides:
|
|
674
|
+
// - supplied grid matches a membership → { proceed: true, grid }
|
|
675
|
+
// - >1 grid and none supplied → { picker } (a ready-to-return result)
|
|
676
|
+
// - exactly one grid → { single: annotatedOrg } — the caller
|
|
677
|
+
// decides how to treat a not-ready single grid (drop blocks; plug warns)
|
|
678
|
+
// - no orgs / listing failed → { proceed: true } (fall through)
|
|
679
|
+
// User-facing text says "grid" (Gilad's org→grid rename); the structured payload
|
|
680
|
+
// carries `needs_grid` AND the `needs_org`/`orgs`/`org`-slug fields the existing
|
|
681
|
+
// org-picker web widget reads, so the web card keeps working. Stateless — no
|
|
682
|
+
// dependence on prior-call state (ChatGPT Apps SDK reconnects every call).
|
|
683
|
+
export async function resolveGridOrAsk(ctx, { token, suppliedGrid, edition }, deps = {}) {
|
|
684
|
+
const listOrgs = deps.fetchUserOrgs || fetchUserOrgs;
|
|
685
|
+
const orgs = await listOrgs(token);
|
|
686
|
+
const activeOrg = await ctx.getActiveOrg();
|
|
687
|
+
const matched = suppliedGrid && orgs.find((o) => o.slug === suppliedGrid);
|
|
688
|
+
if (matched) {
|
|
689
|
+
// Supplied grid matches — proceed. The agent should already have checked
|
|
690
|
+
// render_ready and warned the user; we don't block here.
|
|
691
|
+
return { proceed: true, grid: suppliedGrid };
|
|
692
|
+
}
|
|
693
|
+
if (orgs.length > 1) {
|
|
694
|
+
// No valid grid supplied and multiple grids — ask once. Mark the active
|
|
695
|
+
// grid so the agent can offer it as the default.
|
|
696
|
+
const annotated = orgs.map((o) => ({ ...o, is_active: o.slug === activeOrg }));
|
|
697
|
+
// Sort: active grid first, then ready grids, then not-ready grids.
|
|
698
|
+
annotated.sort((a, b) => {
|
|
699
|
+
if (a.is_active !== b.is_active) return b.is_active ? 1 : -1;
|
|
700
|
+
if (a.render_ready !== b.render_ready) return b.render_ready ? 1 : -1;
|
|
701
|
+
return 0;
|
|
702
|
+
});
|
|
703
|
+
const lines = ["Which grid should this be published to?"];
|
|
704
|
+
for (const o of annotated) {
|
|
705
|
+
const tags = [];
|
|
706
|
+
if (o.is_active) tags.push("your active grid");
|
|
707
|
+
if (!o.render_ready) tags.push("not set up yet");
|
|
708
|
+
const suffix = tags.length ? ` (${tags.join(", ")})` : "";
|
|
709
|
+
lines.push(` ${o.slug} — ${o.name} (${o.role})${suffix}`);
|
|
710
|
+
}
|
|
711
|
+
lines.push("Pass the grid slug in the `grid` parameter.");
|
|
712
|
+
const readyCount = annotated.filter((o) => o.render_ready).length;
|
|
713
|
+
if (readyCount === 0) {
|
|
714
|
+
lines.push("Note: none of your grids are fully set up yet. You can use anonymous: true as a fallback.");
|
|
715
|
+
}
|
|
716
|
+
return {
|
|
717
|
+
picker: {
|
|
718
|
+
text: lines.join("\n"),
|
|
719
|
+
// `needs_grid` is the new field; `needs_org`/`orgs` are kept as aliases
|
|
720
|
+
// so the existing org-picker.html web widget (reads data.orgs) still works.
|
|
721
|
+
structured: { needs_grid: true, needs_org: true, grids: annotated, orgs: annotated },
|
|
722
|
+
...(edition === "web" ? { meta: { "openai/outputTemplate": ORG_PICKER_URI } } : {}),
|
|
723
|
+
},
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
if (orgs.length === 1) {
|
|
727
|
+
return { single: { ...orgs[0], is_active: orgs[0].slug === activeOrg } };
|
|
728
|
+
}
|
|
729
|
+
return { proceed: true };
|
|
730
|
+
}
|
|
731
|
+
|
|
670
732
|
// After an authenticated web drop, upgrade visibility to "link" so the artifact
|
|
671
733
|
// is shareable and its preview renders without a sign-in wall. Best-effort — a
|
|
672
734
|
// failure here does not fail the drop; the user can always call gridctl_visibility.
|
|
@@ -733,9 +795,11 @@ export function resolvePlugUrl(data) {
|
|
|
733
795
|
|
|
734
796
|
export async function runDrop(
|
|
735
797
|
ctx,
|
|
736
|
-
{ html, path: filePath, filename, anonymous, org, fresh, entity_id, owner_token },
|
|
798
|
+
{ html, path: filePath, filename, anonymous, org, grid, fresh, entity_id, owner_token },
|
|
737
799
|
deps = {},
|
|
738
800
|
) {
|
|
801
|
+
// `grid` is an accepted alias for `org` (Gilad's org→grid rename); `org` still works.
|
|
802
|
+
const targetGrid = org ?? grid;
|
|
739
803
|
// Defensive: the web edition schema excludes `path`, but if a model still
|
|
740
804
|
// passes one (e.g. from a cached tool description), reject early with a
|
|
741
805
|
// clear explanation.
|
|
@@ -779,7 +843,7 @@ export async function runDrop(
|
|
|
779
843
|
if (anonymous !== true) {
|
|
780
844
|
authToken = await ctx.getToken();
|
|
781
845
|
if (authToken) {
|
|
782
|
-
orgSlug =
|
|
846
|
+
orgSlug = targetGrid || (await ctx.getActiveOrg());
|
|
783
847
|
}
|
|
784
848
|
}
|
|
785
849
|
|
|
@@ -1847,7 +1911,8 @@ export function registerTools(server, ctx) {
|
|
|
1847
1911
|
),
|
|
1848
1912
|
filename: z.string().optional().describe("Filename to present. Defaults to index.html for inline HTML."),
|
|
1849
1913
|
anonymous: z.boolean().optional().describe("Force an anonymous drop even if the user is signed in."),
|
|
1850
|
-
|
|
1914
|
+
grid: z.string().optional().describe("Leave unset; the tool will ask the user which grid to publish into. Only set this after the user picks from the list the tool returns."),
|
|
1915
|
+
org: z.string().optional().describe("Deprecated alias for `grid` (kept for compatibility). Prefer `grid`."),
|
|
1851
1916
|
...dropReplugParams,
|
|
1852
1917
|
}
|
|
1853
1918
|
: {
|
|
@@ -1855,7 +1920,8 @@ export function registerTools(server, ctx) {
|
|
|
1855
1920
|
path: z.string().optional().describe("Path to a local file to upload instead of inline HTML."),
|
|
1856
1921
|
filename: z.string().optional().describe("Filename to present. Defaults to index.html for inline HTML."),
|
|
1857
1922
|
anonymous: z.boolean().optional().describe("Force an anonymous drop even if the user is signed in."),
|
|
1858
|
-
|
|
1923
|
+
grid: z.string().optional().describe("Leave unset; the tool will ask the user which grid to publish into. Only set this after the user picks from the list the tool returns."),
|
|
1924
|
+
org: z.string().optional().describe("Deprecated alias for `grid` (kept for compatibility). Prefer `grid`."),
|
|
1859
1925
|
...dropReplugParams,
|
|
1860
1926
|
};
|
|
1861
1927
|
|
|
@@ -1877,13 +1943,20 @@ export function registerTools(server, ctx) {
|
|
|
1877
1943
|
value: z.string().describe("Visibility value to pass to gridctl_visibility."),
|
|
1878
1944
|
label: z.string().describe("Human-readable label."),
|
|
1879
1945
|
})).optional().describe("Available visibility levels."),
|
|
1880
|
-
|
|
1946
|
+
needs_grid: z.boolean().optional().describe("True when the user must choose a grid before dropping."),
|
|
1947
|
+
needs_org: z.boolean().optional().describe("Deprecated alias of needs_grid (kept so the org-picker widget keeps working)."),
|
|
1948
|
+
grids: z.array(z.object({
|
|
1949
|
+
slug: z.string().describe("Grid slug to pass as the grid parameter."),
|
|
1950
|
+
name: z.string().describe("Human-readable grid name."),
|
|
1951
|
+
role: z.string().describe("User's role in the grid."),
|
|
1952
|
+
is_active: z.boolean().optional().describe("True if this is the user's currently active grid."),
|
|
1953
|
+
})).optional().describe("The user's grids, when a grid choice is needed."),
|
|
1881
1954
|
orgs: z.array(z.object({
|
|
1882
|
-
slug: z.string().describe("
|
|
1883
|
-
name: z.string().describe("Human-readable
|
|
1884
|
-
role: z.string().describe("User's role in the
|
|
1885
|
-
is_active: z.boolean().optional().describe("True if this is the user's currently active
|
|
1886
|
-
})).optional().describe("
|
|
1955
|
+
slug: z.string().describe("Grid slug to pass as the grid parameter."),
|
|
1956
|
+
name: z.string().describe("Human-readable grid name."),
|
|
1957
|
+
role: z.string().describe("User's role in the grid."),
|
|
1958
|
+
is_active: z.boolean().optional().describe("True if this is the user's currently active grid."),
|
|
1959
|
+
})).optional().describe("Deprecated alias of grids (kept so the org-picker widget keeps working)."),
|
|
1887
1960
|
needs_sign_in: z.boolean().optional().describe("True when sign-in is needed before dropping."),
|
|
1888
1961
|
login_url: z.string().optional().describe("Sign-in URL when authentication is needed."),
|
|
1889
1962
|
},
|
|
@@ -1918,63 +1991,37 @@ export function registerTools(server, ctx) {
|
|
|
1918
1991
|
}
|
|
1919
1992
|
}
|
|
1920
1993
|
|
|
1921
|
-
// Stateless
|
|
1994
|
+
// Stateless grid disambiguation — both editions, when authenticated.
|
|
1922
1995
|
// No dependency on prior-call state so it works even when the client
|
|
1923
|
-
// reconnects on every tool call (ChatGPT Apps SDK behaviour).
|
|
1996
|
+
// reconnects on every tool call (ChatGPT Apps SDK behaviour). `grid` is
|
|
1997
|
+
// accepted as an alias for `org` (Gilad's rename); `org` still works.
|
|
1924
1998
|
if (input?.anonymous !== true) {
|
|
1925
1999
|
const token = await ctx.getToken();
|
|
1926
2000
|
if (token) {
|
|
1927
|
-
const
|
|
1928
|
-
const
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
}));
|
|
1943
|
-
// Sort: active org first, then ready orgs, then not-ready orgs.
|
|
1944
|
-
annotated.sort((a, b) => {
|
|
1945
|
-
if (a.is_active !== b.is_active) return b.is_active ? 1 : -1;
|
|
1946
|
-
if (a.render_ready !== b.render_ready) return b.render_ready ? 1 : -1;
|
|
1947
|
-
return 0;
|
|
1948
|
-
});
|
|
1949
|
-
const lines = ["Which org should this be published to?"];
|
|
1950
|
-
for (const o of annotated) {
|
|
1951
|
-
const tags = [];
|
|
1952
|
-
if (o.is_active) tags.push("your active org");
|
|
1953
|
-
if (!o.render_ready) tags.push("not set up yet");
|
|
1954
|
-
const suffix = tags.length ? ` (${tags.join(", ")})` : "";
|
|
1955
|
-
lines.push(` ${o.slug} — ${o.name} (${o.role})${suffix}`);
|
|
1956
|
-
}
|
|
1957
|
-
lines.push("Pass the org slug in the org parameter to publish.");
|
|
1958
|
-
const readyCount = annotated.filter((o) => o.render_ready).length;
|
|
1959
|
-
if (readyCount === 0) {
|
|
1960
|
-
lines.push("Note: none of your orgs are fully set up yet. You can use anonymous: true as a fallback.");
|
|
1961
|
-
}
|
|
1962
|
-
return okResult({
|
|
1963
|
-
text: lines.join("\n"),
|
|
1964
|
-
structured: { needs_org: true, orgs: annotated },
|
|
1965
|
-
...(ctx.edition === "web" ? { meta: { "openai/outputTemplate": ORG_PICKER_URI } } : {}),
|
|
1966
|
-
});
|
|
1967
|
-
} else if (orgs.length === 1) {
|
|
1968
|
-
if (!orgs[0].render_ready) {
|
|
1969
|
-
// Single org but not set up — tell the agent so it can warn
|
|
1970
|
-
// the user and offer anonymous drop as fallback.
|
|
1971
|
-
const annotated = [{ ...orgs[0], is_active: orgs[0].slug === activeOrg, render_ready: false }];
|
|
2001
|
+
const suppliedGrid = input?.grid || input?.org;
|
|
2002
|
+
const decision = await resolveGridOrAsk(ctx, {
|
|
2003
|
+
token,
|
|
2004
|
+
suppliedGrid,
|
|
2005
|
+
edition: ctx.edition,
|
|
2006
|
+
});
|
|
2007
|
+
if (decision.picker) {
|
|
2008
|
+
return okResult(decision.picker);
|
|
2009
|
+
}
|
|
2010
|
+
if (decision.single) {
|
|
2011
|
+
if (!decision.single.render_ready) {
|
|
2012
|
+
// Single grid but not set up — tell the agent so it can warn
|
|
2013
|
+
// the user and offer anonymous drop as fallback. (drop-specific:
|
|
2014
|
+
// drop blocks here; plug proceeds with a warning.)
|
|
2015
|
+
const annotated = [{ ...decision.single, render_ready: false }];
|
|
1972
2016
|
return okResult({
|
|
1973
|
-
text: `Your only
|
|
1974
|
-
structured: { needs_org: true, org_not_ready: true, orgs: annotated },
|
|
2017
|
+
text: `Your only grid "${decision.single.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.`,
|
|
2018
|
+
structured: { needs_grid: true, needs_org: true, grid_not_ready: true, org_not_ready: true, grids: annotated, orgs: annotated },
|
|
1975
2019
|
});
|
|
1976
2020
|
}
|
|
1977
|
-
input = { ...(input || {}), org:
|
|
2021
|
+
input = { ...(input || {}), org: decision.single.slug };
|
|
2022
|
+
} else if (decision.grid) {
|
|
2023
|
+
// Supplied grid matched — normalize onto `org` for runDrop.
|
|
2024
|
+
input = { ...(input || {}), org: decision.grid };
|
|
1978
2025
|
}
|
|
1979
2026
|
}
|
|
1980
2027
|
}
|
|
@@ -2090,6 +2137,42 @@ export function registerTools(server, ctx) {
|
|
|
2090
2137
|
},
|
|
2091
2138
|
async (input) => {
|
|
2092
2139
|
try {
|
|
2140
|
+
// Grid-picker parity with gridctl_drop: a signed-in user with >1 grid is
|
|
2141
|
+
// ASKED which grid to publish to on every CREATE. Only for authed creates
|
|
2142
|
+
// (no target_entity_id, not anon). Edits NEVER ask — the grid is fixed by
|
|
2143
|
+
// the entity. Anon proceeds as a Guest-Grid drop. Explicit valid grid
|
|
2144
|
+
// proceeds. A single grid proceeds (with a warning if it isn't set up yet).
|
|
2145
|
+
const isEdit =
|
|
2146
|
+
typeof input?.target_entity_id === "string" && input.target_entity_id.length > 0;
|
|
2147
|
+
if (input?.anon !== true && !isEdit) {
|
|
2148
|
+
const token = await ctx.getToken();
|
|
2149
|
+
if (token) {
|
|
2150
|
+
const decision = await resolveGridOrAsk(ctx, {
|
|
2151
|
+
token,
|
|
2152
|
+
suppliedGrid: input?.grid,
|
|
2153
|
+
edition: ctx.edition,
|
|
2154
|
+
});
|
|
2155
|
+
if (decision.picker) {
|
|
2156
|
+
// Do NOT silently default to the active grid — surface the ask.
|
|
2157
|
+
return okResult(decision.picker);
|
|
2158
|
+
}
|
|
2159
|
+
if (decision.single) {
|
|
2160
|
+
// Proceed into the single grid; warn (don't block) if not set up yet.
|
|
2161
|
+
input = { ...(input || {}), grid: decision.single.slug };
|
|
2162
|
+
if (decision.single.render_ready === false) {
|
|
2163
|
+
const res = await runPlug(ctx, input || {});
|
|
2164
|
+
return okResult({
|
|
2165
|
+
...res,
|
|
2166
|
+
text:
|
|
2167
|
+
`Warning: your only grid "${decision.single.slug}" isn't fully set up yet — the page may not load until provisioning completes.\n` +
|
|
2168
|
+
res.text,
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
2171
|
+
} else if (decision.grid) {
|
|
2172
|
+
input = { ...(input || {}), grid: decision.grid };
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2093
2176
|
return okResult(await runPlug(ctx, input || {}));
|
|
2094
2177
|
} catch (err) {
|
|
2095
2178
|
return fail(err.message);
|
|
@@ -34,18 +34,20 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;b
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
function render(data) {
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
// `grids` is the current field; fall back to `orgs` for older payloads.
|
|
38
|
+
var items = (data && (data.grids || data.orgs)) || null;
|
|
39
|
+
if (!items || items.length === 0) {
|
|
40
|
+
root.innerHTML = '<div class="card"><div class="title">No grids found</div></div>';
|
|
39
41
|
return;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
var html =
|
|
43
45
|
'<div class="card">' +
|
|
44
|
-
'<div class="title">Choose
|
|
45
|
-
'<div class="subtitle">Which
|
|
46
|
+
'<div class="title">Choose a grid</div>' +
|
|
47
|
+
'<div class="subtitle">Which grid should this be published to?</div>' +
|
|
46
48
|
'<div class="orgs">';
|
|
47
|
-
for (var i = 0; i <
|
|
48
|
-
var org =
|
|
49
|
+
for (var i = 0; i < items.length; i++) {
|
|
50
|
+
var org = items[i];
|
|
49
51
|
var notReady = org.render_ready === false;
|
|
50
52
|
html +=
|
|
51
53
|
'<button class="org-btn' + (notReady ? ' not-ready' : '') + '" data-slug="' + esc(org.slug) + '">' +
|
|
@@ -71,7 +73,8 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;b
|
|
|
71
73
|
if (input.html) args.html = input.html;
|
|
72
74
|
if (input.filename) args.filename = input.filename;
|
|
73
75
|
if (input.fresh !== undefined) args.fresh = input.fresh;
|
|
74
|
-
args.
|
|
76
|
+
args.grid = slug;
|
|
77
|
+
args.org = slug; // alias kept for compatibility
|
|
75
78
|
window.openai.callTool("cloudgrid_drop", args).then(function () {
|
|
76
79
|
// The host will render the new result with the live-result card.
|
|
77
80
|
}).catch(function () {
|