@groundfloorcloud/mcp 0.1.15 → 0.2.0

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.
Files changed (3) hide show
  1. package/README.md +22 -8
  2. package/dist/index.js +523 -145
  3. package/package.json +4 -4
package/README.md CHANGED
@@ -63,6 +63,12 @@ which node
63
63
 
64
64
  The `command` must be your Node (from `which node`), not `npx` and not Cursor’s helper Node. Do not put a JWT in mcp.json.
65
65
 
66
+ ### Environment overrides
67
+
68
+ Use the prefixed names: `GROUNDFLOOR_API_URL`, `GROUNDFLOOR_WORKSPACE_ID`, `GROUNDFLOOR_TOKEN` (CI only; normally the `gf login` session is used).
69
+
70
+ **Deprecated:** the unprefixed `CP_API`, `WORKSPACE_ID` and `TOKEN` are still honoured as fallbacks, but they collide with unrelated variables in a shell. When one of them supplies a value the server writes a one-line notice to stderr (variable name only, never the value) and they will be removed in a later release.
71
+
66
72
  ## Tools
67
73
 
68
74
  | Tool | Role |
@@ -71,30 +77,38 @@ The `command` must be your Node (from `which node`), not `npx` and not Cursor’
71
77
  | `workspaces_list` | List; switch with `gf workspaces use` |
72
78
  | `coderunner_list` / `_get_status` / `_get_logs` / `_process_log` | Inspect workloads (Process Log does not need a job_id) |
73
79
  | `environments_list` | List workspace environments |
74
- | `coderunner_cancel_deployment` | Delete current (or given) deployment |
80
+ | `coderunner_cancel_deployment` | **Destructive.** Deletes one deployment (`deployment_id` is required); if it is the live one, the service goes down. Always the selected workspace (no per-call `workspace_id`) |
75
81
  | `apps_list` / `apps_get` | Inspect product Apps |
76
- | `secrets_list` / `files_list` / `domains_list` / `dataplane_status` | Inspect (no secrets values) |
77
- | `docs_list` / `docs_get` | Product docs (`coderunner`, `shell`, `design-standard`, `css-and-styling`, `databases` are embedded; `access` and `m2m` are live pages) |
78
- | `suggest_cli` | Command to run: login, deploy, init_app, publish_app, m2m_token, … |
82
+ | `secrets_list` / `files_list` / `dataplane_status` | Inspect (no secrets values) |
83
+ | `domains_list` | Custom domains of an App's Coderunner (`app_id` or `coderunner_id`). Domains exist on Apps, not standalone Coderunners |
84
+ | `quota_get` / `billing_summary` | Plan quota and billing for the workspace's **account** (account id is not the workspace id; billing needs Owner or Billing admin) |
85
+ | `docs_list` / `docs_get` | Product docs (`coderunner`, `coderunner-runtime`, `shell`, `design-standard`, `css-and-styling`, `databases`, `vault`, `feature-flags`, `authentication` are embedded; `access` and `m2m` are live pages, with an `offset` parameter to read past the 12,000-character page limit) |
86
+ | `suggest_cli` | Command to run: login, deploy, create_app, init_app, publish_app, m2m_token, … |
79
87
 
80
88
  Never create `app_kind=coderunner`. Deploy with `gf deploy`.
81
89
 
82
- **Shell apps:** `docs_get` id `shell`. Always `gf apps init --slug <portal-slug>`,
83
- then `npm run release` and `gf apps publish --path release.zip`. Do not invent
90
+ **Shell apps:** `docs_get` id `shell`. Always `gf apps create --name … --slug <portal-slug> --kind shell_federated`,
91
+ then `gf apps init --slug <portal-slug>`, `npm install`, `npm run release` and `gf apps publish --path release.zip`. Do not invent
84
92
  `App.tsx` / `vite.config`, zip `dist/`, or upload only `remoteEntry.js`.
85
93
  Own CSS framework: `docs_get` id `css-and-styling` (`FederatedStyleRoot`).
86
94
 
95
+ **App end users:** `docs_get` id `authentication`. They sign in only to the app via the workspace App Authentication realm; they have no Groundfloor console account or workspace membership.
96
+
97
+ **Tool annotations:** every tool except `coderunner_cancel_deployment` is marked `readOnlyHint`; the cancel tool is marked `destructiveHint`.
98
+
87
99
  **Databases:** `docs_get` id `databases`. Dataplane is not a database. Shared
88
100
  PostgreSQL is ready through Dataplane; a dedicated PostgreSQL is what you can
89
101
  deploy today. No other engines are offered.
90
102
 
91
- **Coderunner how-to** is embedded: `docs_get` id `coderunner` (also
92
- `coderunner-overview`). That page covers:
103
+ **Coderunner how-to** is embedded: `docs_get` id `coderunner`. The former
104
+ `coderunner-overview` id is no longer listed by `docs_list`, but `docs_get`
105
+ still accepts it as an alias and returns the same page. That page covers:
93
106
 
94
107
  - Types: `function`, `job`, `schedule`, `service` (**also called Deployment**)
95
108
  - App authentication is optional (end-user login); not required to create a coderunner
96
109
  - **Dockerfile is required** for `service` / Deployment
97
110
  - Runtime contract: Node `scripts.start`, listen on `PORT`/8080, bind `0.0.0.0` (`docs_get` id `coderunner-runtime`)
111
+ - Handler signature `(context, payload)` and the HTTP return shape, plus verified limits
98
112
  - Local development against live vault/files/secrets (`gf env`; as the workload use `gf m2m token`, not `gf token`)
99
113
 
100
114
  `workspace_context` includes a `coderunner_howto` summary so agents see this
package/dist/index.js CHANGED
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+
6
+ // src/server.ts
7
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
8
  import { z } from "zod";
7
9
 
8
10
  // ../cli/src/auth/oauth.ts
