@groundfloorcloud/mcp 0.1.16 → 0.2.1
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 +484 -94
- 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,6 +469,88 @@ 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 is granted like any other role: new collections start with
|
|
526
|
+
an \`admin\` grant and new workspaces give it Files write, and removing a
|
|
527
|
+
grant removes admins' access. Keep \`admin\` in every Access PUT unless the
|
|
528
|
+
user means to remove it. A signed-in user with no recognised role
|
|
529
|
+
gets 403 on role-gated endpoints (Vault, feature flags).
|
|
530
|
+
- SSO (OIDC or SAML) can be connected per workspace; role names in the
|
|
531
|
+
provider's tokens must match the roles granted on Site access.
|
|
532
|
+
|
|
533
|
+
## Multi-tenant (customer companies inside one workspace)
|
|
534
|
+
|
|
535
|
+
Turn on **Multi-tenant** in Settings to get a Tenants tab: one tenant per
|
|
536
|
+
customer company, invite-only onboarding, per-tenant SSO. The user's token
|
|
537
|
+
carries the tenant in the \`organization\` claim; the app must keep each tenant's
|
|
538
|
+
data apart using that claim. Site access, data, files and M2M tokens stay shared
|
|
539
|
+
across the workspace.
|
|
540
|
+
|
|
541
|
+
## Turning it off
|
|
542
|
+
|
|
543
|
+
Switching to None or External keeps users, roles and settings (switch back and
|
|
544
|
+
they return). To remove them, turn sign-in off first, then **Delete Groundfloor
|
|
545
|
+
login**. Access grants and M2M tokens are kept.
|
|
546
|
+
|
|
547
|
+
## In a Shell app
|
|
548
|
+
|
|
549
|
+
The signed-in user's JWT comes from \`window.__GF_GET_ACCESS_TOKEN__()\`. See
|
|
550
|
+
\`docs_get\` id \`shell\`. Unattended workloads use M2M instead (\`docs_get\` id
|
|
551
|
+
\`m2m\`), not an end-user login.
|
|
552
|
+
`.trim();
|
|
553
|
+
|
|
447
554
|
// src/coderunner-runtime.ts
|
|
448
555
|
var CODERUNNER_RUNTIME_BODY = `
|
|
449
556
|
The contract depends on the workload type.
|
|
@@ -469,9 +576,58 @@ The contract depends on the workload type.
|
|
|
469
576
|
(\`Cannot find module './handler'\`, \`MODULE_NOT_FOUND\`) and the deployment
|
|
470
577
|
ends **Failed (status 4)**. Check \`gf coderunner logs -c <id>\`.
|
|
471
578
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
579
|
+
### The handler signature
|
|
580
|
+
|
|
581
|
+
Arguments are **\`(context, payload)\`** in that order. Export a function named
|
|
582
|
+
\`handler\` (CommonJS \`module.exports = { handler }\`; an ES module
|
|
583
|
+
\`export async function handler(context, payload)\` also works):
|
|
584
|
+
|
|
585
|
+
\`\`\`js
|
|
586
|
+
const handler = async (context, payload) => {
|
|
587
|
+
// your code
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
module.exports = { handler };
|
|
591
|
+
\`\`\`
|
|
592
|
+
|
|
593
|
+
- \`payload\` is the JSON input: the request body for a function, the run's input
|
|
594
|
+
for a job or schedule. Guard for a missing body (\`payload?.name\`).
|
|
595
|
+
- \`context\` is reserved for runtime information. Do not rely on its contents.
|
|
596
|
+
|
|
597
|
+
### Function return shape (HTTP)
|
|
598
|
+
|
|
599
|
+
Whatever the handler returns becomes the response; you may return anything.
|
|
600
|
+
The documented convention, used by every example, is:
|
|
601
|
+
|
|
602
|
+
\`\`\`js
|
|
603
|
+
const handler = async (context, payload) => {
|
|
604
|
+
if (!payload?.name) {
|
|
605
|
+
return {
|
|
606
|
+
statusCode: 400,
|
|
607
|
+
body: { error: "name is required" },
|
|
608
|
+
headers: { "content-type": "application/json" },
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
return {
|
|
612
|
+
statusCode: 200,
|
|
613
|
+
body: { greeting: \`Hello, \${payload.name}\` },
|
|
614
|
+
headers: { "content-type": "application/json" },
|
|
615
|
+
};
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
module.exports = { handler };
|
|
619
|
+
\`\`\`
|
|
620
|
+
|
|
621
|
+
Use \`200\`/\`201\` for success, \`400\` for bad input, \`401\`/\`403\` for auth,
|
|
622
|
+
\`404\` for not found, \`500\` for unexpected errors (no internals in the body).
|
|
623
|
+
Callers POST JSON to the Deployment URL with a bearer token, or use
|
|
624
|
+
\`gf coderunner run -c <slug> --payload '<json>'\`.
|
|
625
|
+
|
|
626
|
+
### Jobs and schedules: return value and failure
|
|
627
|
+
|
|
628
|
+
A job or schedule needs no return value. If an error **escapes** the handler the
|
|
629
|
+
run is marked **Failed**; if you catch it, the run succeeds and handling it is
|
|
630
|
+
up to you.
|
|
475
631
|
|
|
476
632
|
## Node: service (or any Dockerfile that runs \`npm start\`)
|
|
477
633
|
|
|
@@ -480,7 +636,7 @@ A \`service\` **requires** a root Dockerfile; you own \`CMD\`. If it is
|
|
|
480
636
|
and the process must follow the HTTP contract:
|
|
481
637
|
|
|
482
638
|
\`\`\`dockerfile
|
|
483
|
-
FROM node:
|
|
639
|
+
FROM node:22-alpine
|
|
484
640
|
WORKDIR /app
|
|
485
641
|
COPY . .
|
|
486
642
|
EXPOSE 8080
|
|
@@ -505,8 +661,14 @@ Offer GET \`/\` or \`/health\` for the probe. A Dockerfile whose \`CMD\` is not
|
|
|
505
661
|
|
|
506
662
|
## Python
|
|
507
663
|
|
|
508
|
-
|
|
509
|
-
\`
|
|
664
|
+
Function, job and schedule: \`handler.py\` at the ZIP root (with
|
|
665
|
+
\`requirements.txt\`) defining \`def handler(context, payload):\`, same argument
|
|
666
|
+
order, same return convention (\`statusCode\`, \`body\`,
|
|
667
|
+
\`headers\`; an exception escaping the handler fails a job run). \`print\` goes
|
|
668
|
+
to the Process Log.
|
|
669
|
+
|
|
670
|
+
A Dockerfile-based service binds \`0.0.0.0\` and \`PORT\` (default 8080), for
|
|
671
|
+
example \`uvicorn main:app --host 0.0.0.0 --port \${PORT:-8080}\`.
|
|
510
672
|
|
|
511
673
|
## Jobs and schedules
|
|
512
674
|
|
|
@@ -580,8 +742,9 @@ Pick **one** type per coderunner. Changing type later means a new coderunner.
|
|
|
580
742
|
\`/app/main.js\` that does \`require('./handler')\`; \`npm start\` is **not** run and
|
|
581
743
|
\`scripts.start\` is not required. Missing \`handler.js\` \u2192 crash loop
|
|
582
744
|
(\`Cannot find module './handler'\`), deployment **Failed (status 4)**.
|
|
583
|
-
\`gf deploy\` fails fast on this. Handler arguments
|
|
584
|
-
|
|
745
|
+
\`gf deploy\` fails fast on this. Handler arguments are \`(context, payload)\`; the
|
|
746
|
+
signature and the HTTP return shape (\`{ statusCode, body, headers }\`) are in the
|
|
747
|
+
Runtime contract section above.
|
|
585
748
|
- Node with your own Dockerfile (\`CMD ["npm","start"]\`) instead: listen on
|
|
586
749
|
\`process.env.PORT || 8080\`, bind **\`0.0.0.0\`** (health GET \`/\` or \`/health\`), and
|
|
587
750
|
define \`scripts.start\`.
|
|
@@ -613,7 +776,7 @@ Pick **one** type per coderunner. Changing type later means a new coderunner.
|
|
|
613
776
|
- Node example:
|
|
614
777
|
|
|
615
778
|
\`\`\`dockerfile
|
|
616
|
-
FROM node:
|
|
779
|
+
FROM node:22-alpine
|
|
617
780
|
WORKDIR /app
|
|
618
781
|
COPY package*.json ./
|
|
619
782
|
RUN npm ci --omit=dev
|
|
@@ -736,9 +899,28 @@ do the fetch yourself; keep using \`--secret\` injection for SDKs that only
|
|
|
736
899
|
read a literal \`process.env\` value. Either way, cache the revealed value
|
|
737
900
|
in your process rather than calling \`reveal\` on every request.
|
|
738
901
|
|
|
902
|
+
## Limits an agent will hit
|
|
903
|
+
|
|
904
|
+
Verified against the control plane source; anything not listed here is not a
|
|
905
|
+
documented limit.
|
|
906
|
+
|
|
907
|
+
- **M2M token endpoint**: 20 requests per minute (per client and per source).
|
|
908
|
+
Cache the token until near expiry; do not mint per request.
|
|
909
|
+
- **Vault collection list/query**: \`limit\` 1-500 (default 50).
|
|
910
|
+
- **Feature flag string values**: at most 10,000 characters.
|
|
911
|
+
- **\`docs_get\` live pages**: truncated at 12,000 characters; use \`offset\` to
|
|
912
|
+
read the rest.
|
|
913
|
+
|
|
914
|
+
Plan quota and usage are account-scoped, not workspace-scoped. The **account
|
|
915
|
+
id is not the workspace id**: read \`account_id\` from \`workspaces_list\`. If a
|
|
916
|
+
deploy is refused or you need to know what is in use, call \`quota_get\`.
|
|
917
|
+
\`billing_summary\` shows spend, and only Owners and Billing admins can read it
|
|
918
|
+
(others get 403).
|
|
919
|
+
|
|
739
920
|
## Agent actions
|
|
740
921
|
|
|
741
|
-
|
|
922
|
+
|
|
923
|
+
Inspect: \`workspace_context\`, \`quota_get\`, \`docs_get\` id \`coderunner\` or \`coderunner-runtime\`, \`suggest_cli\`.
|
|
742
924
|
Act: \`gf deploy\`, \`gf coderunner run|status|logs\`, \`gf m2m token\`, \`gf identities ls|mint\`, \`gf api\`. MCP does not deploy.
|
|
743
925
|
`.trim();
|
|
744
926
|
|
|
@@ -911,7 +1093,9 @@ One endpoint for every runtime:
|
|
|
911
1093
|
|
|
912
1094
|
Weak \`ETag\` + \`Cache-Control: private, no-cache\`. Send \`If-None-Match\` \u2192 304.
|
|
913
1095
|
Poll no faster than every 30s (the kit uses 60s). No push channel yet.
|
|
914
|
-
Flag values are not secrets
|
|
1096
|
+
Flag values are not secrets, but the endpoint is role-gated: only users with a
|
|
1097
|
+
recognised site role can read them (see Permission below); a signed-in user
|
|
1098
|
+
with no role gets 403.
|
|
915
1099
|
|
|
916
1100
|
### Shell federated app (browser)
|
|
917
1101
|
|
|
@@ -1026,8 +1210,9 @@ routes (omit it or use \`false\` otherwise \u2014 never \`null\`). \`visibility:
|
|
|
1026
1210
|
|
|
1027
1211
|
## suggest_cli
|
|
1028
1212
|
|
|
1213
|
+
- 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)
|
|
1029
1214
|
- init_app \u2192 \`gf apps init --slug <portal-slug>\`
|
|
1030
|
-
- publish_app \u2192 init + npm install + npm run release + gf apps publish --path release.zip
|
|
1215
|
+
- publish_app \u2192 create + init + npm install + npm run release + gf apps publish --path release.zip
|
|
1031
1216
|
`;
|
|
1032
1217
|
|
|
1033
1218
|
// src/vault-guide.ts
|
|
@@ -1074,14 +1259,63 @@ Example:
|
|
|
1074
1259
|
Or use \`gf api -X POST /v1/workspaces/{workspace_id}/vault/collections --body '{...}'\`.
|
|
1075
1260
|
|
|
1076
1261
|
\`GET \u2026/vault/collections\` lists schema-registered collections. Implicit
|
|
1077
|
-
tables created by the first \`POST \u2026/vault/{collection}\` appear when
|
|
1078
|
-
\`include_implicit=true\` (or after a schema create).
|
|
1262
|
+
tables created by the first \`POST \u2026/vault/collections/{collection}\` appear when
|
|
1263
|
+
you pass \`include_implicit=true\` (or after a schema create).
|
|
1264
|
+
|
|
1265
|
+
## Records: routes
|
|
1266
|
+
|
|
1267
|
+
All under \`/v1/workspaces/{workspace_id}/vault\`:
|
|
1268
|
+
|
|
1269
|
+
| Method and path | Purpose |
|
|
1270
|
+
|---|---|
|
|
1271
|
+
| \`POST /collections/{collection}/query\` | Filtered, paginated read |
|
|
1272
|
+
| \`GET /collections/{collection}/count\` | Count; optional \`filter\` query param (JSON object) |
|
|
1273
|
+
| \`POST /collections/{collection}\` | Create one record, body \`{"record": {...}}\`, 201, returns \`{"document": {...}}\` |
|
|
1274
|
+
| \`PUT /collections/{collection}/{document_id}\` | Update one record, body \`{"record": {...}}\`, returns \`{"document": {...}}\` |
|
|
1275
|
+
| \`DELETE /collections/{collection}/{document_id}\` | Delete one record, 204 |
|
|
1276
|
+
| \`GET /collections/{collection}/schema\` | Schema |
|
|
1277
|
+
| \`GET\`/\`PUT /collections/{collection}/acl\` | Collection ACL |
|
|
1079
1278
|
|
|
1080
|
-
|
|
1279
|
+
There is **no** \`/vault/{collection}\` route; always include \`/collections/\`.
|
|
1280
|
+
\`?connection=\` picks the database (default \`postgresql\`).
|
|
1081
1281
|
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1282
|
+
## Query: request and response
|
|
1283
|
+
|
|
1284
|
+
Request body for \`POST \u2026/collections/{collection}/query\` (all optional):
|
|
1285
|
+
\`filter\`, \`sort\` (e.g. \`["-created_at", "name"]\`), \`projection\`,
|
|
1286
|
+
\`limit\` (1-500, default 50), \`offset\` (default 0), \`connection\`.
|
|
1287
|
+
|
|
1288
|
+
Response:
|
|
1289
|
+
|
|
1290
|
+
\`\`\`json
|
|
1291
|
+
{ "items": [ {...} ], "total": 123, "has_more": true, "next_offset": 50 }
|
|
1292
|
+
\`\`\`
|
|
1293
|
+
|
|
1294
|
+
- The array key is **\`items\`** (not \`records\`).
|
|
1295
|
+
- \`total\` is the match count from Dataplane (see below for \`$text\`) and is
|
|
1296
|
+
**null** when upstream gave no count; it is never filled in from the page
|
|
1297
|
+
length. Do not treat \`total === items.length\` as the full table size.
|
|
1298
|
+
- Page with \`next_offset\` while \`has_more\` is true; \`next_offset\` is null
|
|
1299
|
+
on the last page.
|
|
1300
|
+
|
|
1301
|
+
## Substring search (\`$text\` / \`$search\`)
|
|
1302
|
+
|
|
1303
|
+
\`{ "filter": { "$text": "alice" } }\` is a case-insensitive substring match
|
|
1304
|
+
over every field value. Control Plane runs it itself by scanning the caller's
|
|
1305
|
+
ACL-filtered view, at most \`VAULT_TEXT_SEARCH_MAX_SCAN_ROWS\` rows per request
|
|
1306
|
+
(default 10 000).
|
|
1307
|
+
|
|
1308
|
+
- Plain query: for \`$text\`, \`total\` is computed by Control Plane (not
|
|
1309
|
+
Dataplane). It is exact when the collection fits under the cap; past the cap
|
|
1310
|
+
it is **null** and the results cover only the scanned rows.
|
|
1311
|
+
- Large collections: send \`"chunked_scan": true\`, then
|
|
1312
|
+
\`"scan_cursor": <scan.next_cursor>\` with the same filter/sort until
|
|
1313
|
+
\`scan.complete\` is true. Each response has a \`scan\` block
|
|
1314
|
+
(\`complete\`, \`next_cursor\`, \`scanned_rows\`, \`total_rows\`); the exact
|
|
1315
|
+
match total is the sum of \`items\` across chunks. The cursor is not a
|
|
1316
|
+
snapshot \u2014 concurrent writes can skip or repeat rows.
|
|
1317
|
+
- \`GET \u2026/count\` with \`$text\` uses the plain bounded scan and returns 400
|
|
1318
|
+
past the cap; use the chunked query instead.
|
|
1085
1319
|
|
|
1086
1320
|
## Filters
|
|
1087
1321
|
|
|
@@ -1095,6 +1329,18 @@ Grant site roles on **Administer \u2192 Access**. Workload identities get
|
|
|
1095
1329
|
collection grants via \`PUT \u2026/vault/collections/{name}/acl\`. M2M is Team
|
|
1096
1330
|
Writer (workspace-wide), not an Access subject.
|
|
1097
1331
|
|
|
1332
|
+
- \`PUT \u2026/acl\` is a **full replace**, saved exactly as sent. \`GET\` the
|
|
1333
|
+
current grants first and send them all back with your change: every site
|
|
1334
|
+
role grant, \`admin\` included, and every workload identity grant. Anything
|
|
1335
|
+
you leave out is removed.
|
|
1336
|
+
- The built-in site role \`admin\` is granted like any other role. New
|
|
1337
|
+
collections start with \`admin\` \u2192 \`delete\`; if a PUT leaves \`admin\`
|
|
1338
|
+
out, admins lose access to that collection. Never drop it unless the user
|
|
1339
|
+
asked for that.
|
|
1340
|
+
- Only builders (team members with \`ddl\`, or the Client Portal) read or
|
|
1341
|
+
change ACLs. A site (app end user) token gets 403 on every ACL route,
|
|
1342
|
+
even with the \`admin\` role.
|
|
1343
|
+
|
|
1098
1344
|
## Identities vs M2M
|
|
1099
1345
|
|
|
1100
1346
|
- Unattended Coderunners: attach an M2M client, \`gf deploy\`, then
|
|
@@ -1114,6 +1360,17 @@ schedules.
|
|
|
1114
1360
|
|
|
1115
1361
|
Do not \`--env SENDGRID_SECRET=\u2026\`. That is **422**.
|
|
1116
1362
|
\`gf secrets set SENDGRID_SECRET\` then \`gf deploy --secret SENDGRID_SECRET\`.
|
|
1363
|
+
|
|
1364
|
+
**Caveat:** \`--secret\` injection needs the platform-wide Control Plane setting
|
|
1365
|
+
\`CODERUNNER_INJECT_ENV_SECRETS\`, which is **off by default** and not
|
|
1366
|
+
configurable per workspace. When it is off the secret is not injected. Fallback:
|
|
1367
|
+
give the Coderunner's M2M client the \`secrets:read\` scope (Portal
|
|
1368
|
+
**Administer \u2192 M2M tokens**) and read the value at runtime with
|
|
1369
|
+
\`GET /v1/workspaces/{workspace_id}/secrets/{key}\` using the M2M bearer token.
|
|
1370
|
+
It is read-only, live (no redeploy on rotation) and workspace-wide. Details in
|
|
1371
|
+
\`docs_get\` id \`coderunner\`. The deployed value of the setting cannot be
|
|
1372
|
+
checked from the repo; if \`--secret\` fails or the variable is empty, use the
|
|
1373
|
+
fallback rather than plaintext \`--env\`.
|
|
1117
1374
|
`.trim();
|
|
1118
1375
|
|
|
1119
1376
|
// src/version.ts
|
|
@@ -1149,7 +1406,7 @@ var DOC_CATALOG = [
|
|
|
1149
1406
|
path: "/docs/deploy",
|
|
1150
1407
|
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.",
|
|
1151
1408
|
embedded: CODERUNNER_GUIDE,
|
|
1152
|
-
lastUpdated: "2026-10-
|
|
1409
|
+
lastUpdated: "2026-10-08"
|
|
1153
1410
|
},
|
|
1154
1411
|
{
|
|
1155
1412
|
id: "coderunner-runtime",
|
|
@@ -1157,27 +1414,21 @@ var DOC_CATALOG = [
|
|
|
1157
1414
|
path: "/docs/deploy",
|
|
1158
1415
|
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.",
|
|
1159
1416
|
embedded: CODERUNNER_RUNTIME_CONTRACT,
|
|
1160
|
-
lastUpdated: "2026-10-
|
|
1161
|
-
},
|
|
1162
|
-
{
|
|
1163
|
-
id: "coderunner-overview",
|
|
1164
|
-
title: "Coderunner overview",
|
|
1165
|
-
path: "/docs/deploy",
|
|
1166
|
-
summary: "Architecture and lifecycle. Prefer docs_get id coderunner for type usage, Dockerfile, and create prerequisites.",
|
|
1167
|
-
embedded: CODERUNNER_GUIDE,
|
|
1168
|
-
lastUpdated: "2026-10-02"
|
|
1417
|
+
lastUpdated: "2026-10-08"
|
|
1169
1418
|
},
|
|
1170
1419
|
{
|
|
1171
1420
|
id: "authentication",
|
|
1172
1421
|
title: "Workspace Authentication",
|
|
1173
1422
|
path: "/docs/access/app-authentication",
|
|
1174
|
-
summary: "
|
|
1423
|
+
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.",
|
|
1424
|
+
embedded: APP_AUTHENTICATION_GUIDE,
|
|
1425
|
+
lastUpdated: "2026-10-12"
|
|
1175
1426
|
},
|
|
1176
1427
|
{
|
|
1177
1428
|
id: "access",
|
|
1178
1429
|
title: "Access",
|
|
1179
1430
|
path: "/docs/access/site-access",
|
|
1180
|
-
summary: "Grant site roles Data Vault collections, workspace-wide Files, and Apps launch.
|
|
1431
|
+
summary: "Grant site roles Data Vault collections, workspace-wide Files, and Apps launch. The built-in admin role is granted like any other role; every Access PUT is a full replace, so include admin and the existing grants or they are removed. Only builders manage Access; app end users get 403. M2M is not an Access subject. Team membership is unchanged."
|
|
1181
1432
|
},
|
|
1182
1433
|
{
|
|
1183
1434
|
id: "m2m",
|
|
@@ -1197,7 +1448,7 @@ var DOC_CATALOG = [
|
|
|
1197
1448
|
path: "/docs/apps/shell",
|
|
1198
1449
|
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.",
|
|
1199
1450
|
embedded: SHELL_APP_GUIDE,
|
|
1200
|
-
lastUpdated: "2026-
|
|
1451
|
+
lastUpdated: "2026-10-08"
|
|
1201
1452
|
},
|
|
1202
1453
|
{
|
|
1203
1454
|
id: "design-standard",
|
|
@@ -1227,9 +1478,9 @@ var DOC_CATALOG = [
|
|
|
1227
1478
|
id: "vault",
|
|
1228
1479
|
title: "Data Vault",
|
|
1229
1480
|
path: "/docs/api/vault",
|
|
1230
|
-
summary: "Create collections with JSON Schema, query
|
|
1481
|
+
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.",
|
|
1231
1482
|
embedded: VAULT_GUIDE,
|
|
1232
|
-
lastUpdated: "2026-
|
|
1483
|
+
lastUpdated: "2026-10-12"
|
|
1233
1484
|
},
|
|
1234
1485
|
{
|
|
1235
1486
|
id: "feature-flags",
|
|
@@ -1237,7 +1488,7 @@ var DOC_CATALOG = [
|
|
|
1237
1488
|
path: "/docs/configure/feature-flags",
|
|
1238
1489
|
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.",
|
|
1239
1490
|
embedded: FLAGS_GUIDE,
|
|
1240
|
-
lastUpdated: "2026-
|
|
1491
|
+
lastUpdated: "2026-10-08"
|
|
1241
1492
|
},
|
|
1242
1493
|
{
|
|
1243
1494
|
id: "flags-api",
|
|
@@ -1245,7 +1496,7 @@ var DOC_CATALOG = [
|
|
|
1245
1496
|
path: "/docs/api/flags",
|
|
1246
1497
|
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.",
|
|
1247
1498
|
embedded: FLAGS_GUIDE,
|
|
1248
|
-
lastUpdated: "2026-
|
|
1499
|
+
lastUpdated: "2026-10-08"
|
|
1249
1500
|
},
|
|
1250
1501
|
{
|
|
1251
1502
|
id: "scheduled-jobs",
|
|
@@ -1253,12 +1504,19 @@ var DOC_CATALOG = [
|
|
|
1253
1504
|
path: "/docs/deploy/schedules",
|
|
1254
1505
|
summary: "workload_type=schedule plus cron. gf coderunner status shows type/schedule/last_job. Use Process Log, not a public URL.",
|
|
1255
1506
|
embedded: CODERUNNER_GUIDE,
|
|
1256
|
-
lastUpdated: "2026-10-
|
|
1507
|
+
lastUpdated: "2026-10-08"
|
|
1257
1508
|
}
|
|
1258
1509
|
];
|
|
1259
1510
|
function docUrl(entry) {
|
|
1260
1511
|
return `${DOCS_ORIGIN}${entry.path}`;
|
|
1261
1512
|
}
|
|
1513
|
+
var DOC_ALIASES = {
|
|
1514
|
+
"coderunner-overview": "coderunner"
|
|
1515
|
+
};
|
|
1516
|
+
function getDocById(id) {
|
|
1517
|
+
const key = DOC_ALIASES[id] ?? id;
|
|
1518
|
+
return DOC_CATALOG.find((d) => d.id === key);
|
|
1519
|
+
}
|
|
1262
1520
|
function findDoc(query) {
|
|
1263
1521
|
const q = query.trim().toLowerCase();
|
|
1264
1522
|
if (!q) return DOC_CATALOG;
|
|
@@ -1272,7 +1530,7 @@ function htmlToText(html) {
|
|
|
1272
1530
|
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();
|
|
1273
1531
|
}
|
|
1274
1532
|
var MAX_CHARS = 12e3;
|
|
1275
|
-
async function fetchDocPage(entry) {
|
|
1533
|
+
async function fetchDocPage(entry, offset = 0) {
|
|
1276
1534
|
const url = docUrl(entry);
|
|
1277
1535
|
if (entry.embedded?.trim()) {
|
|
1278
1536
|
return {
|
|
@@ -1293,15 +1551,18 @@ async function fetchDocPage(entry) {
|
|
|
1293
1551
|
throw new Error(`Docs fetch failed: ${res.status} ${url}`);
|
|
1294
1552
|
}
|
|
1295
1553
|
const html = await res.text();
|
|
1296
|
-
|
|
1297
|
-
const
|
|
1298
|
-
|
|
1299
|
-
|
|
1554
|
+
const full = htmlToText(html);
|
|
1555
|
+
const start = Math.max(0, Math.min(offset, full.length));
|
|
1556
|
+
const end = start + MAX_CHARS;
|
|
1557
|
+
const truncated = full.length > end;
|
|
1558
|
+
const body = full.slice(start, end) + (truncated ? "\n\u2026" : "");
|
|
1300
1559
|
return {
|
|
1301
1560
|
url,
|
|
1302
1561
|
title: entry.title,
|
|
1303
1562
|
text: body,
|
|
1304
1563
|
truncated,
|
|
1564
|
+
totalChars: full.length,
|
|
1565
|
+
nextOffset: truncated ? end : null,
|
|
1305
1566
|
source: "docs.groundfloor.cloud",
|
|
1306
1567
|
lastUpdated: null,
|
|
1307
1568
|
mcpVersion: GF_MCP_VERSION
|
|
@@ -1309,6 +1570,10 @@ async function fetchDocPage(entry) {
|
|
|
1309
1570
|
}
|
|
1310
1571
|
|
|
1311
1572
|
// src/playbook.ts
|
|
1573
|
+
function createAppCommand(slug, name) {
|
|
1574
|
+
const displayName = name?.trim() || "<App Name>";
|
|
1575
|
+
return `gf apps create --name ${JSON.stringify(displayName)} --slug ${slug || "<portal-slug>"} --kind shell_federated`;
|
|
1576
|
+
}
|
|
1312
1577
|
function suggestCli(intent, args) {
|
|
1313
1578
|
const ws = args?.workspace ? ` -w ${args.workspace}` : "";
|
|
1314
1579
|
const name = args?.name ?? args?.coderunner;
|
|
@@ -1349,6 +1614,14 @@ function suggestCli(intent, args) {
|
|
|
1349
1614
|
why: "Process Log (stdout/stderr). gf deploy prints the last lines on failure; this command fetches more. runner_id accepts a coderunner id.",
|
|
1350
1615
|
docs: "https://docs.groundfloor.cloud/docs/deploy"
|
|
1351
1616
|
};
|
|
1617
|
+
case "create_app": {
|
|
1618
|
+
const slug = args?.slug?.trim();
|
|
1619
|
+
return {
|
|
1620
|
+
command: createAppCommand(slug, args?.name),
|
|
1621
|
+
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).',
|
|
1622
|
+
docs: "https://docs.groundfloor.cloud/docs/apps/shell/getting-started"
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1352
1625
|
case "init_app": {
|
|
1353
1626
|
const slug = args?.slug?.trim();
|
|
1354
1627
|
return {
|
|
@@ -1357,12 +1630,15 @@ function suggestCli(intent, args) {
|
|
|
1357
1630
|
docs: "https://docs.groundfloor.cloud/docs/apps/shell"
|
|
1358
1631
|
};
|
|
1359
1632
|
}
|
|
1360
|
-
case "publish_app":
|
|
1633
|
+
case "publish_app": {
|
|
1634
|
+
const slug = args?.slug?.trim();
|
|
1635
|
+
const dir = slug || "<slug>";
|
|
1361
1636
|
return {
|
|
1362
|
-
command: args?.
|
|
1363
|
-
why: "Ship a Shell federated App.
|
|
1637
|
+
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`,
|
|
1638
|
+
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.",
|
|
1364
1639
|
docs: "https://docs.groundfloor.cloud/docs/apps/shell"
|
|
1365
1640
|
};
|
|
1641
|
+
}
|
|
1366
1642
|
case "environments":
|
|
1367
1643
|
return {
|
|
1368
1644
|
command: `gf environments ls${ws}`,
|
|
@@ -1371,8 +1647,8 @@ function suggestCli(intent, args) {
|
|
|
1371
1647
|
};
|
|
1372
1648
|
case "cancel_deployment":
|
|
1373
1649
|
return {
|
|
1374
|
-
command: name ? `gf coderunner cancel-deployment -c ${name}${ws}` : `gf coderunner cancel-deployment -c <id-or-slug
|
|
1375
|
-
why: "Delete the current deployment (or --deployment-id). Use this for a deploy stuck in deploying.",
|
|
1650
|
+
command: name ? `gf coderunner cancel-deployment -c ${name} --yes${ws}` : `gf coderunner cancel-deployment -c <id-or-slug> --yes${ws}`,
|
|
1651
|
+
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).",
|
|
1376
1652
|
docs: "https://docs.groundfloor.cloud/docs/deploy"
|
|
1377
1653
|
};
|
|
1378
1654
|
case "secrets":
|
|
@@ -1403,7 +1679,7 @@ function suggestCli(intent, args) {
|
|
|
1403
1679
|
default:
|
|
1404
1680
|
return {
|
|
1405
1681
|
command: "gf --help",
|
|
1406
|
-
why: `Unknown intent "${intent}". Use login | switch_workspace | deploy | run | status | logs | environments | cancel_deployment | init_app | publish_app | secrets | flags | dataplane | vault_token.`,
|
|
1682
|
+
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.`,
|
|
1407
1683
|
docs: "https://docs.groundfloor.cloud/docs/developer-tools/cli"
|
|
1408
1684
|
};
|
|
1409
1685
|
}
|
|
@@ -1434,7 +1710,9 @@ function errText(err) {
|
|
|
1434
1710
|
return { content: [{ type: "text", text: message }], isError: true };
|
|
1435
1711
|
}
|
|
1436
1712
|
|
|
1437
|
-
// src/
|
|
1713
|
+
// src/server.ts
|
|
1714
|
+
var READ_ONLY = { readOnlyHint: true };
|
|
1715
|
+
var DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true };
|
|
1438
1716
|
var wsId = z.string().optional().describe("Override the gf default workspace for this call only");
|
|
1439
1717
|
async function cfg(workspaceId, requireWorkspace = true) {
|
|
1440
1718
|
return resolveMcpConfig({ workspaceId, requireWorkspace });
|
|
@@ -1453,6 +1731,7 @@ server.tool(
|
|
|
1453
1731
|
"whoami",
|
|
1454
1732
|
"Show the gf CLI session (identity, cell, selected workspace). Run `gf login` if this fails.",
|
|
1455
1733
|
{},
|
|
1734
|
+
READ_ONLY,
|
|
1456
1735
|
async () => {
|
|
1457
1736
|
try {
|
|
1458
1737
|
const c = await cfg(void 0, false);
|
|
@@ -1476,6 +1755,7 @@ server.tool(
|
|
|
1476
1755
|
"workspace_context",
|
|
1477
1756
|
"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.",
|
|
1478
1757
|
{ workspace_id: wsId },
|
|
1758
|
+
READ_ONLY,
|
|
1479
1759
|
async ({ workspace_id }) => {
|
|
1480
1760
|
try {
|
|
1481
1761
|
const c = await cfg(workspace_id);
|
|
@@ -1535,12 +1815,16 @@ server.tool(
|
|
|
1535
1815
|
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.",
|
|
1536
1816
|
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.'
|
|
1537
1817
|
},
|
|
1818
|
+
app_end_users: {
|
|
1819
|
+
docs_get: "authentication",
|
|
1820
|
+
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."
|
|
1821
|
+
},
|
|
1538
1822
|
shell_app_howto: {
|
|
1539
1823
|
docs_get: "shell",
|
|
1540
|
-
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.",
|
|
1824
|
+
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.",
|
|
1541
1825
|
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.",
|
|
1542
1826
|
design: "docs_get id design-standard",
|
|
1543
|
-
suggest_cli: "init_app | publish_app"
|
|
1827
|
+
suggest_cli: "create_app | init_app | publish_app"
|
|
1544
1828
|
},
|
|
1545
1829
|
databases_howto: {
|
|
1546
1830
|
docs_get: "databases",
|
|
@@ -1559,6 +1843,7 @@ server.tool(
|
|
|
1559
1843
|
flags: suggestCli("flags"),
|
|
1560
1844
|
cancel_deployment: suggestCli("cancel_deployment"),
|
|
1561
1845
|
vault_token: suggestCli("m2m_token"),
|
|
1846
|
+
create_app: suggestCli("create_app"),
|
|
1562
1847
|
init_app: suggestCli("init_app"),
|
|
1563
1848
|
publish_app: suggestCli("publish_app")
|
|
1564
1849
|
},
|
|
@@ -1578,6 +1863,7 @@ server.tool(
|
|
|
1578
1863
|
"workspaces_list",
|
|
1579
1864
|
"List workspaces visible to the current user. To change the default, tell the user to run `gf workspaces use <uuid>` (CLI owns that).",
|
|
1580
1865
|
{},
|
|
1866
|
+
READ_ONLY,
|
|
1581
1867
|
async () => {
|
|
1582
1868
|
try {
|
|
1583
1869
|
const c = await cfg(void 0, false);
|
|
@@ -1599,6 +1885,7 @@ server.tool(
|
|
|
1599
1885
|
"coderunner_list",
|
|
1600
1886
|
"List Coderunners in the workspace (read-only).",
|
|
1601
1887
|
{ workspace_id: wsId },
|
|
1888
|
+
READ_ONLY,
|
|
1602
1889
|
async ({ workspace_id }) => {
|
|
1603
1890
|
try {
|
|
1604
1891
|
const c = await cfg(workspace_id);
|
|
@@ -1616,6 +1903,7 @@ server.tool(
|
|
|
1616
1903
|
workspace_id: wsId,
|
|
1617
1904
|
coderunner_id: z.string().min(1).describe("Coderunner id or slug")
|
|
1618
1905
|
},
|
|
1906
|
+
READ_ONLY,
|
|
1619
1907
|
async (args) => {
|
|
1620
1908
|
try {
|
|
1621
1909
|
const c = await cfg(args.workspace_id);
|
|
@@ -1645,6 +1933,7 @@ server.tool(
|
|
|
1645
1933
|
search: z.string().optional(),
|
|
1646
1934
|
deployment_id: z.string().optional()
|
|
1647
1935
|
},
|
|
1936
|
+
READ_ONLY,
|
|
1648
1937
|
async (args) => {
|
|
1649
1938
|
try {
|
|
1650
1939
|
const c = await cfg(args.workspace_id);
|
|
@@ -1671,6 +1960,7 @@ server.tool(
|
|
|
1671
1960
|
"environments_list",
|
|
1672
1961
|
"List environments in the workspace (id, name, status).",
|
|
1673
1962
|
{ workspace_id: wsId },
|
|
1963
|
+
READ_ONLY,
|
|
1674
1964
|
async ({ workspace_id }) => {
|
|
1675
1965
|
try {
|
|
1676
1966
|
const c = await cfg(workspace_id);
|
|
@@ -1686,30 +1976,23 @@ server.tool(
|
|
|
1686
1976
|
);
|
|
1687
1977
|
server.tool(
|
|
1688
1978
|
"coderunner_cancel_deployment",
|
|
1689
|
-
"
|
|
1979
|
+
"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.",
|
|
1690
1980
|
{
|
|
1691
|
-
workspace_id: wsId,
|
|
1692
1981
|
coderunner_id: z.string().min(1).describe("Coderunner id or slug"),
|
|
1693
|
-
deployment_id: z.string().
|
|
1982
|
+
deployment_id: z.string().trim().min(1).describe(
|
|
1983
|
+
"Id of the deployment to delete (coderunner_get_status \u2192 deployment.id). Required: there is no default."
|
|
1984
|
+
)
|
|
1694
1985
|
},
|
|
1986
|
+
DESTRUCTIVE,
|
|
1695
1987
|
async (args) => {
|
|
1696
1988
|
try {
|
|
1697
|
-
const c = await cfg(
|
|
1989
|
+
const c = await cfg();
|
|
1698
1990
|
const id = await resolveCoderunnerId(
|
|
1699
1991
|
c.apiUrl,
|
|
1700
1992
|
c.workspaceId,
|
|
1701
1993
|
args.coderunner_id
|
|
1702
1994
|
);
|
|
1703
|
-
|
|
1704
|
-
if (!deploymentId) {
|
|
1705
|
-
const current = await getDeployment(c.apiUrl, c.workspaceId, id);
|
|
1706
|
-
deploymentId = current?.id?.trim();
|
|
1707
|
-
}
|
|
1708
|
-
if (!deploymentId) {
|
|
1709
|
-
throw new Error(
|
|
1710
|
-
`No current deployment for coderunner ${id}. Pass deployment_id.`
|
|
1711
|
-
);
|
|
1712
|
-
}
|
|
1995
|
+
const deploymentId = args.deployment_id;
|
|
1713
1996
|
await deleteCoderunnerDeployment(
|
|
1714
1997
|
c.apiUrl,
|
|
1715
1998
|
c.workspaceId,
|
|
@@ -1717,6 +2000,7 @@ server.tool(
|
|
|
1717
2000
|
deploymentId
|
|
1718
2001
|
);
|
|
1719
2002
|
return text({
|
|
2003
|
+
workspace_id: c.workspaceId,
|
|
1720
2004
|
coderunner_id: id,
|
|
1721
2005
|
deployment_id: deploymentId,
|
|
1722
2006
|
deleted: true,
|
|
@@ -1735,6 +2019,7 @@ server.tool(
|
|
|
1735
2019
|
coderunner_id: z.string().min(1),
|
|
1736
2020
|
job_id: z.string().min(1)
|
|
1737
2021
|
},
|
|
2022
|
+
READ_ONLY,
|
|
1738
2023
|
async (args) => {
|
|
1739
2024
|
try {
|
|
1740
2025
|
const c = await cfg(args.workspace_id);
|
|
@@ -1753,6 +2038,7 @@ server.tool(
|
|
|
1753
2038
|
"apps_list",
|
|
1754
2039
|
"List product Apps (Shell / standalone). Workloads are Coderunners, not Apps.",
|
|
1755
2040
|
{ workspace_id: wsId },
|
|
2041
|
+
READ_ONLY,
|
|
1756
2042
|
async ({ workspace_id }) => {
|
|
1757
2043
|
try {
|
|
1758
2044
|
const c = await cfg(workspace_id);
|
|
@@ -1766,6 +2052,7 @@ server.tool(
|
|
|
1766
2052
|
"apps_get",
|
|
1767
2053
|
"Get one product App by id or slug.",
|
|
1768
2054
|
{ workspace_id: wsId, app_id: z.string().min(1) },
|
|
2055
|
+
READ_ONLY,
|
|
1769
2056
|
async (args) => {
|
|
1770
2057
|
try {
|
|
1771
2058
|
const c = await cfg(args.workspace_id);
|
|
@@ -1783,6 +2070,7 @@ server.tool(
|
|
|
1783
2070
|
"secrets_list",
|
|
1784
2071
|
"List secret keys (values are not shown). To set/delete, use `gf secrets`.",
|
|
1785
2072
|
{ workspace_id: wsId, environment_id: z.string().optional() },
|
|
2073
|
+
READ_ONLY,
|
|
1786
2074
|
async (args) => {
|
|
1787
2075
|
try {
|
|
1788
2076
|
const c = await cfg(args.workspace_id);
|
|
@@ -1798,6 +2086,7 @@ server.tool(
|
|
|
1798
2086
|
"flags_list",
|
|
1799
2087
|
"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.",
|
|
1800
2088
|
{ workspace_id: wsId, environment_id: z.string().optional() },
|
|
2089
|
+
READ_ONLY,
|
|
1801
2090
|
async (args) => {
|
|
1802
2091
|
try {
|
|
1803
2092
|
const c = await cfg(args.workspace_id);
|
|
@@ -1813,6 +2102,7 @@ server.tool(
|
|
|
1813
2102
|
"files_list",
|
|
1814
2103
|
"List workspace files (read-only). Upload/download with `gf files`.",
|
|
1815
2104
|
{ workspace_id: wsId },
|
|
2105
|
+
READ_ONLY,
|
|
1816
2106
|
async ({ workspace_id }) => {
|
|
1817
2107
|
try {
|
|
1818
2108
|
const c = await cfg(workspace_id);
|
|
@@ -1824,17 +2114,45 @@ server.tool(
|
|
|
1824
2114
|
);
|
|
1825
2115
|
server.tool(
|
|
1826
2116
|
"domains_list",
|
|
1827
|
-
|
|
1828
|
-
{
|
|
2117
|
+
'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.',
|
|
2118
|
+
{
|
|
2119
|
+
workspace_id: wsId,
|
|
2120
|
+
app_id: z.string().min(1).optional().describe("App id or slug"),
|
|
2121
|
+
coderunner_id: z.string().min(1).optional().describe("Coderunner id or slug; resolves to the App whose primary Coderunner it is")
|
|
2122
|
+
},
|
|
2123
|
+
READ_ONLY,
|
|
1829
2124
|
async (args) => {
|
|
1830
2125
|
try {
|
|
2126
|
+
if (!args.app_id && !args.coderunner_id) {
|
|
2127
|
+
throw new Error("Pass app_id (id or slug) or coderunner_id.");
|
|
2128
|
+
}
|
|
1831
2129
|
const c = await cfg(args.workspace_id);
|
|
1832
|
-
const
|
|
2130
|
+
const listed = await listApps(c.apiUrl, c.workspaceId);
|
|
2131
|
+
let match;
|
|
2132
|
+
if (args.app_id) {
|
|
2133
|
+
match = listed.apps.find(
|
|
2134
|
+
(app) => app.id === args.app_id || app.slug === args.app_id
|
|
2135
|
+
);
|
|
2136
|
+
if (!match) throw new Error(`App not found: ${args.app_id}`);
|
|
2137
|
+
} else {
|
|
2138
|
+
const crId = await resolveCoderunnerId(
|
|
2139
|
+
c.apiUrl,
|
|
2140
|
+
c.workspaceId,
|
|
2141
|
+
args.coderunner_id
|
|
2142
|
+
);
|
|
2143
|
+
match = listed.apps.find((app) => app.primary_coderunner_id === crId);
|
|
2144
|
+
if (!match) {
|
|
2145
|
+
throw new Error(
|
|
2146
|
+
`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>).`
|
|
2147
|
+
);
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
const domains = await cpGetJson(
|
|
1833
2151
|
c.apiUrl,
|
|
1834
|
-
c.
|
|
1835
|
-
|
|
2152
|
+
c.token,
|
|
2153
|
+
`/v1/workspaces/${encodeURIComponent(c.workspaceId)}/apps/${encodeURIComponent(match.id)}/coderunner/domains`
|
|
1836
2154
|
);
|
|
1837
|
-
return text(
|
|
2155
|
+
return text({ app_id: match.id, app_slug: match.slug, ...domains });
|
|
1838
2156
|
} catch (e) {
|
|
1839
2157
|
return errText(e);
|
|
1840
2158
|
}
|
|
@@ -1842,8 +2160,9 @@ server.tool(
|
|
|
1842
2160
|
);
|
|
1843
2161
|
server.tool(
|
|
1844
2162
|
"dataplane_status",
|
|
1845
|
-
"Dataplane setup status
|
|
2163
|
+
"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.",
|
|
1846
2164
|
{ workspace_id: wsId },
|
|
2165
|
+
READ_ONLY,
|
|
1847
2166
|
async ({ workspace_id }) => {
|
|
1848
2167
|
try {
|
|
1849
2168
|
const c = await cfg(workspace_id);
|
|
@@ -1853,10 +2172,73 @@ server.tool(
|
|
|
1853
2172
|
}
|
|
1854
2173
|
}
|
|
1855
2174
|
);
|
|
2175
|
+
var accountId = z.string().optional().describe(
|
|
2176
|
+
"Organization/account id (NOT the workspace id). Defaults to the account that owns the selected workspace."
|
|
2177
|
+
);
|
|
2178
|
+
async function resolveAccountId(c, explicit) {
|
|
2179
|
+
if (explicit?.trim()) return explicit.trim();
|
|
2180
|
+
const workspaces = await listWorkspaces(c.apiUrl);
|
|
2181
|
+
const ws = workspaces.find((w) => w.id === c.workspaceId);
|
|
2182
|
+
const id = ws?.account_id || ws?.organization_id;
|
|
2183
|
+
if (!id) {
|
|
2184
|
+
throw new Error(
|
|
2185
|
+
"Could not determine the account for the selected workspace. Pass account_id (organization id)."
|
|
2186
|
+
);
|
|
2187
|
+
}
|
|
2188
|
+
return id;
|
|
2189
|
+
}
|
|
2190
|
+
server.tool(
|
|
2191
|
+
"quota_get",
|
|
2192
|
+
"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.",
|
|
2193
|
+
{ workspace_id: wsId, account_id: accountId },
|
|
2194
|
+
READ_ONLY,
|
|
2195
|
+
async (args) => {
|
|
2196
|
+
try {
|
|
2197
|
+
const c = await cfg(args.workspace_id);
|
|
2198
|
+
const id = await resolveAccountId(c, args.account_id);
|
|
2199
|
+
return text(
|
|
2200
|
+
await cpGetJson(
|
|
2201
|
+
c.apiUrl,
|
|
2202
|
+
c.token,
|
|
2203
|
+
`/v1/accounts/${encodeURIComponent(id)}/quota`
|
|
2204
|
+
)
|
|
2205
|
+
);
|
|
2206
|
+
} catch (e) {
|
|
2207
|
+
return errText(e);
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
);
|
|
2211
|
+
server.tool(
|
|
2212
|
+
"billing_summary",
|
|
2213
|
+
"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.",
|
|
2214
|
+
{
|
|
2215
|
+
workspace_id: wsId,
|
|
2216
|
+
account_id: accountId,
|
|
2217
|
+
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)")
|
|
2218
|
+
},
|
|
2219
|
+
READ_ONLY,
|
|
2220
|
+
async (args) => {
|
|
2221
|
+
try {
|
|
2222
|
+
const c = await cfg(args.workspace_id);
|
|
2223
|
+
const id = await resolveAccountId(c, args.account_id);
|
|
2224
|
+
const qs = args.since_seconds ? `?since_seconds=${args.since_seconds}` : "";
|
|
2225
|
+
return text(
|
|
2226
|
+
await cpGetJson(
|
|
2227
|
+
c.apiUrl,
|
|
2228
|
+
c.token,
|
|
2229
|
+
`/v1/accounts/${encodeURIComponent(id)}/billing/summary${qs}`
|
|
2230
|
+
)
|
|
2231
|
+
);
|
|
2232
|
+
} catch (e) {
|
|
2233
|
+
return errText(e);
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
);
|
|
1856
2237
|
server.tool(
|
|
1857
2238
|
"docs_list",
|
|
1858
2239
|
"Catalog of Groundfloor product docs (docs.groundfloor.cloud) the MCP can fetch.",
|
|
1859
2240
|
{ query: z.string().optional() },
|
|
2241
|
+
READ_ONLY,
|
|
1860
2242
|
async ({ query }) => {
|
|
1861
2243
|
const hits = findDoc(query ?? "");
|
|
1862
2244
|
return text({
|
|
@@ -1874,15 +2256,19 @@ server.tool(
|
|
|
1874
2256
|
);
|
|
1875
2257
|
server.tool(
|
|
1876
2258
|
"docs_get",
|
|
1877
|
-
"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.",
|
|
2259
|
+
"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.",
|
|
1878
2260
|
{
|
|
1879
2261
|
id: z.string().min(1).describe(
|
|
1880
2262
|
"Catalog id, e.g. coderunner | coderunner-runtime | shell | design-standard | css-and-styling | databases | agents | developers | authentication | access | m2m"
|
|
2263
|
+
),
|
|
2264
|
+
offset: z.number().int().min(0).optional().describe(
|
|
2265
|
+
"Live (non-embedded) pages only: character offset to resume from. Pass nextOffset from the previous result when truncated is true."
|
|
1881
2266
|
)
|
|
1882
2267
|
},
|
|
1883
|
-
|
|
2268
|
+
READ_ONLY,
|
|
2269
|
+
async ({ id, offset }) => {
|
|
1884
2270
|
try {
|
|
1885
|
-
const entry =
|
|
2271
|
+
const entry = getDocById(id);
|
|
1886
2272
|
if (!entry) {
|
|
1887
2273
|
return errText(
|
|
1888
2274
|
new Error(
|
|
@@ -1890,7 +2276,7 @@ server.tool(
|
|
|
1890
2276
|
)
|
|
1891
2277
|
);
|
|
1892
2278
|
}
|
|
1893
|
-
return text(await fetchDocPage(entry));
|
|
2279
|
+
return text(await fetchDocPage(entry, offset ?? 0));
|
|
1894
2280
|
} catch (e) {
|
|
1895
2281
|
return errText(e);
|
|
1896
2282
|
}
|
|
@@ -1898,7 +2284,7 @@ server.tool(
|
|
|
1898
2284
|
);
|
|
1899
2285
|
server.tool(
|
|
1900
2286
|
"suggest_cli",
|
|
1901
|
-
"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.",
|
|
2287
|
+
"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.",
|
|
1902
2288
|
{
|
|
1903
2289
|
intent: z.enum([
|
|
1904
2290
|
"login",
|
|
@@ -1909,6 +2295,7 @@ server.tool(
|
|
|
1909
2295
|
"logs",
|
|
1910
2296
|
"environments",
|
|
1911
2297
|
"cancel_deployment",
|
|
2298
|
+
"create_app",
|
|
1912
2299
|
"init_app",
|
|
1913
2300
|
"publish_app",
|
|
1914
2301
|
"secrets",
|
|
@@ -1918,10 +2305,11 @@ server.tool(
|
|
|
1918
2305
|
"m2m_token"
|
|
1919
2306
|
]),
|
|
1920
2307
|
workspace: z.string().optional(),
|
|
1921
|
-
name: z.string().optional().describe("Coderunner name/id for deploy/run/status/logs/cancel_deployment/vault_token"),
|
|
1922
|
-
slug: z.string().optional().describe("Portal app slug for init_app / publish_app"),
|
|
2308
|
+
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"),
|
|
2309
|
+
slug: z.string().optional().describe("Portal app slug for create_app / init_app / publish_app"),
|
|
1923
2310
|
cell: z.enum(["production", "stage", "dev"]).optional()
|
|
1924
2311
|
},
|
|
2312
|
+
READ_ONLY,
|
|
1925
2313
|
async (args) => {
|
|
1926
2314
|
return text(
|
|
1927
2315
|
suggestCli(args.intent, {
|
|
@@ -1934,6 +2322,8 @@ server.tool(
|
|
|
1934
2322
|
);
|
|
1935
2323
|
}
|
|
1936
2324
|
);
|
|
2325
|
+
|
|
2326
|
+
// src/index.ts
|
|
1937
2327
|
async function main() {
|
|
1938
2328
|
const transport = new StdioServerTransport();
|
|
1939
2329
|
await server.connect(transport);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@groundfloorcloud/mcp",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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",
|