@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.
- package/README.md +22 -8
- package/dist/index.js +523 -145
- 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` |
|
|
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` / `
|
|
77
|
-
| `
|
|
78
|
-
| `
|
|
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
|
|
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
|
|
92
|
-
`coderunner-overview`
|
|
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
|
-
|
|
442
|
+
pick("API URL", "GROUNDFLOOR_API_URL", "CP_API") || file.apiUrl || CELLS.production.apiUrl
|
|
418
443
|
);
|
|
419
|
-
const workspaceId = overrides?.workspaceId ||
|
|
420
|
-
const envToken = (
|
|
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
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
560
|
+
\`gf deploy\` checks the Node bits before upload.
|
|
455
561
|
|
|
456
|
-
|
|
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:
|
|
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. \`
|
|
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.
|
|
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
|
-
|
|
507
|
-
\`
|
|
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
|
-
|
|
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
|
|
520
|
-
\`
|
|
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 (\`
|
|
575
|
-
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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:
|
|
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
|
-
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1276
|
+
There is **no** \`/vault/{collection}\` route; always include \`/collections/\`.
|
|
1277
|
+
\`?connection=\` picks the database (default \`postgresql\`).
|
|
1078
1278
|
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
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 (
|
|
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-
|
|
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
|
|
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-
|
|
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: "
|
|
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-
|
|
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
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/'/g, "'").replace(/"/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
|
-
|
|
1294
|
-
const
|
|
1295
|
-
|
|
1296
|
-
|
|
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
|
|
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?.
|
|
1360
|
-
why: "Ship a Shell federated App.
|
|
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
|
|
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/
|
|
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: "
|
|
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: "
|
|
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
|
-
"
|
|
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().
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
1825
|
-
{
|
|
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
|
|
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.
|
|
1832
|
-
|
|
2137
|
+
c.token,
|
|
2138
|
+
`/v1/workspaces/${encodeURIComponent(c.workspaceId)}/apps/${encodeURIComponent(match.id)}/coderunner/domains`
|
|
1833
2139
|
);
|
|
1834
|
-
return text(
|
|
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
|
|
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
|
-
|
|
2253
|
+
READ_ONLY,
|
|
2254
|
+
async ({ id, offset }) => {
|
|
1881
2255
|
try {
|
|
1882
|
-
const entry =
|
|
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.
|
|
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": "
|
|
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.
|
|
37
|
-
"zod": "^
|
|
36
|
+
"@modelcontextprotocol/sdk": "^1.23.0",
|
|
37
|
+
"zod": "^4.4.3"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/node": "^20",
|