@@ -95,7 +97,7 @@ var EXPIRY_SKEW_MS = 3e4;
95
97
  var NotLoggedInError = class extends Error {
96
98
  constructor() {
97
99
  super(
98
- "Not logged in. Set GROUNDFLOOR_TOKEN, or run `gf login` first."
100
+ "Not logged in. Set GROUNDFLOOR_TOKEN to a Groundfloor access token (e.g. from `gf m2m token`), or run `gf login` first."
99
101
  );
100
102
  this.name = "NotLoggedInError";
101
103
  }
@@ -129,11 +131,26 @@ function sessionFromTokens(tokens, ctx) {
129
131
  function isFresh(session) {
130
132
  return Date.now() < session.expiresAt - EXPIRY_SKEW_MS;
131
133
  }
134
+ function isExpiredJwt(token) {
135
+ const exp = decodeJwt(token)?.exp;
136
+ return typeof exp === "number" && Date.now() >= exp * 1e3 - EXPIRY_SKEW_MS;
137
+ }
132
138
  async function getValidAccessToken() {
133
139
  if (accessTokenProvider) {
134
140
  return accessTokenProvider();
135
141
  }
142
+ const envToken = process.env.GROUNDFLOOR_TOKEN?.trim();
143
+ let envTokenExpired = false;
144
+ if (envToken) {
145
+ if (!isExpiredJwt(envToken)) return envToken;
146
+ envTokenExpired = true;
147
+ }
136
148
  const session = await readAuth();
149
+ if (envTokenExpired && (!session || !session.accessToken)) {
150
+ throw new Error(
151
+ "GROUNDFLOOR_TOKEN has expired and there is no saved `gf login` session. Mint a fresh token (e.g. `gf m2m token`) or run `gf login`."
152
+ );
153
+ }
137
154
  if (!session || !session.accessToken) {
138
155
  throw new NotLoggedInError();
139
156
  }
@@ -305,17 +322,6 @@ function getProcessLogs(apiUrl, workspaceId, query = {}) {
305
322
  `/v1/workspaces/${encodeURIComponent(workspaceId)}/logs${qs ? `?${qs}` : ""}`
306
323
  );
307
324
  }
308
- function domainsBase(workspaceId, coderunnerId) {
309
- return `/v1/workspaces/${encodeURIComponent(
310
- workspaceId
311
- )}/coderunners/${encodeURIComponent(coderunnerId)}/domains`;
312
- }
313
- function listDomains(apiUrl, workspaceId, coderunnerId) {
314
- return cpGet(
315
- apiUrl,
316
- domainsBase(workspaceId, coderunnerId)
317
- );
318
- }
319
325
  function wsPath(workspaceId, suffix) {
320
326
  return `/v1/workspaces/${encodeURIComponent(workspaceId)}${suffix}`;
321
327
  }
@@ -411,13 +417,32 @@ function trimSlash(url) {
411
417
  }
412
418
  var MISSING_TOKEN = "Not signed in. Run `gf login` (or `gf login --stage`) so MCP can use ~/.groundfloor. Do not paste JWTs into Cursor MCP config.";
413
419
  var MISSING_WORKSPACE = "No workspace selected. Run `gf workspaces` then `gf workspaces use <uuid>`, or pass workspace_id on this tool.";
420
+ var reportedSources = /* @__PURE__ */ new Set();
421
+ function noteDeprecatedEnv(setting, used, preferred) {
422
+ const key = `${setting}:${used}`;
423
+ if (reportedSources.has(key)) return;
424
+ reportedSources.add(key);
425
+ console.error(
426
+ `groundfloor MCP: ${setting} taken from deprecated env var ${used}; set ${preferred} instead (unprefixed names will be removed).`
427
+ );
428
+ }
429
+ function pick(setting, preferred, deprecated) {
430
+ const a = process.env[preferred];
431
+ if (a) return a;
432
+ const b = process.env[deprecated];
433
+ if (b) {
434
+ noteDeprecatedEnv(setting, deprecated, preferred);
435
+ return b;
436
+ }
437
+ return "";
438
+ }
414
439
  async function resolveMcpConfig(overrides) {
415
440
  const file = await resolveConfig();
416
441
  const apiUrl = trimSlash(
417
- process.env.GROUNDFLOOR_API_URL || process.env.CP_API || file.apiUrl || CELLS.production.apiUrl
442
+ pick("API URL", "GROUNDFLOOR_API_URL", "CP_API") || file.apiUrl || CELLS.production.apiUrl
418
443
  );
419
- const workspaceId = overrides?.workspaceId || process.env.GROUNDFLOOR_WORKSPACE_ID || process.env.WORKSPACE_ID || file.workspaceId || "";
420
- const envToken = (process.env.GROUNDFLOOR_TOKEN || process.env.TOKEN || "").trim();
444
+ const workspaceId = overrides?.workspaceId || pick("workspace id", "GROUNDFLOOR_WORKSPACE_ID", "WORKSPACE_ID") || file.workspaceId || "";
445
+ const envToken = pick("access token", "GROUNDFLOOR_TOKEN", "TOKEN").trim();
421
446
  if (envToken) {
422
447
  setAccessTokenProvider(async () => envToken);
423
448
  } else {
@@ -444,19 +469,171 @@ async function resolveMcpConfig(overrides) {
444
469
  return { apiUrl, workspaceId, token };
445
470
  }
446
471
 
472
+ // src/cp-get.ts
473
+ async function cpGetJson(apiUrl, token, path2) {
474
+ const res = await fetch(`${apiUrl}${path2}`, {
475
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/json" }
476
+ });
477
+ if (!res.ok) {
478
+ let detail = res.statusText;
479
+ try {
480
+ const body = await res.json();
481
+ if (body?.detail !== void 0) {
482
+ detail = formatCpErrorDetail(body.detail, res.statusText);
483
+ }
484
+ } catch {
485
+ }
486
+ throw new CpError(res.status, `${res.status} ${detail}`);
487
+ }
488
+ return await res.json();
489
+ }
490
+
491
+ // src/app-authentication-guide.ts
492
+ var APP_AUTHENTICATION_GUIDE = `
493
+ # App authentication (end users of a customer's app)
494
+
495
+ ## The rule
496
+
497
+ People who use an app built on Groundfloor sign in **only to that app**,
498
+ through the workspace's **App authentication** realm. They do **not** get a
499
+ Groundfloor console (Client Portal) account and are **not** workspace members.
500
+ Portal team roles (Owner, Admin, Writer) are a separate system and never apply
501
+ to app end users. Do not build, suggest or provision Portal logins for them.
502
+
503
+ App authentication is **optional**. It is not needed to create Apps or deploy
504
+ Coderunners, and it never changes how the team signs in to the Portal.
505
+
506
+ ## Sign-in modes (Portal: Administer \u2192 App authentication, Admin or Owner)
507
+
508
+ | Mode | End users sign in with | Use when |
509
+ |---|---|---|
510
+ | None | Nothing | Apps are public or handle access themselves |
511
+ | Groundfloor | A hosted sign-in page; users, roles, SSO and branding managed in the Portal | You want sign-in without running an identity provider |
512
+ | External | Your own OpenID Connect provider (Okta, Entra ID, Auth0) | Users already have accounts there |
513
+
514
+ For External, register \`https://<your-shell-site>/*\` as a redirect address with
515
+ the provider.
516
+
517
+ ## Users and roles (Groundfloor mode)
518
+
519
+ - **Invite user**: they set their own password (email needs Branding set up).
520
+ - **Add user**: you set a password now; no email is sent.
521
+ - Each user has a role: built-in \`viewer\`, \`user\`, \`admin\`, or a custom role
522
+ created on the **Roles** tab. Roles here are unrelated to Portal team roles.
523
+ - The role decides what the user reaches once granted on **Site access**
524
+ (Data Vault collections, workspace-wide Files, App launch). The built-in
525
+ \`admin\` role reaches everything. A signed-in user with no recognised role
526
+ gets 403 on role-gated endpoints (Vault, feature flags).
527
+ - SSO (OIDC or SAML) can be connected per workspace; role names in the
528
+ provider's tokens must match the roles granted on Site access.
529
+
530
+ ## Multi-tenant (customer companies inside one workspace)
531
+
532
+ Turn on **Multi-tenant** in Settings to get a Tenants tab: one tenant per
533
+ customer company, invite-only onboarding, per-tenant SSO. The user's token
534
+ carries the tenant in the \`organization\` claim; the app must keep each tenant's
535
+ data apart using that claim. Site access, data, files and M2M tokens stay shared
536
+ across the workspace.
537
+
538
+ ## Turning it off
539
+
540
+ Switching to None or External keeps users, roles and settings (switch back and
541
+ they return). To remove them, turn sign-in off first, then **Delete Groundfloor
542
+ login**. Access grants and M2M tokens are kept.
543
+
544
+ ## In a Shell app
545
+
546
+ The signed-in user's JWT comes from \`window.__GF_GET_ACCESS_TOKEN__()\`. See
547
+ \`docs_get\` id \`shell\`. Unattended workloads use M2M instead (\`docs_get\` id
548
+ \`m2m\`), not an end-user login.
549
+ `.trim();
550
+
447
551
  // src/coderunner-runtime.ts
448
552
  var CODERUNNER_RUNTIME_BODY = `
449
- When a ZIP has **no Dockerfile**, Coderunner injects a language bootstrap image.
450
- Your process must match that image \u2014 \`gf deploy\` checks the Node bits before
451
- upload. A custom Dockerfile (required for \`service\` / Deployment) replaces
452
- the bootstrap; the listen/bind rules still apply.
553
+ The contract depends on the workload type.
554
+
555
+ | Type | Entrypoint (no Dockerfile) | Needs |
556
+ |---|---|---|
557
+ | \`function\`, \`job\`, \`schedule\` | Generated \`/app/main.js\` that does \`require('./handler')\` | Root \`handler.js\` exporting \`handler\` |
558
+ | \`service\` (Deployment) | Your Dockerfile \`CMD\` (Dockerfile required) | \`npm start\` / \`PORT\` / bind \`0.0.0.0\` if CMD is \`npm start\` |
453
559
 
454
- ## Node (no Dockerfile)
560
+ \`gf deploy\` checks the Node bits before upload.
455
561
 
456
- Bootstrap is effectively:
562
+ ## Node: function, job, schedule
563
+
564
+ \`npm start\` is **not** run for these types. The platform's generated
565
+ \`/app/main.js\` loads your code with \`require('./handler')\`, so:
566
+
567
+ 1. Put a \`handler.js\` at the ZIP root (not nested) that exports \`handler\`.
568
+ \`gf deploy\` also accepts a \`package.json\` \`"main"\` that points at an
569
+ existing file, but \`./handler\` is the documented path; prefer \`handler.js\`.
570
+ 2. \`scripts.start\` is not required, and a server that listens on a port is
571
+ not needed.
572
+ 3. Missing \`handler.js\` \u2192 the new revision crash-loops
573
+ (\`Cannot find module './handler'\`, \`MODULE_NOT_FOUND\`) and the deployment
574
+ ends **Failed (status 4)**. Check \`gf coderunner logs -c <id>\`.
575
+
576
+ ### The handler signature
577
+
578
+ Arguments are **\`(context, payload)\`** in that order. Export a function named
579
+ \`handler\` (CommonJS \`module.exports = { handler }\`; an ES module
580
+ \`export async function handler(context, payload)\` also works):
581
+
582
+ \`\`\`js
583
+ const handler = async (context, payload) => {
584
+ // your code
585
+ };
586
+
587
+ module.exports = { handler };
588
+ \`\`\`
589
+
590
+ - \`payload\` is the JSON input: the request body for a function, the run's input
591
+ for a job or schedule. Guard for a missing body (\`payload?.name\`).
592
+ - \`context\` is reserved for runtime information. Do not rely on its contents.
593
+
594
+ ### Function return shape (HTTP)
595
+
596
+ Whatever the handler returns becomes the response; you may return anything.
597
+ The documented convention, used by every example, is:
598
+
599
+ \`\`\`js
600
+ const handler = async (context, payload) => {
601
+ if (!payload?.name) {
602
+ return {
603
+ statusCode: 400,
604
+ body: { error: "name is required" },
605
+ headers: { "content-type": "application/json" },
606
+ };
607
+ }
608
+ return {
609
+ statusCode: 200,
610
+ body: { greeting: \`Hello, \${payload.name}\` },
611
+ headers: { "content-type": "application/json" },
612
+ };
613
+ };
614
+
615
+ module.exports = { handler };
616
+ \`\`\`
617
+
618
+ Use \`200\`/\`201\` for success, \`400\` for bad input, \`401\`/\`403\` for auth,
619
+ \`404\` for not found, \`500\` for unexpected errors (no internals in the body).
620
+ Callers POST JSON to the Deployment URL with a bearer token, or use
621
+ \`gf coderunner run -c <slug> --payload '<json>'\`.
622
+
623
+ ### Jobs and schedules: return value and failure
624
+
625
+ A job or schedule needs no return value. If an error **escapes** the handler the
626
+ run is marked **Failed**; if you catch it, the run succeeds and handling it is
627
+ up to you.
628
+
629
+ ## Node: service (or any Dockerfile that runs \`npm start\`)
630
+
631
+ A \`service\` **requires** a root Dockerfile; you own \`CMD\`. If it is
632
+ \`npm start\` (for any type), \`package.json\` needs \`"scripts": { "start": ... }\`,
633
+ and the process must follow the HTTP contract:
457
634
 
458
635
  \`\`\`dockerfile
459
- FROM node:18-alpine
636
+ FROM node:22-alpine
460
637
  WORKDIR /app
461
638
  COPY . .
462
639
  EXPOSE 8080
@@ -464,8 +641,7 @@ ENV PORT=8080
464
641
  CMD ["npm", "start"]
465
642
  \`\`\`
466
643
 
467
- 1. \`package.json\` **must** define \`"scripts": { "start": "node <entrypoint>" }\`.
468
- Missing \`start\` \u2192 container crash loop, probe \`127.0.0.1:8080\` connection
644
+ 1. Missing \`start\` \u2192 container crash loop, probe \`127.0.0.1:8080\` connection
469
645
  refused, deployment **Failed (status 4)** after several minutes.
470
646
  2. Listen on \`Number(process.env.PORT || 8080)\`.
471
647
  3. Bind **\`0.0.0.0\`**, not \`127.0.0.1\`. Cluster traffic is not loopback.
@@ -477,47 +653,34 @@ server.listen(port, "0.0.0.0", () => {
477
653
  });
478
654
  \`\`\`
479
655
 
480
- Offer GET \`/\` or \`/health\` for the probe. Sample: \`samples/hello-fn/\`.
481
-
482
- ## Known issue: bootstrap has been reported to skip \`npm start\`
483
-
484
- Reported 2026-09-22 against a \`function\` deploy with no Dockerfile, **unresolved as of this
485
- writing**: instead of running \`npm start\` per the contract above, the platform's own
486
- \`/app/main.js\` ran \`require('./handler')\` \u2014 a file/module contract this document does not
487
- describe \u2014 even though \`scripts.start\` and the entrypoint were present and correct. Symptom in
488
- \`gf coderunner logs -c <id>\`: \`Cannot find module './handler'\` (\`MODULE_NOT_FOUND\`), repeated
489
- probe \`127.0.0.1:8080\` connection refused, deployment **Failed (status 4)**. Whether \`job\` and
490
- \`schedule\` share this bootstrap path and are equally affected has not been confirmed either way \u2014
491
- treat the workaround below as precautionary for those types too if you hit the same signature.
492
-
493
- **Workaround:** add your own root \`Dockerfile\` (e.g. \`CMD ["npm", "start"]\`) and keep your
494
- existing \`workload_type\` \u2014 a Dockerfile bypasses the injected bootstrap entirely regardless of
495
- type (see "Node (with Dockerfile)" below), so this sidesteps the bug without changing how the
496
- workload is invoked.
497
-
498
- ## Node (with Dockerfile)
499
-
500
- You own \`CMD\`. If it is \`npm start\`, you still need \`scripts.start\`.
501
- Listen on 8080 / \`PORT\` and bind \`0.0.0.0\`. \`service\` (Deployment)
502
- **requires** a root Dockerfile \u2014 bootstrap is not injected on that path.
656
+ Offer GET \`/\` or \`/health\` for the probe. A Dockerfile whose \`CMD\` is not
657
+ \`npm start\` replaces the generated bootstrap entirely, for any type.
503
658
 
504
659
  ## Python
505
660
 
506
- Bind \`0.0.0.0\` and \`PORT\` (default 8080), for example
507
- \`uvicorn main:app --host 0.0.0.0 --port \${PORT:-8080}\`.
661
+ Function, job and schedule: \`handler.py\` at the ZIP root (with
662
+ \`requirements.txt\`) defining \`def handler(context, payload):\`, same argument
663
+ order, same return convention (\`statusCode\`, \`body\`,
664
+ \`headers\`; an exception escaping the handler fails a job run). \`print\` goes
665
+ to the Process Log.
666
+
667
+ A Dockerfile-based service binds \`0.0.0.0\` and \`PORT\` (default 8080), for
668
+ example \`uvicorn main:app --host 0.0.0.0 --port \${PORT:-8080}\`.
508
669
 
509
670
  ## Jobs and schedules
510
671
 
511
- Same package/start rules. The process may exit 0 when finished; it does not
512
- need a public URL. Schedules have **no** Deployment URL. Set cron with
672
+ Node: \`handler.js\` exporting \`handler\`, same as functions (no \`npm start\`).
673
+ They do not need a public URL. Schedules have **no** Deployment URL. Set cron with
513
674
  \`gf deploy --workload-type schedule --schedule "<cron>"\` or
514
675
  \`groundfloor.json\` \`"schedule"\`.
515
676
 
516
677
  ## What \`gf deploy\` checks
517
678
 
518
679
  - \`service\` without a root Dockerfile \u2192 fail immediately.
519
- - Node without a Dockerfile (or a Dockerfile that runs \`npm start\`) and no
520
- \`scripts.start\` \u2192 fail immediately with this contract.
680
+ - Node \`function\`/\`job\`/\`schedule\` without a Dockerfile and without a root
681
+ \`handler.js\` (or a \`package.json\` \`"main"\` that resolves to a file) \u2192 fail
682
+ immediately. \`scripts.start\` is not required.
683
+ - Node with a Dockerfile that runs \`npm start\` and no \`scripts.start\` \u2192 fail immediately.
521
684
  - Listen/bind are not parsed from source; match the sample or the probe fails
522
685
  at deploy time. Check Process Log / \`gf coderunner logs -c <id>\`.
523
686
  `.trim();
@@ -571,24 +734,25 @@ Pick **one** type per coderunner. Changing type later means a new coderunner.
571
734
 
572
735
  ### Function
573
736
 
574
- - ZIP at archive root (\`package.json\` / \`index.js\` or \`main.py\` not nested).
575
- - Listen on \`process.env.PORT || 8080\`, bind **\`0.0.0.0\`** (health GET \`/\` or \`/health\`).
576
- - **Node:** \`package.json\` **must** include \`"scripts": { "start": "node index.js" }\`.
577
- The runtime image is \`CMD ["npm", "start"]\`. Missing \`start\` \u2192 crash loop,
578
- probe \`127.0.0.1:8080\` connection refused, deployment **Failed (status 4)**.
737
+ - ZIP at archive root (\`handler.js\` / \`package.json\` or \`main.py\` not nested).
738
+ - **Node:** a root \`handler.js\` exporting \`handler\`. The platform runs a generated
739
+ \`/app/main.js\` that does \`require('./handler')\`; \`npm start\` is **not** run and
740
+ \`scripts.start\` is not required. Missing \`handler.js\` \u2192 crash loop
741
+ (\`Cannot find module './handler'\`), deployment **Failed (status 4)**.
742
+ \`gf deploy\` fails fast on this. Handler arguments are \`(context, payload)\`; the
743
+ signature and the HTTP return shape (\`{ statusCode, body, headers }\`) are in the
744
+ Runtime contract section above.
745
+ - Node with your own Dockerfile (\`CMD ["npm","start"]\`) instead: listen on
746
+ \`process.env.PORT || 8080\`, bind **\`0.0.0.0\`** (health GET \`/\` or \`/health\`), and
747
+ define \`scripts.start\`.
579
748
  - Python: process must bind 0.0.0.0:\${PORT:-8080}.
580
749
  - After deploy: \`gf coderunner run -c <slug> --payload '{"hello":"world"}'\`.
581
- - \u26A0\uFE0F See "Known issue" in \`docs_get\` id \`coderunner-runtime\` if \`gf coderunner logs\` shows
582
- \`Cannot find module './handler'\` despite a correct \`scripts.start\` \u2014 add a Dockerfile as the
583
- workaround, no type change needed.
584
750
 
585
751
  ### Job
586
752
 
587
- - Same package rules as function. No need to serve forever; exit 0 on success.
753
+ - Node: same as function (root \`handler.js\` exporting \`handler\`; no \`npm start\`).
754
+ No need to serve forever; exit 0 on success.
588
755
  - Deploy, then start a run (\`gf coderunner run\`). Check Process Log.
589
- - \u26A0\uFE0F Not confirmed whether the no-Dockerfile bootstrap bug in \`docs_get\` id \`coderunner-runtime\`
590
- ("Known issue") also hits \`job\` \u2014 if a run never starts and logs show the same
591
- \`MODULE_NOT_FOUND: './handler'\`, apply the same Dockerfile workaround.
592
756
 
593
757
  ### Schedule
594
758
 
@@ -598,8 +762,7 @@ Pick **one** type per coderunner. Changing type later means a new coderunner.
598
762
  - GET/list/status expose top-level \`workload_type\` and \`schedule\`.
599
763
  \`gf coderunner status -c <slug>\` prints type, schedule, last_job.
600
764
  - No public invoke URL. Debug with \`gf coderunner logs\`, not curl.
601
- - \u26A0\uFE0F Same caveat as Job: unconfirmed whether the bootstrap "Known issue" affects scheduled runs;
602
- same Dockerfile workaround if you see the identical crash signature.
765
+ - Node: same handler contract as function (root \`handler.js\`).
603
766
 
604
767
  ### Service (Deployment)
605
768
 
@@ -610,7 +773,7 @@ Pick **one** type per coderunner. Changing type later means a new coderunner.
610
773
  - Node example:
611
774
 
612
775
  \`\`\`dockerfile
613
- FROM node:18-alpine
776
+ FROM node:22-alpine
614
777
  WORKDIR /app
615
778
  COPY package*.json ./
616
779
  RUN npm ci --omit=dev
@@ -620,7 +783,7 @@ ENV PORT=8080
620
783
  CMD ["npm", "start"]
621
784
  \`\`\`
622
785
 
623
- - Still require Node \`"start"\` if CMD is \`npm start\`.
786
+ - Services follow the \`npm start\` / \`PORT\` contract: Node needs \`scripts.start\` if CMD is \`npm start\`.
624
787
  - Prefer curling the Deployment URL (\`/health\`) over \`\u2026/run\`.
625
788
 
626
789
  ## Latest deploy rules (all types)
@@ -651,7 +814,7 @@ gf dataplane status # provision if needed: gf dataplane provision
651
814
  gf secrets ls
652
815
  \`\`\`
653
816
 
654
- Local HTTP (Node function/service):
817
+ Local HTTP (Node service, or a Dockerfile-based function):
655
818
 
656
819
  \`\`\`bash
657
820
  export PORT=8080
@@ -733,9 +896,28 @@ do the fetch yourself; keep using \`--secret\` injection for SDKs that only
733
896
  read a literal \`process.env\` value. Either way, cache the revealed value
734
897
  in your process rather than calling \`reveal\` on every request.
735
898
 
899
+ ## Limits an agent will hit
900
+
901
+ Verified against the control plane source; anything not listed here is not a
902
+ documented limit.
903
+
904
+ - **M2M token endpoint**: 20 requests per minute (per client and per source).
905
+ Cache the token until near expiry; do not mint per request.
906
+ - **Vault collection list/query**: \`limit\` 1-500 (default 50).
907
+ - **Feature flag string values**: at most 10,000 characters.
908
+ - **\`docs_get\` live pages**: truncated at 12,000 characters; use \`offset\` to
909
+ read the rest.
910
+
911
+ Plan quota and usage are account-scoped, not workspace-scoped. The **account
912
+ id is not the workspace id**: read \`account_id\` from \`workspaces_list\`. If a
913
+ deploy is refused or you need to know what is in use, call \`quota_get\`.
914
+ \`billing_summary\` shows spend, and only Owners and Billing admins can read it
915
+ (others get 403).
916
+
736
917
  ## Agent actions
737
918
 
738
- Inspect: \`workspace_context\`, \`docs_get\` id \`coderunner\` or \`coderunner-runtime\`, \`suggest_cli\`.
919
+
920
+ Inspect: \`workspace_context\`, \`quota_get\`, \`docs_get\` id \`coderunner\` or \`coderunner-runtime\`, \`suggest_cli\`.
739
921
  Act: \`gf deploy\`, \`gf coderunner run|status|logs\`, \`gf m2m token\`, \`gf identities ls|mint\`, \`gf api\`. MCP does not deploy.
740
922
  `.trim();
741
923
 
@@ -908,7 +1090,9 @@ One endpoint for every runtime:
908
1090
 
909
1091
  Weak \`ETag\` + \`Cache-Control: private, no-cache\`. Send \`If-None-Match\` \u2192 304.
910
1092
  Poll no faster than every 30s (the kit uses 60s). No push channel yet.
911
- Flag values are not secrets \u2014 every signed-in end user can read them.
1093
+ Flag values are not secrets, but the endpoint is role-gated: only users with a
1094
+ recognised site role can read them (see Permission below); a signed-in user
1095
+ with no role gets 403.
912
1096
 
913
1097
  ### Shell federated app (browser)
914
1098
 
@@ -1023,8 +1207,9 @@ routes (omit it or use \`false\` otherwise \u2014 never \`null\`). \`visibility:
1023
1207
 
1024
1208
  ## suggest_cli
1025
1209
 
1210
+ - create_app \u2192 \`gf apps create --name "<App Name>" --slug <portal-slug> --kind shell_federated\` (register the App first; publish fails with App not found otherwise)
1026
1211
  - init_app \u2192 \`gf apps init --slug <portal-slug>\`
1027
- - publish_app \u2192 init + npm install + npm run release + gf apps publish --path release.zip
1212
+ - publish_app \u2192 create + init + npm install + npm run release + gf apps publish --path release.zip
1028
1213
  `;
1029
1214
 
1030
1215
  // src/vault-guide.ts
@@ -1071,14 +1256,63 @@ Example:
1071
1256
  Or use \`gf api -X POST /v1/workspaces/{workspace_id}/vault/collections --body '{...}'\`.
1072
1257
 
1073
1258
  \`GET \u2026/vault/collections\` lists schema-registered collections. Implicit
1074
- tables created by the first \`POST \u2026/vault/{collection}\` appear when you pass
1075
- \`include_implicit=true\` (or after a schema create).
1259
+ tables created by the first \`POST \u2026/vault/collections/{collection}\` appear when
1260
+ you pass \`include_implicit=true\` (or after a schema create).
1261
+
1262
+ ## Records: routes
1263
+
1264
+ All under \`/v1/workspaces/{workspace_id}/vault\`:
1265
+
1266
+ | Method and path | Purpose |
1267
+ |---|---|
1268
+ | \`POST /collections/{collection}/query\` | Filtered, paginated read |
1269
+ | \`GET /collections/{collection}/count\` | Count; optional \`filter\` query param (JSON object) |
1270
+ | \`POST /collections/{collection}\` | Create one record, body \`{"record": {...}}\`, 201, returns \`{"document": {...}}\` |
1271
+ | \`PUT /collections/{collection}/{document_id}\` | Update one record, body \`{"record": {...}}\`, returns \`{"document": {...}}\` |
1272
+ | \`DELETE /collections/{collection}/{document_id}\` | Delete one record, 204 |
1273
+ | \`GET /collections/{collection}/schema\` | Schema |
1274
+ | \`GET\`/\`PUT /collections/{collection}/acl\` | Collection ACL |
1076
1275
 
1077
- ## Query totals
1276
+ There is **no** \`/vault/{collection}\` route; always include \`/collections/\`.
1277
+ \`?connection=\` picks the database (default \`postgresql\`).
1078
1278
 
1079
- \`POST \u2026/vault/{collection}/query\` \`total\` is the **unpaged** match count.
1080
- \`limit\` only sizes \`records\`. Do not treat \`total === records.length\` as
1081
- the full table size.
1279
+ ## Query: request and response
1280
+
1281
+ Request body for \`POST \u2026/collections/{collection}/query\` (all optional):
1282
+ \`filter\`, \`sort\` (e.g. \`["-created_at", "name"]\`), \`projection\`,
1283
+ \`limit\` (1-500, default 50), \`offset\` (default 0), \`connection\`.
1284
+
1285
+ Response:
1286
+
1287
+ \`\`\`json
1288
+ { "items": [ {...} ], "total": 123, "has_more": true, "next_offset": 50 }
1289
+ \`\`\`
1290
+
1291
+ - The array key is **\`items\`** (not \`records\`).
1292
+ - \`total\` is the match count from Dataplane (see below for \`$text\`) and is
1293
+ **null** when upstream gave no count; it is never filled in from the page
1294
+ length. Do not treat \`total === items.length\` as the full table size.
1295
+ - Page with \`next_offset\` while \`has_more\` is true; \`next_offset\` is null
1296
+ on the last page.
1297
+
1298
+ ## Substring search (\`$text\` / \`$search\`)
1299
+
1300
+ \`{ "filter": { "$text": "alice" } }\` is a case-insensitive substring match
1301
+ over every field value. Control Plane runs it itself by scanning the caller's
1302
+ ACL-filtered view, at most \`VAULT_TEXT_SEARCH_MAX_SCAN_ROWS\` rows per request
1303
+ (default 10 000).
1304
+
1305
+ - Plain query: for \`$text\`, \`total\` is computed by Control Plane (not
1306
+ Dataplane). It is exact when the collection fits under the cap; past the cap
1307
+ it is **null** and the results cover only the scanned rows.
1308
+ - Large collections: send \`"chunked_scan": true\`, then
1309
+ \`"scan_cursor": <scan.next_cursor>\` with the same filter/sort until
1310
+ \`scan.complete\` is true. Each response has a \`scan\` block
1311
+ (\`complete\`, \`next_cursor\`, \`scanned_rows\`, \`total_rows\`); the exact
1312
+ match total is the sum of \`items\` across chunks. The cursor is not a
1313
+ snapshot \u2014 concurrent writes can skip or repeat rows.
1314
+ - \`GET \u2026/count\` with \`$text\` uses the plain bounded scan and returns 400
1315
+ past the cap; use the chunked query instead.
1082
1316
 
1083
1317
  ## Filters
1084
1318
 
@@ -1111,6 +1345,17 @@ schedules.
1111
1345
 
1112
1346
  Do not \`--env SENDGRID_SECRET=\u2026\`. That is **422**.
1113
1347
  \`gf secrets set SENDGRID_SECRET\` then \`gf deploy --secret SENDGRID_SECRET\`.
1348
+
1349
+ **Caveat:** \`--secret\` injection needs the platform-wide Control Plane setting
1350
+ \`CODERUNNER_INJECT_ENV_SECRETS\`, which is **off by default** and not
1351
+ configurable per workspace. When it is off the secret is not injected. Fallback:
1352
+ give the Coderunner's M2M client the \`secrets:read\` scope (Portal
1353
+ **Administer \u2192 M2M tokens**) and read the value at runtime with
1354
+ \`GET /v1/workspaces/{workspace_id}/secrets/{key}\` using the M2M bearer token.
1355
+ It is read-only, live (no redeploy on rotation) and workspace-wide. Details in
1356
+ \`docs_get\` id \`coderunner\`. The deployed value of the setting cannot be
1357
+ checked from the repo; if \`--secret\` fails or the variable is empty, use the
1358
+ fallback rather than plaintext \`--env\`.
1114
1359
  `.trim();
1115
1360
 
1116
1361
  // src/version.ts
@@ -1144,31 +1389,25 @@ var DOC_CATALOG = [
1144
1389
  id: "coderunner",
1145
1390
  title: "Coderunner",
1146
1391
  path: "/docs/deploy",
1147
- summary: "Workload types (function, job, schedule, service/Deployment), runtime contract (scripts.start, 0.0.0.0:8080), Dockerfile for services, Coderunner identities for Vault, App authentication optional, local live data, gf deploy.",
1392
+ summary: "Workload types (function, job, schedule, service/Deployment), runtime contract (handler.js for function/job/schedule; npm start, PORT 8080 for service), Dockerfile for services, Coderunner identities for Vault, App authentication optional, local live data, gf deploy.",
1148
1393
  embedded: CODERUNNER_GUIDE,
1149
- lastUpdated: "2026-09-25"
1394
+ lastUpdated: "2026-10-08"
1150
1395
  },
1151
1396
  {
1152
1397
  id: "coderunner-runtime",
1153
1398
  title: "Coderunner runtime contract",
1154
1399
  path: "/docs/deploy",
1155
- summary: "What the platform expects your code to do: Node bootstrap (npm start, PORT/8080, bind 0.0.0.0), Python bind, service Dockerfile.",
1400
+ summary: "What the platform expects your code to do: Node handler.js for function/job/schedule, npm start + PORT/8080 + bind 0.0.0.0 for service/Dockerfile, Python bind.",
1156
1401
  embedded: CODERUNNER_RUNTIME_CONTRACT,
1157
- lastUpdated: "2026-09-23"
1158
- },
1159
- {
1160
- id: "coderunner-overview",
1161
- title: "Coderunner overview",
1162
- path: "/docs/deploy",
1163
- summary: "Architecture and lifecycle. Prefer docs_get id coderunner for type usage, Dockerfile, and create prerequisites.",
1164
- embedded: CODERUNNER_GUIDE,
1165
- lastUpdated: "2026-09-25"
1402
+ lastUpdated: "2026-10-08"
1166
1403
  },
1167
1404
  {
1168
1405
  id: "authentication",
1169
1406
  title: "Workspace Authentication",
1170
1407
  path: "/docs/access/app-authentication",
1171
- summary: "Workspace site auth (Groundfloor or external) is optional end-user login. Leaving Groundfloor keeps the realm (disabled) until Delete Groundfloor login. Separate from portal Team. Not required to create Apps or Coderunners."
1408
+ summary: "App end users sign in only to the app via the workspace App Authentication realm; no Groundfloor console account or workspace membership. Modes none / Groundfloor / external, users and roles, SSO, multi-tenant. Optional; not required to create Apps or Coderunners. Separate from portal Team.",
1409
+ embedded: APP_AUTHENTICATION_GUIDE,
1410
+ lastUpdated: "2026-10-08"
1172
1411
  },
1173
1412
  {
1174
1413
  id: "access",
@@ -1194,7 +1433,7 @@ var DOC_CATALOG = [
1194
1433
  path: "/docs/apps/shell",
1195
1434
  summary: "Product Apps vs Coderunner. Always gf apps init --slug <portal-slug>, then npm run release and gf apps publish --path release.zip. Never zip dist/ or upload only remoteEntry.js.",
1196
1435
  embedded: SHELL_APP_GUIDE,
1197
- lastUpdated: "2026-09-26"
1436
+ lastUpdated: "2026-10-08"
1198
1437
  },
1199
1438
  {
1200
1439
  id: "design-standard",
@@ -1224,9 +1463,9 @@ var DOC_CATALOG = [
1224
1463
  id: "vault",
1225
1464
  title: "Data Vault",
1226
1465
  path: "/docs/api/vault",
1227
- summary: "Create collections with JSON Schema, query totals vs limit, ACLs, implicit tables, nested filters, identities vs M2M.",
1466
+ summary: "Create collections with JSON Schema, record routes (/vault/collections/{collection}/\u2026), query request/response (items, total, has_more, next_offset, limit 1-500), $text substring search (bounded scan, chunked_scan/scan_cursor), ACLs, implicit tables, nested filters, identities vs M2M, secrets fallback.",
1228
1467
  embedded: VAULT_GUIDE,
1229
- lastUpdated: "2026-09-26"
1468
+ lastUpdated: "2026-10-09"
1230
1469
  },
1231
1470
  {
1232
1471
  id: "feature-flags",
@@ -1234,7 +1473,7 @@ var DOC_CATALOG = [
1234
1473
  path: "/docs/configure/feature-flags",
1235
1474
  summary: "Built-in per-environment feature flags (toggles / config values) \u2014 use these, not a Vault collection. gf flags ls|set|compare|promote. Apps read GET \u2026/flags/runtime: Shell apps with the end-user JWT + window.__GF_ENVIRONMENT_ID__ (starter-kit useFlag), Coderunners with gf m2m token + GROUNDFLOOR_ENVIRONMENT_ID. Default environment is dev.",
1236
1475
  embedded: FLAGS_GUIDE,
1237
- lastUpdated: "2026-09-26"
1476
+ lastUpdated: "2026-10-08"
1238
1477
  },
1239
1478
  {
1240
1479
  id: "flags-api",
@@ -1242,7 +1481,7 @@ var DOC_CATALOG = [
1242
1481
  path: "/docs/api/flags",
1243
1482
  summary: "REST reference for flags: list/get/put/delete per environment_id (omitted \u2192 workspace default, dev), compare, promote, runtime read. Key format, value_type validation, auth per method (M2M read-only), error codes.",
1244
1483
  embedded: FLAGS_GUIDE,
1245
- lastUpdated: "2026-09-26"
1484
+ lastUpdated: "2026-10-08"
1246
1485
  },
1247
1486
  {
1248
1487
  id: "scheduled-jobs",
@@ -1250,12 +1489,19 @@ var DOC_CATALOG = [
1250
1489
  path: "/docs/deploy/schedules",
1251
1490
  summary: "workload_type=schedule plus cron. gf coderunner status shows type/schedule/last_job. Use Process Log, not a public URL.",
1252
1491
  embedded: CODERUNNER_GUIDE,
1253
- lastUpdated: "2026-09-25"
1492
+ lastUpdated: "2026-10-08"
1254
1493
  }
1255
1494
  ];
1256
1495
  function docUrl(entry) {
1257
1496
  return `${DOCS_ORIGIN}${entry.path}`;
1258
1497
  }
1498
+ var DOC_ALIASES = {
1499
+ "coderunner-overview": "coderunner"
1500
+ };
1501
+ function getDocById(id) {
1502
+ const key = DOC_ALIASES[id] ?? id;
1503
+ return DOC_CATALOG.find((d) => d.id === key);
1504
+ }
1259
1505
  function findDoc(query) {
1260
1506
  const q = query.trim().toLowerCase();
1261
1507
  if (!q) return DOC_CATALOG;
@@ -1269,7 +1515,7 @@ function htmlToText(html) {
1269
1515
  return article.replace(/<script\b[\s\S]*?<\/script>/gi, " ").replace(/<style\b[\s\S]*?<\/style>/gi, " ").replace(/<nav\b[\s\S]*?<\/nav>/gi, " ").replace(/<footer\b[\s\S]*?<\/footer>/gi, " ").replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&#39;/g, "'").replace(/&quot;/g, '"').replace(/\s+/g, " ").trim();
1270
1516
  }
1271
1517
  var MAX_CHARS = 12e3;
1272
- async function fetchDocPage(entry) {
1518
+ async function fetchDocPage(entry, offset = 0) {
1273
1519
  const url = docUrl(entry);
1274
1520
  if (entry.embedded?.trim()) {
1275
1521
  return {
@@ -1290,15 +1536,18 @@ async function fetchDocPage(entry) {
1290
1536
  throw new Error(`Docs fetch failed: ${res.status} ${url}`);
1291
1537
  }
1292
1538
  const html = await res.text();
1293
- let body = htmlToText(html);
1294
- const truncated = body.length > MAX_CHARS;
1295
- if (truncated) body = `${body.slice(0, MAX_CHARS)}
1296
- \u2026`;
1539
+ const full = htmlToText(html);
1540
+ const start = Math.max(0, Math.min(offset, full.length));
1541
+ const end = start + MAX_CHARS;
1542
+ const truncated = full.length > end;
1543
+ const body = full.slice(start, end) + (truncated ? "\n\u2026" : "");
1297
1544
  return {
1298
1545
  url,
1299
1546
  title: entry.title,
1300
1547
  text: body,
1301
1548
  truncated,
1549
+ totalChars: full.length,
1550
+ nextOffset: truncated ? end : null,
1302
1551
  source: "docs.groundfloor.cloud",
1303
1552
  lastUpdated: null,
1304
1553
  mcpVersion: GF_MCP_VERSION
@@ -1306,6 +1555,10 @@ async function fetchDocPage(entry) {
1306
1555
  }
1307
1556
 
1308
1557
  // src/playbook.ts
1558
+ function createAppCommand(slug, name) {
1559
+ const displayName = name?.trim() || "<App Name>";
1560
+ return `gf apps create --name ${JSON.stringify(displayName)} --slug ${slug || "<portal-slug>"} --kind shell_federated`;
1561
+ }
1309
1562
  function suggestCli(intent, args) {
1310
1563
  const ws = args?.workspace ? ` -w ${args.workspace}` : "";
1311
1564
  const name = args?.name ?? args?.coderunner;
@@ -1325,7 +1578,7 @@ function suggestCli(intent, args) {
1325
1578
  case "deploy":
1326
1579
  return {
1327
1580
  command: name ? `gf deploy -n ${name}${ws}` : `gf deploy${ws}`,
1328
- why: 'Package the current (or --path) folder and deploy as a Coderunner. App authentication is optional (end-user login only). workloadType function|job|schedule|service (service = Deployment; requires a Dockerfile). Schedules need --schedule "<cron>" or groundfloor.json schedule. Node bootstrap: scripts.start, listen PORT||8080, bind 0.0.0.0. Do not create an App for this.',
1581
+ why: 'Package the current (or --path) folder and deploy as a Coderunner. App authentication is optional (end-user login only). workloadType function|job|schedule|service (service = Deployment; requires a Dockerfile). Schedules need --schedule "<cron>" or groundfloor.json schedule. Node function/job/schedule: root handler.js exporting handler (no npm start). Node service/Dockerfile: scripts.start, listen PORT||8080, bind 0.0.0.0. Do not create an App for this.',
1329
1582
  docs: "https://docs.groundfloor.cloud/docs/deploy"
1330
1583
  };
1331
1584
  case "run":
@@ -1346,6 +1599,14 @@ function suggestCli(intent, args) {
1346
1599
  why: "Process Log (stdout/stderr). gf deploy prints the last lines on failure; this command fetches more. runner_id accepts a coderunner id.",
1347
1600
  docs: "https://docs.groundfloor.cloud/docs/deploy"
1348
1601
  };
1602
+ case "create_app": {
1603
+ const slug = args?.slug?.trim();
1604
+ return {
1605
+ command: createAppCommand(slug, args?.name),
1606
+ why: 'Register the App in the workspace. gf apps publish resolves the App by slug and fails with "App not found" if this was never run. A 409 means the slug already exists: skip to init/publish. This is a Shell federated App, not a Coderunner (never --kind coderunner).',
1607
+ docs: "https://docs.groundfloor.cloud/docs/apps/shell/getting-started"
1608
+ };
1609
+ }
1349
1610
  case "init_app": {
1350
1611
  const slug = args?.slug?.trim();
1351
1612
  return {
@@ -1354,12 +1615,15 @@ function suggestCli(intent, args) {
1354
1615
  docs: "https://docs.groundfloor.cloud/docs/apps/shell"
1355
1616
  };
1356
1617
  }
1357
- case "publish_app":
1618
+ case "publish_app": {
1619
+ const slug = args?.slug?.trim();
1620
+ const dir = slug || "<slug>";
1358
1621
  return {
1359
- command: args?.slug ? `gf apps init --slug ${args.slug} && cd ${args.slug} && npm install && npm run release && gf apps publish --path release.zip` : "gf apps init --slug <portal-slug> && cd <slug> && npm install && npm run release && gf apps publish --path release.zip",
1360
- why: "Ship a Shell federated App. Always init the kit first. npm run release (not zip dist/, not a lone remoteEntry.js). Do not gf deploy. Do not invent App.tsx.",
1622
+ command: `${createAppCommand(slug, args?.name)} && gf apps init --slug ${slug || "<portal-slug>"} && cd ${dir} && npm install && npm run release && gf apps publish --path release.zip`,
1623
+ why: "Ship a Shell federated App. Register it (gf apps create), then init the kit. Skip create if the App already exists (create returns 409). npm run release (not zip dist/, not a lone remoteEntry.js). Do not gf deploy. Do not invent App.tsx.",
1361
1624
  docs: "https://docs.groundfloor.cloud/docs/apps/shell"
1362
1625
  };
1626
+ }
1363
1627
  case "environments":
1364
1628
  return {
1365
1629
  command: `gf environments ls${ws}`,
@@ -1368,8 +1632,8 @@ function suggestCli(intent, args) {
1368
1632
  };
1369
1633
  case "cancel_deployment":
1370
1634
  return {
1371
- command: name ? `gf coderunner cancel-deployment -c ${name}${ws}` : `gf coderunner cancel-deployment -c <id-or-slug>${ws}`,
1372
- why: "Delete the current deployment (or --deployment-id). Use this for a deploy stuck in deploying.",
1635
+ command: name ? `gf coderunner cancel-deployment -c ${name} --yes${ws}` : `gf coderunner cancel-deployment -c <id-or-slug> --yes${ws}`,
1636
+ why: "Delete the current deployment (or --deployment-id). Use this for a deploy stuck in deploying. --yes cancels without a confirmation prompt (required without a TTY).",
1373
1637
  docs: "https://docs.groundfloor.cloud/docs/deploy"
1374
1638
  };
1375
1639
  case "secrets":
@@ -1400,7 +1664,7 @@ function suggestCli(intent, args) {
1400
1664
  default:
1401
1665
  return {
1402
1666
  command: "gf --help",
1403
- why: `Unknown intent "${intent}". Use login | switch_workspace | deploy | run | status | logs | environments | cancel_deployment | init_app | publish_app | secrets | flags | dataplane | vault_token.`,
1667
+ why: `Unknown intent "${intent}". Use login | switch_workspace | deploy | run | status | logs | environments | cancel_deployment | create_app | init_app | publish_app | secrets | flags | dataplane | vault_token.`,
1404
1668
  docs: "https://docs.groundfloor.cloud/docs/developer-tools/cli"
1405
1669
  };
1406
1670
  }
@@ -1431,7 +1695,9 @@ function errText(err) {
1431
1695
  return { content: [{ type: "text", text: message }], isError: true };
1432
1696
  }
1433
1697
 
1434
- // src/index.ts
1698
+ // src/server.ts
1699
+ var READ_ONLY = { readOnlyHint: true };
1700
+ var DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true };
1435
1701
  var wsId = z.string().optional().describe("Override the gf default workspace for this call only");
1436
1702
  async function cfg(workspaceId, requireWorkspace = true) {
1437
1703
  return resolveMcpConfig({ workspaceId, requireWorkspace });
@@ -1450,6 +1716,7 @@ server.tool(
1450
1716
  "whoami",
1451
1717
  "Show the gf CLI session (identity, cell, selected workspace). Run `gf login` if this fails.",
1452
1718
  {},
1719
+ READ_ONLY,
1453
1720
  async () => {
1454
1721
  try {
1455
1722
  const c = await cfg(void 0, false);
@@ -1473,6 +1740,7 @@ server.tool(
1473
1740
  "workspace_context",
1474
1741
  "Live context for the selected workspace: session, coderunners, apps, environments (with the default one), feature flags, dataplane (access layer, not a database), databases_howto, suggested gf commands. Use docs_get id coderunner before deploying; id databases for data work; id feature-flags for toggles/config; id css-and-styling for own CSS framework.",
1475
1742
  { workspace_id: wsId },
1743
+ READ_ONLY,
1476
1744
  async ({ workspace_id }) => {
1477
1745
  try {
1478
1746
  const c = await cfg(workspace_id);
@@ -1524,20 +1792,24 @@ server.tool(
1524
1792
  runtime_contract: "coderunner-runtime",
1525
1793
  before_create: "Team write can create Coderunners without App authentication. Site auth is optional (end-user login). Mode none uses Portal JWKS (GET /v1/public/jwks.json).",
1526
1794
  types: {
1527
- function: "HTTP handler on PORT/8080 bound to 0.0.0.0. Invoke with gf coderunner run or the Deployment URL. Node requires package.json scripts.start.",
1528
- job: "Run-to-completion worker. Deploy, then gf coderunner run.",
1795
+ function: "Request/response handler. Invoke with gf coderunner run or the Deployment URL. Node: root handler.js exporting handler (npm start is not run); see docs_get coderunner-runtime.",
1796
+ job: "Run-to-completion worker. Node: root handler.js exporting handler. Deploy, then gf coderunner run.",
1529
1797
  schedule: 'Cron job. No public Deployment URL. Set cron with gf deploy --workload-type schedule --schedule "<cron>" (or groundfloor.json schedule).',
1530
1798
  service: "Long-running Deployment (same type as service). Dockerfile at ZIP root is required. Listen on 8080 / 0.0.0.0."
1531
1799
  },
1532
- node: "No Dockerfile \u2192 platform injects node:18-alpine CMD npm start EXPOSE 8080. Require scripts.start; listen on PORT||8080; bind 0.0.0.0. gf deploy fails fast if scripts.start is missing.",
1800
+ node: "function/job/schedule without a Dockerfile run a generated /app/main.js that requires ./handler: ship a root handler.js exporting handler (scripts.start not needed). service/Dockerfile running npm start: scripts.start, listen PORT||8080, bind 0.0.0.0. gf deploy fails fast on either missing.",
1533
1801
  local_live_data: 'eval "$(gf env)". For files/secrets as a builder use Bearer $(gf token). For Vault/Files as the deployed workload, attach an M2M client and use Bearer $(gf m2m token). Never DATAPLANE_SERVICE_API_KEY. Do not zip .env files.'
1534
1802
  },
1803
+ app_end_users: {
1804
+ docs_get: "authentication",
1805
+ rule: "End users of a customer's app sign in only to that app, through the workspace's App Authentication realm (Administer \u2192 App authentication). They have no Groundfloor console account and no workspace membership; their site role (viewer/user/admin or custom) decides what they reach via Site access. Do not build or suggest Portal logins for them."
1806
+ },
1535
1807
  shell_app_howto: {
1536
1808
  docs_get: "shell",
1537
- rule: "Never invent a Vite + Module Federation app. gf apps init --slug <portal-slug>, npm install, npm run release, gf apps publish --path release.zip. Do not zip dist/ or upload only remoteEntry.js.",
1809
+ rule: "Never invent a Vite + Module Federation app. gf apps create --name <name> --slug <portal-slug> --kind shell_federated (registers the App; publish fails with App not found without it), gf apps init --slug <portal-slug>, npm install, npm run release, gf apps publish --path release.zip. Do not zip dist/ or upload only remoteEntry.js.",
1538
1810
  css: "Tailwind is the default (docs_get id css-and-styling). Own framework: FederatedStyleRoot + ?inline CSS in Shadow DOM. Do not load a second CSS framework onto the Shell page.",
1539
1811
  design: "docs_get id design-standard",
1540
- suggest_cli: "init_app | publish_app"
1812
+ suggest_cli: "create_app | init_app | publish_app"
1541
1813
  },
1542
1814
  databases_howto: {
1543
1815
  docs_get: "databases",
@@ -1556,6 +1828,7 @@ server.tool(
1556
1828
  flags: suggestCli("flags"),
1557
1829
  cancel_deployment: suggestCli("cancel_deployment"),
1558
1830
  vault_token: suggestCli("m2m_token"),
1831
+ create_app: suggestCli("create_app"),
1559
1832
  init_app: suggestCli("init_app"),
1560
1833
  publish_app: suggestCli("publish_app")
1561
1834
  },
@@ -1575,6 +1848,7 @@ server.tool(
1575
1848
  "workspaces_list",
1576
1849
  "List workspaces visible to the current user. To change the default, tell the user to run `gf workspaces use <uuid>` (CLI owns that).",
1577
1850
  {},
1851
+ READ_ONLY,
1578
1852
  async () => {
1579
1853
  try {
1580
1854
  const c = await cfg(void 0, false);
@@ -1596,6 +1870,7 @@ server.tool(
1596
1870
  "coderunner_list",
1597
1871
  "List Coderunners in the workspace (read-only).",
1598
1872
  { workspace_id: wsId },
1873
+ READ_ONLY,
1599
1874
  async ({ workspace_id }) => {
1600
1875
  try {
1601
1876
  const c = await cfg(workspace_id);
@@ -1613,6 +1888,7 @@ server.tool(
1613
1888
  workspace_id: wsId,
1614
1889
  coderunner_id: z.string().min(1).describe("Coderunner id or slug")
1615
1890
  },
1891
+ READ_ONLY,
1616
1892
  async (args) => {
1617
1893
  try {
1618
1894
  const c = await cfg(args.workspace_id);
@@ -1642,6 +1918,7 @@ server.tool(
1642
1918
  search: z.string().optional(),
1643
1919
  deployment_id: z.string().optional()
1644
1920
  },
1921
+ READ_ONLY,
1645
1922
  async (args) => {
1646
1923
  try {
1647
1924
  const c = await cfg(args.workspace_id);
@@ -1668,6 +1945,7 @@ server.tool(
1668
1945
  "environments_list",
1669
1946
  "List environments in the workspace (id, name, status).",
1670
1947
  { workspace_id: wsId },
1948
+ READ_ONLY,
1671
1949
  async ({ workspace_id }) => {
1672
1950
  try {
1673
1951
  const c = await cfg(workspace_id);
@@ -1683,30 +1961,23 @@ server.tool(
1683
1961
  );
1684
1962
  server.tool(
1685
1963
  "coderunner_cancel_deployment",
1686
- "Delete a coderunner deployment (current deployment if deployment_id is omitted). Same as `gf coderunner cancel-deployment`.",
1964
+ "DESTRUCTIVE. Deletes one deployment of a Coderunner; if it is the live deployment the service goes down. deployment_id is required (find it with coderunner_get_status). Acts only on the configured workspace (gf workspaces use). Same as `gf coderunner cancel-deployment --deployment-id`. Confirm with the user before calling.",
1687
1965
  {
1688
- workspace_id: wsId,
1689
1966
  coderunner_id: z.string().min(1).describe("Coderunner id or slug"),
1690
- deployment_id: z.string().optional().describe("Deployment id; defaults to the current deployment")
1967
+ deployment_id: z.string().trim().min(1).describe(
1968
+ "Id of the deployment to delete (coderunner_get_status \u2192 deployment.id). Required: there is no default."
1969
+ )
1691
1970
  },
1971
+ DESTRUCTIVE,
1692
1972
  async (args) => {
1693
1973
  try {
1694
- const c = await cfg(args.workspace_id);
1974
+ const c = await cfg();
1695
1975
  const id = await resolveCoderunnerId(
1696
1976
  c.apiUrl,
1697
1977
  c.workspaceId,
1698
1978
  args.coderunner_id
1699
1979
  );
1700
- let deploymentId = args.deployment_id?.trim();
1701
- if (!deploymentId) {
1702
- const current = await getDeployment(c.apiUrl, c.workspaceId, id);
1703
- deploymentId = current?.id?.trim();
1704
- }
1705
- if (!deploymentId) {
1706
- throw new Error(
1707
- `No current deployment for coderunner ${id}. Pass deployment_id.`
1708
- );
1709
- }
1980
+ const deploymentId = args.deployment_id;
1710
1981
  await deleteCoderunnerDeployment(
1711
1982
  c.apiUrl,
1712
1983
  c.workspaceId,
@@ -1714,6 +1985,7 @@ server.tool(
1714
1985
  deploymentId
1715
1986
  );
1716
1987
  return text({
1988
+ workspace_id: c.workspaceId,
1717
1989
  coderunner_id: id,
1718
1990
  deployment_id: deploymentId,
1719
1991
  deleted: true,
@@ -1732,6 +2004,7 @@ server.tool(
1732
2004
  coderunner_id: z.string().min(1),
1733
2005
  job_id: z.string().min(1)
1734
2006
  },
2007
+ READ_ONLY,
1735
2008
  async (args) => {
1736
2009
  try {
1737
2010
  const c = await cfg(args.workspace_id);
@@ -1750,6 +2023,7 @@ server.tool(
1750
2023
  "apps_list",
1751
2024
  "List product Apps (Shell / standalone). Workloads are Coderunners, not Apps.",
1752
2025
  { workspace_id: wsId },
2026
+ READ_ONLY,
1753
2027
  async ({ workspace_id }) => {
1754
2028
  try {
1755
2029
  const c = await cfg(workspace_id);
@@ -1763,6 +2037,7 @@ server.tool(
1763
2037
  "apps_get",
1764
2038
  "Get one product App by id or slug.",
1765
2039
  { workspace_id: wsId, app_id: z.string().min(1) },
2040
+ READ_ONLY,
1766
2041
  async (args) => {
1767
2042
  try {
1768
2043
  const c = await cfg(args.workspace_id);
@@ -1780,6 +2055,7 @@ server.tool(
1780
2055
  "secrets_list",
1781
2056
  "List secret keys (values are not shown). To set/delete, use `gf secrets`.",
1782
2057
  { workspace_id: wsId, environment_id: z.string().optional() },
2058
+ READ_ONLY,
1783
2059
  async (args) => {
1784
2060
  try {
1785
2061
  const c = await cfg(args.workspace_id);
@@ -1795,6 +2071,7 @@ server.tool(
1795
2071
  "flags_list",
1796
2072
  "List feature flags and values in one environment (omit environment_id \u2192 workspace default, dev; response echoes the resolved id). To set/delete/promote, use `gf flags`. docs_get id feature-flags for runtime use.",
1797
2073
  { workspace_id: wsId, environment_id: z.string().optional() },
2074
+ READ_ONLY,
1798
2075
  async (args) => {
1799
2076
  try {
1800
2077
  const c = await cfg(args.workspace_id);
@@ -1810,6 +2087,7 @@ server.tool(
1810
2087
  "files_list",
1811
2088
  "List workspace files (read-only). Upload/download with `gf files`.",
1812
2089
  { workspace_id: wsId },
2090
+ READ_ONLY,
1813
2091
  async ({ workspace_id }) => {
1814
2092
  try {
1815
2093
  const c = await cfg(workspace_id);
@@ -1821,17 +2099,45 @@ server.tool(
1821
2099
  );
1822
2100
  server.tool(
1823
2101
  "domains_list",
1824
- "List custom domains for a Coderunner service. Add/verify with `gf domains`.",
1825
- { workspace_id: wsId, coderunner_id: z.string().min(1) },
2102
+ 'List custom domains of a standalone App (the service-backed kind). Pass app_id (id or slug) or coderunner_id (the App\'s primary Coderunner). Standalone Coderunners have no domains of their own: the Coderunner domain routes always return 400 "Custom domains are available on Apps" and that server message is surfaced if hit. Shell apps have none. Add/verify with `gf domains` only for an App-backed service.',
2103
+ {
2104
+ workspace_id: wsId,
2105
+ app_id: z.string().min(1).optional().describe("App id or slug"),
2106
+ coderunner_id: z.string().min(1).optional().describe("Coderunner id or slug; resolves to the App whose primary Coderunner it is")
2107
+ },
2108
+ READ_ONLY,
1826
2109
  async (args) => {
1827
2110
  try {
2111
+ if (!args.app_id && !args.coderunner_id) {
2112
+ throw new Error("Pass app_id (id or slug) or coderunner_id.");
2113
+ }
1828
2114
  const c = await cfg(args.workspace_id);
1829
- const id = await resolveCoderunnerId(
2115
+ const listed = await listApps(c.apiUrl, c.workspaceId);
2116
+ let match;
2117
+ if (args.app_id) {
2118
+ match = listed.apps.find(
2119
+ (app) => app.id === args.app_id || app.slug === args.app_id
2120
+ );
2121
+ if (!match) throw new Error(`App not found: ${args.app_id}`);
2122
+ } else {
2123
+ const crId = await resolveCoderunnerId(
2124
+ c.apiUrl,
2125
+ c.workspaceId,
2126
+ args.coderunner_id
2127
+ );
2128
+ match = listed.apps.find((app) => app.primary_coderunner_id === crId);
2129
+ if (!match) {
2130
+ throw new Error(
2131
+ `Coderunner ${crId} is not the primary Coderunner of any App, and standalone Coderunners cannot have custom domains. Package it as an App first (gf apps create --kind standalone --primary-coderunner <id>).`
2132
+ );
2133
+ }
2134
+ }
2135
+ const domains = await cpGetJson(
1830
2136
  c.apiUrl,
1831
- c.workspaceId,
1832
- args.coderunner_id
2137
+ c.token,
2138
+ `/v1/workspaces/${encodeURIComponent(c.workspaceId)}/apps/${encodeURIComponent(match.id)}/coderunner/domains`
1833
2139
  );
1834
- return text(await listDomains(c.apiUrl, c.workspaceId, id));
2140
+ return text({ app_id: match.id, app_slug: match.slug, ...domains });
1835
2141
  } catch (e) {
1836
2142
  return errText(e);
1837
2143
  }
@@ -1839,8 +2145,9 @@ server.tool(
1839
2145
  );
1840
2146
  server.tool(
1841
2147
  "dataplane_status",
1842
- "Dataplane setup status (Dataplane is how Groundfloor talks to databases, not a database itself). PostgreSQL (shared, platform-managed) is ready through it by default. Provision with `gf dataplane provision`.",
2148
+ "Dataplane setup status for the workspace, as reported by Control Plane. Provision with `gf dataplane provision`. See docs_get id databases for what Dataplane is.",
1843
2149
  { workspace_id: wsId },
2150
+ READ_ONLY,
1844
2151
  async ({ workspace_id }) => {
1845
2152
  try {
1846
2153
  const c = await cfg(workspace_id);
@@ -1850,10 +2157,73 @@ server.tool(
1850
2157
  }
1851
2158
  }
1852
2159
  );
2160
+ var accountId = z.string().optional().describe(
2161
+ "Organization/account id (NOT the workspace id). Defaults to the account that owns the selected workspace."
2162
+ );
2163
+ async function resolveAccountId(c, explicit) {
2164
+ if (explicit?.trim()) return explicit.trim();
2165
+ const workspaces = await listWorkspaces(c.apiUrl);
2166
+ const ws = workspaces.find((w) => w.id === c.workspaceId);
2167
+ const id = ws?.account_id || ws?.organization_id;
2168
+ if (!id) {
2169
+ throw new Error(
2170
+ "Could not determine the account for the selected workspace. Pass account_id (organization id)."
2171
+ );
2172
+ }
2173
+ return id;
2174
+ }
2175
+ server.tool(
2176
+ "quota_get",
2177
+ "Plan tier and usage against plan limits (workspaces, file storage, activity-log events, \u2026) for the organization that owns the workspace. Account-scoped, not workspace-scoped; readable by organization members. Use when a create/deploy is refused for plan reasons.",
2178
+ { workspace_id: wsId, account_id: accountId },
2179
+ READ_ONLY,
2180
+ async (args) => {
2181
+ try {
2182
+ const c = await cfg(args.workspace_id);
2183
+ const id = await resolveAccountId(c, args.account_id);
2184
+ return text(
2185
+ await cpGetJson(
2186
+ c.apiUrl,
2187
+ c.token,
2188
+ `/v1/accounts/${encodeURIComponent(id)}/quota`
2189
+ )
2190
+ );
2191
+ } catch (e) {
2192
+ return errText(e);
2193
+ }
2194
+ }
2195
+ );
2196
+ server.tool(
2197
+ "billing_summary",
2198
+ "Compute-unit usage and billing estimate for the organization that owns the workspace (account-scoped, not workspace-scoped). Only Owners and Billing admins may read it; others get 403. The response notes whether it is an estimate.",
2199
+ {
2200
+ workspace_id: wsId,
2201
+ account_id: accountId,
2202
+ since_seconds: z.number().int().min(3600).max(400 * 86400).optional().describe("Lookback window in seconds (server default 30 days; min 1 hour, max ~13 months)")
2203
+ },
2204
+ READ_ONLY,
2205
+ async (args) => {
2206
+ try {
2207
+ const c = await cfg(args.workspace_id);
2208
+ const id = await resolveAccountId(c, args.account_id);
2209
+ const qs = args.since_seconds ? `?since_seconds=${args.since_seconds}` : "";
2210
+ return text(
2211
+ await cpGetJson(
2212
+ c.apiUrl,
2213
+ c.token,
2214
+ `/v1/accounts/${encodeURIComponent(id)}/billing/summary${qs}`
2215
+ )
2216
+ );
2217
+ } catch (e) {
2218
+ return errText(e);
2219
+ }
2220
+ }
2221
+ );
1853
2222
  server.tool(
1854
2223
  "docs_list",
1855
2224
  "Catalog of Groundfloor product docs (docs.groundfloor.cloud) the MCP can fetch.",
1856
2225
  { query: z.string().optional() },
2226
+ READ_ONLY,
1857
2227
  async ({ query }) => {
1858
2228
  const hits = findDoc(query ?? "");
1859
2229
  return text({
@@ -1871,15 +2241,19 @@ server.tool(
1871
2241
  );
1872
2242
  server.tool(
1873
2243
  "docs_get",
1874
- "How-to for Coderunner, Shell, design-standard, css-and-styling, databases, access, m2m. Pass id from docs_list. Prefer id coderunner or coderunner-runtime before deploying. For site grants use id access. For Coderunner Vault/Files use id m2m.",
2244
+ "How-to for Coderunner, Shell, design-standard, css-and-styling, databases, access, m2m, authentication (app end users). Pass id from docs_list. Prefer id coderunner or coderunner-runtime before deploying. For site grants use id access. For Coderunner Vault/Files use id m2m.",
1875
2245
  {
1876
2246
  id: z.string().min(1).describe(
1877
2247
  "Catalog id, e.g. coderunner | coderunner-runtime | shell | design-standard | css-and-styling | databases | agents | developers | authentication | access | m2m"
2248
+ ),
2249
+ offset: z.number().int().min(0).optional().describe(
2250
+ "Live (non-embedded) pages only: character offset to resume from. Pass nextOffset from the previous result when truncated is true."
1878
2251
  )
1879
2252
  },
1880
- async ({ id }) => {
2253
+ READ_ONLY,
2254
+ async ({ id, offset }) => {
1881
2255
  try {
1882
- const entry = DOC_CATALOG.find((d) => d.id === id);
2256
+ const entry = getDocById(id);
1883
2257
  if (!entry) {
1884
2258
  return errText(
1885
2259
  new Error(
@@ -1887,7 +2261,7 @@ server.tool(
1887
2261
  )
1888
2262
  );
1889
2263
  }
1890
- return text(await fetchDocPage(entry));
2264
+ return text(await fetchDocPage(entry, offset ?? 0));
1891
2265
  } catch (e) {
1892
2266
  return errText(e);
1893
2267
  }
@@ -1895,7 +2269,7 @@ server.tool(
1895
2269
  );
1896
2270
  server.tool(
1897
2271
  "suggest_cli",
1898
- "Return the gf command to run. For a Shell App use intent init_app or publish_app (never scaffold App.tsx; never gf deploy). MCP does not execute mutations.",
2272
+ "Return the gf command to run. For a Shell App use intent create_app, init_app or publish_app (publish_app is the full create + init + release + publish chain; never scaffold App.tsx; never gf deploy). MCP does not execute mutations.",
1899
2273
  {
1900
2274
  intent: z.enum([
1901
2275
  "login",
@@ -1906,6 +2280,7 @@ server.tool(
1906
2280
  "logs",
1907
2281
  "environments",
1908
2282
  "cancel_deployment",
2283
+ "create_app",
1909
2284
  "init_app",
1910
2285
  "publish_app",
1911
2286
  "secrets",
@@ -1915,10 +2290,11 @@ server.tool(
1915
2290
  "m2m_token"
1916
2291
  ]),
1917
2292
  workspace: z.string().optional(),
1918
- name: z.string().optional().describe("Coderunner name/id for deploy/run/status/logs/cancel_deployment/vault_token"),
1919
- slug: z.string().optional().describe("Portal app slug for init_app / publish_app"),
2293
+ name: z.string().optional().describe("Coderunner name/id for deploy/run/status/logs/cancel_deployment/vault_token; App display name for create_app / publish_app"),
2294
+ slug: z.string().optional().describe("Portal app slug for create_app / init_app / publish_app"),
1920
2295
  cell: z.enum(["production", "stage", "dev"]).optional()
1921
2296
  },
2297
+ READ_ONLY,
1922
2298
  async (args) => {
1923
2299
  return text(
1924
2300
  suggestCli(args.intent, {
@@ -1931,6 +2307,8 @@ server.tool(
1931
2307
  );
1932
2308
  }
1933
2309
  );
2310
+
2311
+ // src/index.ts
1934
2312
  async function main() {
1935
2313
  const transport = new StdioServerTransport();
1936
2314
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groundfloorcloud/mcp",
3
- "version": "0.1.15",
3
+ "version": "0.2.0",
4
4
  "description": "Groundfloor MCP — inspect workspace state and docs; take actions with the gf CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,14 +27,14 @@
27
27
  "scripts": {
28
28
  "build": "tsup",
29
29
  "dev": "tsup --watch",
30
- "typecheck": "tsup --silent",
30
+ "typecheck": "tsc --noEmit",
31
31
  "start": "node dist/index.js",
32
32
  "clean": "rm -rf dist *.tsbuildinfo .turbo",
33
33
  "prepublishOnly": "npm run build"
34
34
  },
35
35
  "dependencies": {
36
- "@modelcontextprotocol/sdk": "^1.12.1",
37
- "zod": "^3.24.2"
36
+ "@modelcontextprotocol/sdk": "^1.23.0",
37
+ "zod": "^4.4.3"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^20",