@cloudgrid-io/mcp 0.20.52 → 0.20.54

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.
@@ -35,13 +35,14 @@ import { stalenessNote } from "../staleness.js";
35
35
  // `--agent` for agents (NOT the legacy `init <kind> <slug>` positional). The
36
36
  // old `--description` flag was removed from the CLI, so it is gone here too.
37
37
  // Exported pure so the argv is unit-tested without spawning a CLI.
38
- export function buildCreateProjectArgs({ kind, name, type, needs, dir, org } = {}) {
38
+ export function buildCreateProjectArgs({ kind, name, type, needs, dir, grid, org } = {}) {
39
39
  const args = ["new", name];
40
40
  if (kind === "agent") args.push("--agent");
41
41
  if (type) args.push("--type", type);
42
42
  if (Array.isArray(needs) && needs.length) args.push("--needs", needs.join(","));
43
43
  if (dir) args.push("--dir", dir);
44
- if (org) args.push("--grid", org); // CLI 0.12 dropped --org for --grid (same slug)
44
+ const gridSlug = grid || org;
45
+ if (gridSlug) args.push("--grid", gridSlug);
45
46
  return args;
46
47
  }
47
48
 
@@ -86,8 +87,8 @@ export function registerTools(server, ctx) {
86
87
  server.registerTool(name, config, withCapture(name, handler));
87
88
  };
88
89
 
89
- const regTool = (name, description, schema, annotations, handler) => {
90
- server.tool(name, description, schema, annotations, withCapture(name, handler));
90
+ const regTool = (name, title, description, schema, annotations, handler) => {
91
+ server.registerTool(name, { title, description, inputSchema: schema, annotations }, withCapture(name, handler));
91
92
  };
92
93
 
93
94
  // ── Widget resources (web edition, ChatGPT Apps SDK) ──────────────────────
@@ -130,10 +131,13 @@ export function registerTools(server, ctx) {
130
131
  reg(
131
132
  "grid_pickup",
132
133
  {
134
+ title: "Copy an app into your grid",
133
135
  description: "Pick up an app: make your OWN COPY of any app you can see (like a git fork) into a grid you can build in. It mints a NEW entity with lineage back to the source and WITHOUT the source's secrets (set your own before you plug). Plugging your copy creates/updates YOUR entity — the original is never touched. Requires sign-in. To edit the ORIGINAL entity in place (as its owner or a collaborator), use grid_pull instead.",
134
136
  inputSchema: {
135
- id: z.string().describe("The source app to copy: a canonical UUID or <grid-slug>/<entity-slug>."),
136
- into_org_slug: z.string().optional().describe("Grid to create your copy in. Required only when you belong to more than one grid."),
137
+ entity_id: z.string().describe("The source app to copy: a canonical UUID or <grid-slug>/<entity-slug>."),
138
+ grid: z.string().optional().describe("Grid to create your copy in. Required only when you belong to more than one grid."),
139
+ id: z.string().optional().describe("Alias of entity_id (legacy). Prefer entity_id."),
140
+ into_org_slug: z.string().optional().describe("Alias of grid (legacy). Prefer grid."),
137
141
  name: z.string().optional().describe("Slug for your copy. Omit to derive one from the source."),
138
142
  source_version_id: z.string().optional().describe("Copy an older version instead of HEAD, e.g. v_a1b2c3d."),
139
143
  },
@@ -150,8 +154,13 @@ export function registerTools(server, ctx) {
150
154
  },
151
155
  async (input) => {
152
156
  try {
153
- if (!input?.id) return fail("`id` is required (a canonical UUID or <grid-slug>/<entity-slug>).");
154
- return okResult(await runPickup(ctx, input));
157
+ const resolved = {
158
+ ...input,
159
+ id: input?.entity_id || input?.id,
160
+ into_org_slug: input?.grid || input?.into_org_slug,
161
+ };
162
+ if (!resolved.id) return fail("`entity_id` is required (a canonical UUID or <grid-slug>/<entity-slug>).");
163
+ return okResult(await runPickup(ctx, resolved));
155
164
  } catch (err) {
156
165
  return fail(err.message);
157
166
  }
@@ -162,6 +171,7 @@ export function registerTools(server, ctx) {
162
171
  reg(
163
172
  "grid_pull",
164
173
  {
174
+ title: "Pull an app to edit in place",
165
175
  description: "Pull an app to continue/edit it IN PLACE — like `git clone` of the SAME entity: your next grid_plug (with its target_entity_id) updates that entity, and the team sees the new version. Requires PUSH ACCESS: you must own it or be a collaborator. If you can only view it, you CANNOT edit or plug it — say so and tell the user they can make their own copy with grid_pickup, or request collaborator access with the CLI `grid collab <entity>` (rolling out soon). Passing an anonymous drop's `claim_token` claims it into your account. Requires sign-in. Calls the API directly (both editions).",
166
176
  inputSchema: {
167
177
  entity_id: z.string().optional().describe("The entity id to pull. Defaults to this session's last anonymous drop."),
@@ -194,13 +204,15 @@ export function registerTools(server, ctx) {
194
204
  reg(
195
205
  "grid_create_grid",
196
206
  {
197
- description: "Create a new grid (workspace) for the signed-in user — they become its admin. Use when the account has no grid yet (grid_plug returns needs_grid_create, or a plug fails with NO_ACTIVE_ORG): suggest a short slug from the user's name or app, CONFIRM it with the user (the slug is permanent and appears in URLs), create, then re-call grid_plug with grid: <slug>. Never send the user to the console to create a grid by hand. Requires sign-in. Calls the API directly (both editions).",
207
+ title: "Create a new grid",
208
+ description: "Create a new grid (workspace) for the signed-in user — they become its admin. Use when the account has no grid yet (grid_plug returns needs_grid_create, or a plug fails with NO_ACTIVE_ORG): suggest a short slug from the user's name or app, CONFIRM it with the user (the slug is permanent and appears in URLs), create, then call grid_plug with grid: <slug>. A brand-new grid provisions in the background (~30s); grid_plug waits for readiness before deploying, so call it right away — no manual delay or polling needed. Never send the user to the console to create a grid by hand. Requires sign-in. Calls the API directly (both editions).",
198
209
  inputSchema: {
199
210
  slug: z.string().describe("The grid slug: 3-40 lowercase letters, digits, or hyphens, starting with a letter. Permanent — confirm with the user before creating."),
200
211
  name: z.string().optional().describe("Display name (defaults to the slug)."),
201
212
  },
202
213
  outputSchema: {
203
214
  created: z.boolean().optional().describe("True when the grid was created."),
215
+ provisioning: z.boolean().optional().describe("True when the grid is still finishing setup in the background. grid_plug waits for readiness before deploying, so call it right away — no manual polling needed."),
204
216
  grid: z.object({
205
217
  slug: z.string().describe("The new grid's slug — pass it to grid_plug as `grid`."),
206
218
  name: z.string().optional().describe("Its display name."),
@@ -227,6 +239,7 @@ export function registerTools(server, ctx) {
227
239
  // acknowledgement.
228
240
  regTool(
229
241
  "grid_note",
242
+ "Leave a session build note",
230
243
  "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
244
  { summary: z.string().describe("A short plain-language summary of what was built and why.") },
232
245
  { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
@@ -328,6 +341,7 @@ export function registerTools(server, ctx) {
328
341
  };
329
342
 
330
343
  const plugConfig = {
344
+ title: "Plug an app into the grid",
331
345
  description:
332
346
  "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. " +
333
347
  "Use for any request to deploy, publish, host, ship, launch, go live, or share a working link — " +
@@ -530,6 +544,7 @@ export function registerTools(server, ctx) {
530
544
  reg(
531
545
  "grid_get_app_source",
532
546
  {
547
+ title: "Get deployed app source",
533
548
  description:
534
549
  "Retrieve the CURRENT deployed HTML of an inspiration/drop inline as text, so you can edit it and " +
535
550
  "re-plug the SAME URL when you no longer have its source in context (e.g. the user asks to 'change the " +
@@ -583,6 +598,7 @@ export function registerTools(server, ctx) {
583
598
  reg(
584
599
  "grid_login",
585
600
  {
601
+ title: "Sign in to CloudGrid",
586
602
  description: "Start a CLI-free CloudGrid sign-in. Use when the user wants to log in, sign in, or authenticate, or to claim an anonymous drop. Returns a URL to open in the browser; then call grid_login_status to finish. Uses CloudGrid's existing OAuth.",
587
603
  inputSchema: {},
588
604
  outputSchema: {
@@ -613,6 +629,7 @@ export function registerTools(server, ctx) {
613
629
  reg(
614
630
  "grid_login_status",
615
631
  {
632
+ title: "Check sign-in status",
616
633
  description: "Finish a sign-in started by grid_login. Polls once: if you have completed the browser sign-in, it saves your session; otherwise it tells you to finish and try again.",
617
634
  inputSchema: {
618
635
  code: z.string().optional().describe("The sign-in code. Defaults to the most recent grid_login."),
@@ -662,7 +679,8 @@ export function registerTools(server, ctx) {
662
679
  reg(
663
680
  "grid_visibility",
664
681
  {
665
- description: "Change who can see a CloudGrid inspiration OR runtime app/agent. Simple modes: private (only the user), grid (everyone in their grid), link (anyone with the URL — add indexed: true to be findable by search engines). Finer control via the TWO AXES instead: inside (who in the grid: private | spaces | grid) and outside (reach beyond it: none | link | public), with require_signin for a members-only link and `spaces` for selected spaces. 'authenticated' is retired (it maps to a sign-in-required link); 'org' is gone — use grid. Use when the user wants to make something private, restrict who sees it, or open it up — including right after a drop, with no target id needed. Kind-aware routing across both surfaces. Defaults to the drop made in this session. Requires sign-in. Calls the API directly.",
682
+ title: "Change app visibility",
683
+ description: "Change who can see a CloudGrid inspiration OR runtime app/agent. Simple modes: private (only the user), grid (everyone in their grid), link (anyone with the URL — add indexed: true to be findable by search engines). Finer control via the TWO AXES instead: inside (who in the grid: private | spaces | grid) and outside (reach beyond it: none | link | public), with require_signin for a members-only link and `spaces` for selected spaces. 'authenticated' is retired (it maps to a sign-in-required link). Use when the user wants to make something private, restrict who sees it, or open it up — including right after a drop, with no target id needed. Kind-aware routing across both surfaces. Defaults to the drop made in this session. Requires sign-in. Calls the API directly.",
666
684
  inputSchema: {
667
685
  visibility: z.enum(["private", "grid", "link", "public", "authenticated", "space"]).optional().describe("Simple mode: private | grid | link ('public' is an alias of link — pass indexed: true for search-findable; 'authenticated' is the retired alias for a sign-in-required link; 'space' needs the `spaces` list). Pass either this OR the inside/outside axes."),
668
686
  inside: z.enum(["private", "spaces", "grid"]).optional().describe("Axis 1 — who in the grid can see it: private (only the user), spaces (selected spaces — pass `spaces`), or grid (everyone in the grid). Use together with `outside`, instead of `visibility`."),
@@ -670,9 +688,11 @@ export function registerTools(server, ctx) {
670
688
  require_signin: z.boolean().optional().describe("With outside: link (or visibility: link) — the link requires a signed-in CloudGrid account."),
671
689
  spaces: z.array(z.string()).optional().describe("Space slugs, for inside: spaces (or the legacy space/grid modes)."),
672
690
  indexed: z.boolean().optional().describe("With visibility: link — make the page search-engine indexable."),
673
- target: z.string().optional().describe("Entity id. Defaults to this session's last drop."),
691
+ entity_id: z.string().optional().describe("Entity id. Defaults to this session's last drop."),
674
692
  kind: z.enum(["inspiration", "app", "agent"]).optional().describe("Entity kind. Omit to auto-detect from this session's last drop (falls back to trying the runtime surface, then the inspiration surface)."),
675
- org: z.string().optional().describe("Grid of the entity. Defaults to the active grid."),
693
+ grid: z.string().optional().describe("Grid of the entity. Defaults to the active grid."),
694
+ target: z.string().optional().describe("Alias of entity_id (legacy). Prefer entity_id."),
695
+ org: z.string().optional().describe("Alias of grid (legacy). Prefer grid."),
676
696
  },
677
697
  outputSchema: {
678
698
  visibility: z.string().optional().describe("The legacy mode that was set, when a simple mode was used."),
@@ -687,7 +707,12 @@ export function registerTools(server, ctx) {
687
707
  },
688
708
  async (input) => {
689
709
  try {
690
- return okResult(await runVisibility(ctx, input || {}));
710
+ const resolved = {
711
+ ...(input || {}),
712
+ target: input?.entity_id || input?.target,
713
+ org: input?.grid || input?.org,
714
+ };
715
+ return okResult(await runVisibility(ctx, resolved));
691
716
  } catch (err) {
692
717
  return fail(err.message);
693
718
  }
@@ -701,6 +726,7 @@ export function registerTools(server, ctx) {
701
726
  reg(
702
727
  "grid_check_deploy",
703
728
  {
729
+ title: "Check deploy status",
704
730
  description:
705
731
  "Check whether an async runtime-app build has finished and the app is live. Call this after grid_plug returns status \"building\" — repeat every ~15s until it reports success or failed, and do NOT tell the user the app is live until it does. Defaults to this session's last deploy; pass poll_url from a grid_plug result to check another. Requires sign-in (builds are owned). Calls the API directly — works on every edition, including hosted.",
706
732
  inputSchema: {
@@ -731,6 +757,7 @@ export function registerTools(server, ctx) {
731
757
  reg(
732
758
  "grid_list_grids",
733
759
  {
760
+ title: "List your grids",
734
761
  description: "List the signed-in user's grids, each with slug, name, role, and provisioning status. A grid that is still provisioning (render_ready false) may not serve pages yet — prefer a ready grid, and if the user insists on a not-ready one, warn them that pages may not load. Requires sign-in.",
735
762
  inputSchema: {},
736
763
  outputSchema: {
@@ -784,12 +811,12 @@ export function registerTools(server, ctx) {
784
811
  // ── Agent Core orientation tools (authed editions: local + web) ───────────
785
812
  // These serve the delivery ladder's Orient + Load rungs. They are registered
786
813
  // before the local-only cutoff below, so BOTH the local and web (hosted-auth)
787
- // editions expose them. The anon docs edition (src/docs.js) does NOT call
788
- // registerTools, so it never gets them (spec F3).
814
+ // editions expose them.
789
815
 
790
816
  reg(
791
817
  "grid_start",
792
818
  {
819
+ title: "Orient before building",
793
820
  description:
794
821
  "Orient before building with CloudGrid — the live runtime environment where the user's apps run WITH the infrastructure they need (managed database, cache, persistent disk, AI with no API keys), any language or stack. Call this FIRST when the user wants to build, create, make, deploy, publish, or generate something. Returns the CloudGrid playbook (operating rules + golden path) and the index of available workflows (presentation, …). After this, match the user's intent to a workflow and call grid_get_template to load it.",
795
822
  inputSchema: {},
@@ -889,6 +916,7 @@ export function registerTools(server, ctx) {
889
916
  reg(
890
917
  "grid_get_template",
891
918
  {
919
+ title: "Load a workflow or template",
892
920
  description:
893
921
  "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\"}).",
894
922
  inputSchema: {
@@ -927,8 +955,9 @@ export function registerTools(server, ctx) {
927
955
  reg(
928
956
  "grid_report",
929
957
  {
958
+ title: "Report a failure",
930
959
  description:
931
- "Report a genuine CloudGrid failure to the CloudGrid team — ONLY with the user's explicit consent. When a build/deploy or platform call fails unexpectedly, ASK the user first; call this only after they say yes. Send a short `message` (what failed) plus `context` (the tool, inputs, grid, original request, error code/detail). By default it does NOT include the conversation — set include_conversation:true ONLY if the user explicitly agreed to send the chat. Obvious secrets in context are scrubbed before sending. Never sends anything the user didn't agree to.",
960
+ "Report a genuine CloudGrid failure to the CloudGrid team — ONLY with the user's explicit consent. When a build/deploy or platform call fails unexpectedly, ASK the user first; call this only after they say yes. Send a short `message` (what failed) plus `context` (the tool, inputs, grid, original request, error code/detail). By default it does NOT include the conversation — set include_conversation:true ONLY if the user explicitly agreed to send the chat. Redaction is key-name-only: a value is dropped only when its KEY looks secret, so a secret embedded in a value (e.g. inside HTML) is NOT redacted — do not put secrets in `message` or `context`. Never sends anything the user didn't agree to.",
932
961
  inputSchema: {
933
962
  message: z
934
963
  .string()
@@ -936,7 +965,7 @@ export function registerTools(server, ctx) {
936
965
  context: z
937
966
  .object({
938
967
  tool: z.string().optional().describe("The CloudGrid tool that failed, e.g. grid_plug."),
939
- inputs: z.any().optional().describe("The failing inputs (e.g. the HTML/args). Keep it minimal; secrets are scrubbed."),
968
+ inputs: z.any().optional().describe("The failing inputs (e.g. the HTML/args). Keep it minimal and do NOT include secrets — a secret embedded in a value here is not redacted."),
940
969
  grid: z.string().optional().describe("The grid/org slug involved, if any."),
941
970
  original_request: z.string().optional().describe("What the user asked for, in one line."),
942
971
  error_code: z.string().optional().describe("The server error code, e.g. INTERNAL_ERROR."),
@@ -944,7 +973,7 @@ export function registerTools(server, ctx) {
944
973
  })
945
974
  .partial()
946
975
  .optional()
947
- .describe("The failed-request context. Secret-looking values are scrubbed client-side and again server-side."),
976
+ .describe("The failed-request context. Redaction is key-name-only (a value is dropped only when its KEY looks secret) client-side, and the server applies the same key-name filter — a secret embedded in a value is not redacted by either."),
948
977
  include_conversation: z
949
978
  .boolean()
950
979
  .optional()
@@ -1000,6 +1029,7 @@ export function registerTools(server, ctx) {
1000
1029
 
1001
1030
  regTool(
1002
1031
  "grid_create_project",
1032
+ "Scaffold a new project",
1003
1033
  "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_plug). 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.",
1004
1034
  {
1005
1035
  kind: z.enum(["app", "agent"]).describe("Entity kind. 'agent' scaffolds an agent: block."),
@@ -1008,7 +1038,8 @@ export function registerTools(server, ctx) {
1008
1038
  needs: z.array(z.enum(["database", "cache", "kv", "queue", "pubsub", "vector", "ai", "disk"])).optional()
1009
1039
  .describe("Pre-declare infrastructure needs in the scaffolded cloudgrid.yaml (e.g. [\"database\",\"ai\"])."),
1010
1040
  dir: z.string().optional().describe("Target directory. Defaults to the current folder."),
1011
- org: z.string().optional().describe("Override the active grid this project will target."),
1041
+ grid: z.string().optional().describe("Override the active grid this project will target."),
1042
+ org: z.string().optional().describe("Alias of grid (legacy). Prefer grid."),
1012
1043
  cwd: z.string().optional().describe("Working directory. The CLI runs in this directory. Defaults to the MCP server's working directory."),
1013
1044
  },
1014
1045
  { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
@@ -1021,6 +1052,7 @@ export function registerTools(server, ctx) {
1021
1052
 
1022
1053
  regTool(
1023
1054
  "grid_view_logs",
1055
+ "View entity logs",
1024
1056
  "Tail recent logs for an entity. Does not stream; returns a snapshot. Wraps `grid logs`.",
1025
1057
  {
1026
1058
  name: z.string().optional().describe("Entity name. Omit to use the entity linked to the current directory."),
@@ -1039,6 +1071,7 @@ export function registerTools(server, ctx) {
1039
1071
 
1040
1072
  regTool(
1041
1073
  "grid_share",
1074
+ "Set visibility via CLI",
1042
1075
  "Set an entity's visibility and print its URL. Defaults to link (anyone with the URL). Wraps `grid visibility set`.",
1043
1076
  {
1044
1077
  name: z.string().describe("Entity slug."),
@@ -1050,19 +1083,21 @@ export function registerTools(server, ctx) {
1050
1083
 
1051
1084
  regTool(
1052
1085
  "grid_feedback",
1086
+ "List feedback events",
1053
1087
  "List recent feedback events for the active grid. Read-only. Wraps `grid feedback list`.",
1054
1088
  {
1055
1089
  since: z.string().optional().describe("Only events newer than this, e.g. 24h, 7d."),
1056
1090
  limit: z.number().int().positive().max(200).optional().describe("Number of events. Default 50, max 200."),
1057
- org: z.string().optional().describe("Read another grid's feed where you have access."),
1091
+ grid: z.string().optional().describe("Read another grid's feed where you have access."),
1092
+ org: z.string().optional().describe("Alias of grid (legacy). Prefer grid."),
1058
1093
  },
1059
1094
  { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
1060
- cliTool(({ since, limit, org }) => {
1095
+ cliTool(({ since, limit, grid, org }) => {
1061
1096
  const args = ["feedback", "list"];
1062
1097
  if (since) args.push("--since", since);
1063
1098
  if (limit) args.push("--limit", String(limit));
1064
- // CLI 0.12 dropped `--org` in favour of `--grid` (same slug).
1065
- if (org) args.push("--grid", org);
1099
+ const gridSlug = grid || org;
1100
+ if (gridSlug) args.push("--grid", gridSlug);
1066
1101
  return args;
1067
1102
  }),
1068
1103
  );
@@ -1071,6 +1106,7 @@ export function registerTools(server, ctx) {
1071
1106
 
1072
1107
  regTool(
1073
1108
  "grid_whoami",
1109
+ "Show current user and grid",
1074
1110
  "Show the signed-in user and active grid. Wraps `grid whoami`.",
1075
1111
  {},
1076
1112
  { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
@@ -1079,14 +1115,23 @@ export function registerTools(server, ctx) {
1079
1115
 
1080
1116
  regTool(
1081
1117
  "grid_switch_grid",
1118
+ "Switch the active grid",
1082
1119
  "Switch the active grid. Wraps `grid use`.",
1083
- { org: z.string().describe("Org slug to switch to.") },
1120
+ {
1121
+ grid: z.string().optional().describe("Grid slug to switch to."),
1122
+ org: z.string().optional().describe("Alias of grid (legacy). Prefer grid."),
1123
+ },
1084
1124
  { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
1085
- cliTool(({ org }) => ["use", org]),
1125
+ cliTool(({ grid, org }) => {
1126
+ const slug = grid || org;
1127
+ if (!slug) throw new Error("`grid` is required.");
1128
+ return ["use", slug];
1129
+ }),
1086
1130
  );
1087
1131
 
1088
1132
  regTool(
1089
1133
  "grid_logout",
1134
+ "Sign out",
1090
1135
  "Sign out and clear local credentials. Wraps `grid logout`.",
1091
1136
  {},
1092
1137
  { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
@@ -1095,7 +1140,8 @@ export function registerTools(server, ctx) {
1095
1140
 
1096
1141
  regTool(
1097
1142
  "grid_status",
1098
- "Org dashboard, entity detail, or deploy snapshot. Wraps `grid status`.",
1143
+ "Show grid or entity status",
1144
+ "Grid dashboard, entity detail, or deploy snapshot. Wraps `grid status`.",
1099
1145
  { name: z.string().optional().describe("Entity name or trace id. Omit for the org dashboard.") },
1100
1146
  { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
1101
1147
  cliTool(({ name }) => (name ? ["status", name] : ["status"])),
@@ -1103,6 +1149,7 @@ export function registerTools(server, ctx) {
1103
1149
 
1104
1150
  regTool(
1105
1151
  "grid_info",
1152
+ "Show entity metadata",
1106
1153
  "Show metadata for a CloudGrid entity. Wraps `grid info`.",
1107
1154
  { name: z.string().optional().describe("Entity name. Omit for the entity linked to the current directory.") },
1108
1155
  { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
@@ -1120,6 +1167,7 @@ export function registerTools(server, ctx) {
1120
1167
  // cloudgrid_grid behaviour with `grid` mapping to the CLI's `--grid` flag.
1121
1168
  regTool(
1122
1169
  "grid_get",
1170
+ "List grids, entities, or spaces",
1123
1171
  "List CloudGrid grids, entities, or spaces. Wraps `grid get <grids|entities|spaces> --json`.",
1124
1172
  {
1125
1173
  resource: z.enum(["grids", "entities", "spaces"]).describe("What to list: grids, entities, or spaces."),
@@ -1147,6 +1195,7 @@ export function registerTools(server, ctx) {
1147
1195
 
1148
1196
  regTool(
1149
1197
  "grid_describe_grid",
1198
+ "Describe a grid",
1150
1199
  "Show a grid's detail: role, members, spaces, tier, wildcard-TLS state. Wraps `grid describe grid <slug> --json`.",
1151
1200
  { grid: z.string().describe("Grid slug to describe.") },
1152
1201
  { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
@@ -1160,6 +1209,7 @@ export function registerTools(server, ctx) {
1160
1209
  // pulled — use grid_pickup to copy it, or `grid collab` to request access.
1161
1210
  regTool(
1162
1211
  "grid_edit_existing_app",
1212
+ "Pull app source to edit locally",
1163
1213
  "Continue/edit an EXISTING entity locally: download its source + cloudgrid.yaml and link the folder so your next `grid plug` updates it IN PLACE. Requires push access (owner or collaborator). Wraps `grid pull`. To make your own separate copy instead, use grid_pickup.",
1164
1214
  {
1165
1215
  name: z.string().describe("Entity slug or id to pull."),
@@ -1184,6 +1234,7 @@ export function registerTools(server, ctx) {
1184
1234
 
1185
1235
  regTool(
1186
1236
  "grid_rename",
1237
+ "Rename an entity",
1187
1238
  "Rename a CloudGrid entity's display name (slug stays the same). Wraps `grid rename`.",
1188
1239
  {
1189
1240
  name: z.string().describe("Entity slug."),
@@ -1195,6 +1246,7 @@ export function registerTools(server, ctx) {
1195
1246
 
1196
1247
  regTool(
1197
1248
  "grid_take_offline",
1249
+ "Take an entity offline",
1198
1250
  "Take an entity off the grid. Destructive. Wraps `grid unplug`.",
1199
1251
  {
1200
1252
  name: z.string().describe("Entity slug to take down (required)."),
@@ -1206,6 +1258,7 @@ export function registerTools(server, ctx) {
1206
1258
 
1207
1259
  regTool(
1208
1260
  "grid_delete",
1261
+ "Delete an entity",
1209
1262
  "Archive a CloudGrid inspiration. Destructive. Wraps `grid delete entity`.",
1210
1263
  {
1211
1264
  name: z.string().describe("Entity slug to delete (required)."),
@@ -1217,6 +1270,7 @@ export function registerTools(server, ctx) {
1217
1270
 
1218
1271
  regTool(
1219
1272
  "grid_rollback_deploy",
1273
+ "Roll back to a previous version",
1220
1274
  "Rollback an entity to a previous version. Wraps `grid rollback`.",
1221
1275
  {
1222
1276
  name: z.string().describe("Entity slug."),
@@ -1232,6 +1286,7 @@ export function registerTools(server, ctx) {
1232
1286
 
1233
1287
  regTool(
1234
1288
  "grid_list_versions",
1289
+ "List entity versions",
1235
1290
  "List published versions for an entity. Wraps `grid versions`.",
1236
1291
  { name: z.string().optional().describe("Entity name. Omit for the entity linked to the current directory.") },
1237
1292
  { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
@@ -1244,6 +1299,7 @@ export function registerTools(server, ctx) {
1244
1299
 
1245
1300
  regTool(
1246
1301
  "grid_set_env",
1302
+ "Manage environment variables",
1247
1303
  "Manage environment variables for an entity. Wraps `grid env`.",
1248
1304
  {
1249
1305
  action: z.enum(["get", "set", "list"]).describe("get, set, or list."),
@@ -1268,6 +1324,7 @@ export function registerTools(server, ctx) {
1268
1324
 
1269
1325
  regTool(
1270
1326
  "grid_set_secret",
1327
+ "Manage secrets",
1271
1328
  "Set or list secret names for an entity. Never returns secret values. Wraps `grid secrets`.",
1272
1329
  {
1273
1330
  action: z.enum(["set", "list"]).describe("set or list (names only)."),
@@ -1288,6 +1345,7 @@ export function registerTools(server, ctx) {
1288
1345
 
1289
1346
  regTool(
1290
1347
  "grid_scaffold",
1348
+ "Scaffold service folders",
1291
1349
  "Scaffold service folders declared in cloudgrid.yaml (idempotent). Wraps `grid scaffold`.",
1292
1350
  {
1293
1351
  cwd: z.string().optional().describe("Working directory. The CLI runs in this directory. Defaults to the MCP server's working directory."),
@@ -1298,6 +1356,7 @@ export function registerTools(server, ctx) {
1298
1356
 
1299
1357
  regTool(
1300
1358
  "grid_diagnose",
1359
+ "Run local diagnostics",
1301
1360
  "Run CloudGrid diagnostics on the local environment. Wraps `grid doctor`.",
1302
1361
  {},
1303
1362
  { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
@@ -1306,6 +1365,7 @@ export function registerTools(server, ctx) {
1306
1365
 
1307
1366
  regTool(
1308
1367
  "grid_get_url",
1368
+ "Get entity URL",
1309
1369
  "Return the public URL for an entity. Does not open a browser. Wraps `grid open --print`.",
1310
1370
  { name: z.string().optional().describe("Entity name. Omit for the entity linked to the current directory.") },
1311
1371
  { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
package/src/web.js CHANGED
@@ -25,6 +25,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
25
25
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
26
26
  import { registerTools, decodeJwt } from "./tools.js";
27
27
  import { mountOAuth, bearerChallenge } from "./oauth.js";
28
+ import { mountLanding } from "./landing.js";
28
29
  import { createSessionLogger } from "./session-logger.js";
29
30
  import { createSink } from "./log-sink.js";
30
31
  import { INSTRUCTIONS_WEB } from "./playbook.js";
@@ -98,6 +99,10 @@ app.use(express.urlencoded({ extended: false })); // OAuth token exchange is for
98
99
 
99
100
  app.get("/healthz", (_req, res) => res.json({ ok: true, edition: "web" }));
100
101
 
102
+ // The human-facing root page and favicon. Mounted on both postures; the auth line
103
+ // it renders is driven by the same REQUIRE_AUTH flag used below.
104
+ mountLanding(app, PUBLIC_BASE, { requireAuth: REQUIRE_AUTH });
105
+
101
106
  mountOAuth(app, PUBLIC_BASE, { requireAuth: REQUIRE_AUTH });
102
107
 
103
108
  // One transport per MCP session, keyed by the session id.
package/src/docs.js DELETED
@@ -1,147 +0,0 @@
1
- // CloudGrid MCP server — docs edition (public, read-only, no auth).
2
- //
3
- // A public documentation search server exposing search_cloudgrid_documentation
4
- // (and an optional cloudgrid_quickstart_guide) over MCP Streamable HTTP. The
5
- // same Express + StreamableHTTP pattern as the web edition (src/web.js),
6
- // stripped to the minimum: no auth, no OAuth, no session identity, no secrets.
7
- // Safe to expose anonymously.
8
- //
9
- // The corpus (src/corpus/*.md) is indexed at startup with BM25 keyword search.
10
- // The search backend is behind a clean interface so it can be swapped to
11
- // semantic/embedding search later without changing the tool contract.
12
- //
13
- // Run: PORT=8080 node src/docs.js Health: GET /healthz
14
-
15
- import { readFileSync } from "node:fs";
16
- import { randomUUID } from "node:crypto";
17
- import express from "express";
18
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
19
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
20
- import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
21
- import { z } from "zod";
22
- import { DocsSearch, loadCorpus } from "./docs-search.js";
23
-
24
- const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url)));
25
-
26
- const PORT = Number(process.env.PORT || 8080);
27
-
28
- // Build the search index once at startup.
29
- const search = loadCorpus(new DocsSearch());
30
-
31
- // Load the quickstart guide verbatim.
32
- const QUICKSTART = readFileSync(new URL("./corpus/cookbook.md", import.meta.url), "utf-8");
33
-
34
- // ── Tool helpers (same convention as tools.js) ────────────────────────────────
35
- function ok(text) {
36
- return { content: [{ type: "text", text }] };
37
- }
38
-
39
- function createDocsServer() {
40
- const server = new McpServer({ name: "cloudgrid-docs", version });
41
-
42
- // Naming cleanup (spec §6): the docs tools move to the grid_* family. Both
43
- // legacy names stay registered as deprecated aliases (same handler) so nothing
44
- // breaks mid-migration; aliases are removed in a later major.
45
- // search_cloudgrid_documentation → grid_search_docs
46
- // cloudgrid_quickstart_guide → grid_quickstart
47
- const dual = (name, alias, description, schema, handler) => {
48
- server.tool(name, description, schema, handler);
49
- server.tool(alias, `(deprecated: use ${name}) ${description}`, schema, handler);
50
- };
51
-
52
- dual(
53
- "grid_search_docs",
54
- "search_cloudgrid_documentation",
55
- "Search CloudGrid documentation — guides, tutorials, CLI reference, MCP setup, and skill descriptions. Returns the most relevant doc chunks with title, snippet, and source path.",
56
- {
57
- query: z
58
- .string()
59
- .describe(
60
- "The search query, e.g. 'deploy a database app' or 'add the MCP to Cursor'",
61
- ),
62
- },
63
- async ({ query }) => {
64
- const results = search.search(query, 5);
65
- if (results.length === 0) {
66
- return ok("No documentation matched your query. Try different keywords.");
67
- }
68
- const text = results
69
- .map(
70
- (r, i) => `### ${i + 1}. ${r.title}\n${r.snippet}\n_Source: ${r.source}_`,
71
- )
72
- .join("\n\n---\n\n");
73
- return ok(text);
74
- },
75
- );
76
-
77
- dual(
78
- "grid_quickstart",
79
- "cloudgrid_quickstart_guide",
80
- "Get the CloudGrid quickstart guide — the canonical build-and-ship loop from scaffold to deploy to feedback.",
81
- {},
82
- async () => ok(QUICKSTART),
83
- );
84
-
85
- return server;
86
- }
87
-
88
- // ── HTTP server (mirrors src/web.js, no auth) ─────────────────────────────────
89
- const app = express();
90
- app.use(express.json());
91
-
92
- app.get("/healthz", (_req, res) => res.json({ ok: true, edition: "docs" }));
93
-
94
- const transports = Object.create(null);
95
-
96
- app.post("/mcp", async (req, res) => {
97
- const sessionId = req.headers["mcp-session-id"];
98
- let transport = sessionId ? transports[sessionId] : undefined;
99
-
100
- if (transport) {
101
- await transport.handleRequest(req, res, req.body);
102
- return;
103
- }
104
-
105
- if (sessionId || !isInitializeRequest(req.body)) {
106
- res.status(400).json({
107
- jsonrpc: "2.0",
108
- error: { code: -32000, message: "No valid session. Send an initialize request first." },
109
- id: null,
110
- });
111
- return;
112
- }
113
-
114
- const newSessionId = randomUUID();
115
- transport = new StreamableHTTPServerTransport({
116
- sessionIdGenerator: () => newSessionId,
117
- onsessioninitialized: (sid) => {
118
- transports[sid] = transport;
119
- },
120
- });
121
- transport.onclose = () => {
122
- if (transport.sessionId) delete transports[transport.sessionId];
123
- };
124
- const server = createDocsServer();
125
- await server.connect(transport);
126
- await transport.handleRequest(req, res, req.body);
127
- });
128
-
129
- async function handleSessionRequest(req, res) {
130
- const sessionId = req.headers["mcp-session-id"];
131
- const transport = sessionId ? transports[sessionId] : undefined;
132
- if (!transport) {
133
- res.status(400).send("Invalid or missing session id");
134
- return;
135
- }
136
- await transport.handleRequest(req, res);
137
- }
138
-
139
- app.get("/mcp", handleSessionRequest);
140
- app.delete("/mcp", handleSessionRequest);
141
-
142
- app.listen(PORT, () => {
143
- // eslint-disable-next-line no-console
144
- console.error(
145
- `cloudgrid-docs edition listening on :${PORT} (POST /mcp, GET /healthz) — ${search.size} chunks indexed`,
146
- );
147
- });