@cloudgrid-io/mcp 0.20.45 → 0.20.47
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/auth.js +18 -4
- package/src/corpus/playbook.md +5 -5
- package/src/corpus/templates/ai-knowledge-base/AGENTS.md +1 -1
- package/src/corpus/templates/appointment-booking/AGENTS.md +5 -5
- package/src/corpus/templates/appointment-booking/index.md +4 -4
- package/src/corpus/templates/approval-workflow/AGENTS.md +1 -1
- package/src/corpus/templates/calendar-scheduler/AGENTS.md +1 -1
- package/src/corpus/templates/forum/AGENTS.md +4 -4
- package/src/corpus/templates/marketplace/AGENTS.md +1 -1
- package/src/corpus/templates/membership-site/AGENTS.md +5 -5
- package/src/corpus/templates/membership-site/index.md +4 -4
- package/src/corpus/templates/restaurant-reservations/AGENTS.md +7 -7
- package/src/corpus/templates/restaurant-reservations/index.md +1 -1
- package/src/corpus/templates/subscription-management/AGENTS.md +5 -5
- package/src/corpus/templates/subscription-management/index.md +4 -4
- package/src/corpus/troubleshooting/persistent-apps.md +1 -1
- package/src/corpus/workflows/admin-dashboard.md +3 -3
- package/src/corpus/workflows/ai-app.md +4 -4
- package/src/corpus/workflows/ai-knowledge-base.md +3 -3
- package/src/corpus/workflows/analytics-dashboard.md +3 -3
- package/src/corpus/workflows/api-dashboard.md +3 -3
- package/src/corpus/workflows/api-service.md +4 -4
- package/src/corpus/workflows/app-with-data.md +4 -4
- package/src/corpus/workflows/appointment-booking.md +4 -4
- package/src/corpus/workflows/approval-workflow.md +2 -2
- package/src/corpus/workflows/billing-dashboard.md +4 -4
- package/src/corpus/workflows/blog-cms.md +3 -3
- package/src/corpus/workflows/booking-system.md +4 -4
- package/src/corpus/workflows/calendar-scheduler.md +3 -3
- package/src/corpus/workflows/course-platform.md +4 -4
- package/src/corpus/workflows/crm.md +3 -3
- package/src/corpus/workflows/directory.md +3 -3
- package/src/corpus/workflows/docs-app.md +2 -2
- package/src/corpus/workflows/erp.md +4 -4
- package/src/corpus/workflows/event-board.md +3 -3
- package/src/corpus/workflows/expense-tracker.md +3 -3
- package/src/corpus/workflows/feature-request-board.md +3 -3
- package/src/corpus/workflows/financial-dashboard.md +3 -3
- package/src/corpus/workflows/forum.md +4 -4
- package/src/corpus/workflows/hr-portal.md +4 -4
- package/src/corpus/workflows/internal-tools-portal.md +4 -4
- package/src/corpus/workflows/inventory.md +3 -3
- package/src/corpus/workflows/invoice.md +3 -3
- package/src/corpus/workflows/job-board.md +3 -3
- package/src/corpus/workflows/kanban.md +3 -3
- package/src/corpus/workflows/lms.md +4 -4
- package/src/corpus/workflows/marketplace.md +3 -3
- package/src/corpus/workflows/membership-site.md +4 -4
- package/src/corpus/workflows/monitoring-dashboard.md +3 -3
- package/src/corpus/workflows/multi-service.md +2 -2
- package/src/corpus/workflows/online-store.md +4 -4
- package/src/corpus/workflows/product-catalog.md +3 -3
- package/src/corpus/workflows/project-management.md +3 -3
- package/src/corpus/workflows/property-listings.md +3 -3
- package/src/corpus/workflows/quiz-platform.md +3 -3
- package/src/corpus/workflows/restaurant-reservations.md +4 -4
- package/src/corpus/workflows/revenue-dashboard.md +3 -3
- package/src/corpus/workflows/semantic-search.md +4 -4
- package/src/corpus/workflows/subscription-management.md +4 -4
- package/src/corpus/workflows/task-manager.md +3 -3
- package/src/corpus/workflows/ticket-system.md +3 -3
- package/src/corpus/workflows/time-tracking.md +3 -3
- package/src/corpus/workflows/travel-booking.md +4 -4
- package/src/index.js +2 -0
- package/src/tools/cli.js +4 -3
- package/src/tools/constants.js +1 -1
- package/src/tools/deploy.js +17 -7
- package/src/tools/register.js +70 -36
- package/src/web.js +52 -6
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, checkApiConnectivity } from "../auth.js";
|
|
6
|
+
import { newLoginCode, buildLoginUrl, pollStatusOnce, checkApiConnectivity, decodeJwt as decodeJwtAuth } from "../auth.js";
|
|
7
7
|
import { PLAYBOOK, fetchCorpus, listWorkflows } from "../playbook.js";
|
|
8
8
|
import {
|
|
9
9
|
APPS_WIDGETS_ENABLED,
|
|
@@ -227,9 +227,9 @@ export function registerTools(server, ctx) {
|
|
|
227
227
|
// acknowledgement.
|
|
228
228
|
regTool(
|
|
229
229
|
"grid_note",
|
|
230
|
-
"Optionally leave a one-paragraph summary of what you built this session and why. Call it BEFORE a plug, or in a session that ends without one — a successful plug has already posted the QA log, so pass grid_plug's session_note instead.
|
|
230
|
+
"Optionally leave a one-paragraph summary of what you built this session and why. Call it BEFORE a plug, or in a session that ends without one — a successful plug has already posted the QA log, so pass grid_plug's session_note instead. The summary is recorded with the CloudGrid team for QA review.",
|
|
231
231
|
{ summary: z.string().describe("A short plain-language summary of what was built and why.") },
|
|
232
|
-
{ readOnlyHint:
|
|
232
|
+
{ readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
233
233
|
async (input) => {
|
|
234
234
|
try { ctx.logger?.setNarrative(input?.summary); } catch { /* never */ }
|
|
235
235
|
// Honesty after flush: a successful deploy posts the QA log on that same
|
|
@@ -327,10 +327,6 @@ export function registerTools(server, ctx) {
|
|
|
327
327
|
),
|
|
328
328
|
};
|
|
329
329
|
|
|
330
|
-
// grid_plug is the create/re-plug verb. `grid_deploy` is kept as a deprecated
|
|
331
|
-
// back-compat alias (registered right after the primary below); the canonical
|
|
332
|
-
// name is grid_plug, and "deploy" survives only as an intent keyword in this
|
|
333
|
-
// tool's description. MCP-tool name only — the CLI verb `grid plug` is unchanged.
|
|
334
330
|
const plugConfig = {
|
|
335
331
|
description:
|
|
336
332
|
"Plug an app, website, game, or single HTML page into CloudGrid — the live runtime that runs it and provides its infrastructure — and get a live public URL. " +
|
|
@@ -454,17 +450,30 @@ export function registerTools(server, ctx) {
|
|
|
454
450
|
}
|
|
455
451
|
if (!isEdit) {
|
|
456
452
|
const token = await ctx.getToken();
|
|
457
|
-
if (
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
453
|
+
if (input?.anon === true && ctx.state?.authChoiceOffered !== true) {
|
|
454
|
+
if (ctx.state) ctx.state.authChoiceOffered = true;
|
|
455
|
+
if (token) {
|
|
456
|
+
const claims = decodeJwtAuth(token);
|
|
457
|
+
const who = claims.email ? ` as ${claims.email}` : "";
|
|
458
|
+
return okResult({
|
|
459
|
+
text:
|
|
460
|
+
`You are signed in${who}. The user has not explicitly chosen anonymous publishing. Ask the user which they want — do not choose for them:\n` +
|
|
461
|
+
` 1. Publish to your grid (re-call grid_plug without anon: true).\n` +
|
|
462
|
+
` 2. Publish anonymously — a guest link that expires in 7 days unless claimed (re-call grid_plug with anon: true).\n` +
|
|
463
|
+
`Relay this question to the user and STOP until they answer.`,
|
|
464
|
+
structured: { needs_auth: true },
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
return okResult({
|
|
468
|
+
text:
|
|
469
|
+
"This is a new publish and the user is not signed in. Ask the user which they want — do not choose for them:\n" +
|
|
470
|
+
" 1. Sign in and plug it to their grid — run grid_login (opens the browser), then re-call grid_plug.\n" +
|
|
471
|
+
" 2. Plug it as a guest — live immediately at a guest link that expires in 7 days unless claimed (re-call grid_plug with anon: true).\n" +
|
|
472
|
+
"Relay this question to the user and STOP until they answer.",
|
|
473
|
+
structured: { needs_auth: true },
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
if (!token && input?.anon !== true) {
|
|
468
477
|
if (ctx.state) ctx.state.authChoiceOffered = true;
|
|
469
478
|
return okResult({
|
|
470
479
|
text:
|
|
@@ -752,7 +761,7 @@ export function registerTools(server, ctx) {
|
|
|
752
761
|
});
|
|
753
762
|
const readyCount = annotated.filter((o) => o.render_ready).length;
|
|
754
763
|
if (readyCount === 0 && annotated.length > 0) {
|
|
755
|
-
lines.push("\nNone of your grids are fully set up yet.
|
|
764
|
+
lines.push("\nNone of your grids are fully set up yet. Wait until provisioning completes (grid_start will show render_ready: true) before plugging. Do not switch to anonymous unless the user asks for it.");
|
|
756
765
|
}
|
|
757
766
|
// Structured output stays `orgs` (its declared schema); user text says grid.
|
|
758
767
|
return okResult({ text: lines.join("\n"), structured: { orgs: annotated } });
|
|
@@ -786,6 +795,9 @@ export function registerTools(server, ctx) {
|
|
|
786
795
|
.object({
|
|
787
796
|
active_grid: z.string().nullable().describe("The user's active grid/org slug, or null."),
|
|
788
797
|
signed_in: z.boolean().describe("Whether the current session is signed in."),
|
|
798
|
+
email: z.string().optional().describe("The signed-in user's email, when available."),
|
|
799
|
+
session_expired: z.boolean().optional().describe("True when credentials exist but the JWT has expired. Run grid_login to sign in again."),
|
|
800
|
+
identity_changed: z.boolean().optional().describe("True when the session's identity changed via a different transport Bearer. Session state was reset."),
|
|
789
801
|
update_available: z
|
|
790
802
|
.object({
|
|
791
803
|
current: z.string().describe("This MCP's version."),
|
|
@@ -802,9 +814,26 @@ export function registerTools(server, ctx) {
|
|
|
802
814
|
async () => {
|
|
803
815
|
const workflows = listWorkflows();
|
|
804
816
|
let signedIn = false;
|
|
817
|
+
let sessionExpired = false;
|
|
818
|
+
let signedInEmail = null;
|
|
805
819
|
let activeGrid = null;
|
|
806
820
|
try {
|
|
807
|
-
|
|
821
|
+
if (ctx.getCredentialsStatus) {
|
|
822
|
+
const status = await ctx.getCredentialsStatus();
|
|
823
|
+
sessionExpired = status.expired;
|
|
824
|
+
if (status.creds?.jwt) {
|
|
825
|
+
signedIn = true;
|
|
826
|
+
const claims = decodeJwtAuth(status.creds.jwt);
|
|
827
|
+
signedInEmail = claims.email ?? null;
|
|
828
|
+
}
|
|
829
|
+
} else {
|
|
830
|
+
const token = await ctx.getToken();
|
|
831
|
+
signedIn = Boolean(token);
|
|
832
|
+
if (token) {
|
|
833
|
+
const claims = decodeJwtAuth(token);
|
|
834
|
+
signedInEmail = claims.email ?? null;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
808
837
|
} catch {
|
|
809
838
|
signedIn = false;
|
|
810
839
|
}
|
|
@@ -813,28 +842,33 @@ export function registerTools(server, ctx) {
|
|
|
813
842
|
} catch {
|
|
814
843
|
activeGrid = null;
|
|
815
844
|
}
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
// one-line user action — the model relays the note in-session.
|
|
845
|
+
const identityChanged = ctx.state?.identityChanged === true;
|
|
846
|
+
if (identityChanged) ctx.state.identityChanged = false;
|
|
819
847
|
const stale = stalenessNote(ctx.staleness);
|
|
820
|
-
const
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
},
|
|
848
|
+
const contextObj = {
|
|
849
|
+
active_grid: activeGrid,
|
|
850
|
+
signed_in: signedIn,
|
|
851
|
+
...(signedInEmail ? { email: signedInEmail } : {}),
|
|
852
|
+
...(sessionExpired ? { session_expired: true } : {}),
|
|
853
|
+
...(identityChanged ? { identity_changed: true } : {}),
|
|
854
|
+
...(ctx.staleness?.behind
|
|
855
|
+
? { update_available: { current: ctx.staleness.current, latest: ctx.staleness.latest } }
|
|
856
|
+
: {}),
|
|
830
857
|
};
|
|
858
|
+
const structured = { playbook: PLAYBOOK, workflows, context: contextObj };
|
|
831
859
|
const wfLines = workflows.length
|
|
832
860
|
? workflows.map((w) => ` - ${w.name}: ${w.when || w.summary}`).join("\n")
|
|
833
861
|
: " (none available)";
|
|
834
|
-
|
|
862
|
+
let text =
|
|
835
863
|
`${PLAYBOOK}\n\nAvailable workflows:\n${wfLines}\n\n` +
|
|
836
864
|
`Next: match the intent to a workflow and call grid_get_template({kind:"workflow", name}).` +
|
|
837
865
|
(stale ? `\n\n${stale}` : "");
|
|
866
|
+
if (sessionExpired) {
|
|
867
|
+
text += "\n\nYour CloudGrid sign-in has expired. Run grid_login to sign in again.";
|
|
868
|
+
}
|
|
869
|
+
if (identityChanged) {
|
|
870
|
+
text += "\n\nYour session identity changed (a different account connected via transport). Session state has been reset.";
|
|
871
|
+
}
|
|
838
872
|
return okResult({ text, structured });
|
|
839
873
|
},
|
|
840
874
|
);
|
|
@@ -843,7 +877,7 @@ export function registerTools(server, ctx) {
|
|
|
843
877
|
"grid_get_template",
|
|
844
878
|
{
|
|
845
879
|
description:
|
|
846
|
-
"Load a specific CloudGrid workflow, template, example, rule, or doc by name — deterministic retrieval from the bundled corpus
|
|
880
|
+
"Load a specific CloudGrid workflow, template, example, rule, or doc by name — deterministic retrieval from the bundled corpus. Use after grid_start to pull the exact recipe/template you need, e.g. grid_get_template({kind:\"workflow\", name:\"presentation\"}) then grid_get_template({kind:\"template\", name:\"deck\"}).",
|
|
847
881
|
inputSchema: {
|
|
848
882
|
kind: z
|
|
849
883
|
.enum(["workflow", "template", "example", "rule", "troubleshooting", "doc"])
|
|
@@ -965,7 +999,7 @@ export function registerTools(server, ctx) {
|
|
|
965
999
|
cwd: z.string().optional().describe("Working directory. The CLI runs in this directory. Defaults to the MCP server's working directory."),
|
|
966
1000
|
},
|
|
967
1001
|
{ readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
968
|
-
cliTool((input) => buildCreateProjectArgs(input), { cwdParam: true }),
|
|
1002
|
+
cliTool((input) => buildCreateProjectArgs(input), { cwdParam: true, excludeDirFromCwd: true }),
|
|
969
1003
|
);
|
|
970
1004
|
|
|
971
1005
|
// NOTE: grid_plug is no longer CLI-wrapping — the unified direct-API verb
|
package/src/web.js
CHANGED
|
@@ -55,6 +55,12 @@ function bearerOf(req) {
|
|
|
55
55
|
return h && /^Bearer\s+\S+/i.test(h) ? h.replace(/^Bearer\s+/i, "") : null;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
function isTokenExpired(jwt) {
|
|
59
|
+
if (!jwt) return false;
|
|
60
|
+
const claims = decodeJwt(jwt);
|
|
61
|
+
return Boolean(claims.exp && claims.exp * 1000 <= Date.now());
|
|
62
|
+
}
|
|
63
|
+
|
|
58
64
|
// A web session: identity lives in memory for the session's lifetime only. The
|
|
59
65
|
// session id doubles as the stable, opaque end-user id for the trusted-server cap.
|
|
60
66
|
function makeWebContext(sessionId) {
|
|
@@ -63,10 +69,17 @@ function makeWebContext(sessionId) {
|
|
|
63
69
|
edition: "web",
|
|
64
70
|
state: { pendingLoginCode: null, lastAnonClaim: null, lastDrop: null, anonCookie: null },
|
|
65
71
|
canOpenBrowser: false,
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
getToken: async () => {
|
|
73
|
+
const jwt = sessionAuth[sessionId] ?? sessionToken;
|
|
74
|
+
if (jwt && isTokenExpired(jwt)) return null;
|
|
75
|
+
return jwt;
|
|
76
|
+
},
|
|
77
|
+
getCredentialsStatus: async () => {
|
|
78
|
+
const jwt = sessionAuth[sessionId] ?? sessionToken;
|
|
79
|
+
if (!jwt) return { creds: null, expired: false };
|
|
80
|
+
if (isTokenExpired(jwt)) return { creds: null, expired: true };
|
|
81
|
+
return { creds: { jwt }, expired: false };
|
|
82
|
+
},
|
|
70
83
|
getActiveGrid: async () => null,
|
|
71
84
|
saveToken: async (jwt) => {
|
|
72
85
|
sessionToken = jwt;
|
|
@@ -89,6 +102,7 @@ mountOAuth(app, PUBLIC_BASE);
|
|
|
89
102
|
|
|
90
103
|
// One transport per MCP session, keyed by the session id.
|
|
91
104
|
const transports = Object.create(null);
|
|
105
|
+
const sessionContexts = Object.create(null);
|
|
92
106
|
|
|
93
107
|
app.post("/mcp", async (req, res) => {
|
|
94
108
|
const sessionId = req.headers["mcp-session-id"];
|
|
@@ -106,7 +120,22 @@ app.post("/mcp", async (req, res) => {
|
|
|
106
120
|
}
|
|
107
121
|
|
|
108
122
|
if (transport) {
|
|
109
|
-
if (jwt)
|
|
123
|
+
if (jwt) {
|
|
124
|
+
const prev = sessionAuth[sessionId];
|
|
125
|
+
if (prev) {
|
|
126
|
+
const prevSub = decodeJwt(prev).sub;
|
|
127
|
+
const newSub = decodeJwt(jwt).sub;
|
|
128
|
+
if (prevSub && newSub && prevSub !== newSub) {
|
|
129
|
+
const ctxForSession = sessionContexts[sessionId];
|
|
130
|
+
if (ctxForSession) {
|
|
131
|
+
ctxForSession.state.identityChanged = true;
|
|
132
|
+
ctxForSession.state.pendingLoginCode = null;
|
|
133
|
+
ctxForSession.state.authChoiceOffered = false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
sessionAuth[sessionId] = jwt;
|
|
138
|
+
}
|
|
110
139
|
await transport.handleRequest(req, res, req.body);
|
|
111
140
|
return;
|
|
112
141
|
}
|
|
@@ -138,10 +167,12 @@ app.post("/mcp", async (req, res) => {
|
|
|
138
167
|
if (transport.sessionId) {
|
|
139
168
|
delete transports[transport.sessionId];
|
|
140
169
|
delete sessionAuth[transport.sessionId];
|
|
170
|
+
delete sessionContexts[transport.sessionId];
|
|
141
171
|
}
|
|
142
172
|
};
|
|
143
173
|
const server = new McpServer({ name: "cloudgrid-mcp-web", version }, { instructions: INSTRUCTIONS_WEB });
|
|
144
174
|
const webCtx = makeWebContext(newSessionId);
|
|
175
|
+
sessionContexts[newSessionId] = webCtx;
|
|
145
176
|
// QA session log for this MCP session (dark by default). Keyed by the session
|
|
146
177
|
// id — the host connection boundary. No seed user_request here: current hosts
|
|
147
178
|
// don't forward the first message. The model-as-courier path fills it instead —
|
|
@@ -178,7 +209,22 @@ async function handleSessionRequest(req, res) {
|
|
|
178
209
|
return;
|
|
179
210
|
}
|
|
180
211
|
const jwt = bearerOf(req);
|
|
181
|
-
if (jwt)
|
|
212
|
+
if (jwt) {
|
|
213
|
+
const prev = sessionAuth[sessionId];
|
|
214
|
+
if (prev) {
|
|
215
|
+
const prevSub = decodeJwt(prev).sub;
|
|
216
|
+
const newSub = decodeJwt(jwt).sub;
|
|
217
|
+
if (prevSub && newSub && prevSub !== newSub) {
|
|
218
|
+
const ctxForSession = sessionContexts[sessionId];
|
|
219
|
+
if (ctxForSession) {
|
|
220
|
+
ctxForSession.state.identityChanged = true;
|
|
221
|
+
ctxForSession.state.pendingLoginCode = null;
|
|
222
|
+
ctxForSession.state.authChoiceOffered = false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
sessionAuth[sessionId] = jwt;
|
|
227
|
+
}
|
|
182
228
|
await transport.handleRequest(req, res);
|
|
183
229
|
}
|
|
184
230
|
|