@groundfloorcloud/mcp 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +144 -8
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -82,7 +82,7 @@ Use the prefixed names: `GROUNDFLOOR_API_URL`, `GROUNDFLOOR_WORKSPACE_ID`, `GROU
82
82
  | `secrets_list` / `files_list` / `dataplane_status` | Inspect (no secrets values) |
83
83
  | `domains_list` | Custom domains of an App's Coderunner (`app_id` or `coderunner_id`). Domains exist on Apps, not standalone Coderunners |
84
84
  | `quota_get` / `billing_summary` | Plan quota and billing for the workspace's **account** (account id is not the workspace id; billing needs organization Owner, Admin 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) |
85
+ | `docs_list` / `docs_get` | Product docs (`coderunner`, `coderunner-runtime`, `shell`, `design-standard`, `css-and-styling`, `databases`, `vault`, `feature-flags`, `authentication`, `service-access` are embedded; `access` and `m2m` are live pages, with an `offset` parameter to read past the 12,000-character page limit) |
86
86
  | `suggest_cli` | Command to run: login, deploy, create_app, init_app, publish_app, m2m_token, … |
87
87
 
88
88
  Never create `app_kind=coderunner`. Deploy with `gf deploy`.
package/dist/index.js CHANGED
@@ -627,8 +627,12 @@ with no role at all gets 403 too.
627
627
  ## In a Shell app
628
628
 
629
629
  The signed-in user's JWT comes from \`window.__GF_GET_ACCESS_TOKEN__()\`. See
630
- \`docs_get\` id \`shell\`. Unattended workloads use M2M instead (\`docs_get\` id
631
- \`m2m\`), not an end-user login.
630
+ \`docs_get\` id \`shell\`. Roles are in the token's \`realm_access.roles\`
631
+ (Groundfloor mode). A Shell app's backend Service is public and must verify
632
+ that token itself (issuer and client ID from \`/v1/public/workspaces/by-host\`,
633
+ JWKS, RS256): \`docs_get\` id \`service-access\`.
634
+ Unattended workloads use M2M instead (\`docs_get\` id \`m2m\`), not an
635
+ end-user login.
632
636
  `.trim();
633
637
 
634
638
  // src/coderunner-runtime.ts
@@ -868,6 +872,11 @@ CMD ["npm", "start"]
868
872
 
869
873
  - Services follow the \`npm start\` / \`PORT\` contract: Node needs \`scripts.start\` if CMD is \`npm start\`.
870
874
  - Prefer curling the Deployment URL (\`/health\`) over \`\u2026/run\`.
875
+ - **The Deployment URL (and any App domain on it) is public.** Groundfloor
876
+ checks no identity. A Service that is a Shell app's backend must verify the
877
+ user's token itself, and allow the Shell origin for CORS. Off-the-shelf
878
+ images (port 80 defaults, MySQL, local uploads, sessions): \`docs_get\` id
879
+ \`service-access\`.
871
880
 
872
881
  ## Latest deploy rules (all types)
873
882
 
@@ -965,10 +974,14 @@ An M2M client can also read secret **values** directly at runtime instead
965
974
  of (or alongside) \`--secret\` deploy-time injection: grant it the
966
975
  **\`secrets:read\`** scope (same Portal **Administer \u2192 M2M tokens** screen,
967
976
  per-client toggle), then call
968
- \`GET {CONTROLPLANE_URL}/v1/workspaces/{workspace_id}/secrets/{key}\` with
969
- \`Authorization: Bearer\` from \`gf m2m token\` locally, or the deployed
977
+ \`GET {CONTROLPLANE_URL}/v1/workspaces/{workspace_id}/secrets/{key}?environment_id=$GROUNDFLOOR_ENVIRONMENT_ID\`
978
+ with \`Authorization: Bearer\` from \`gf m2m token\` locally, or the deployed
970
979
  workload's injected \`GROUNDFLOOR_M2M_CLIENT_ID\`/\`GROUNDFLOOR_M2M_CLIENT_SECRET\`
971
- exchanged the same way. This path is:
980
+ exchanged the same way. **Always pass \`environment_id\`**
981
+ (\`GROUNDFLOOR_ENVIRONMENT_ID\` is injected on deploy for a Coderunner bound to
982
+ an environment) to choose which environment's value is read: omitted or
983
+ empty, it resolves to the workspace's \`dev\` environment (or its only active
984
+ one), so a prod workload silently reads dev secrets. This path is:
972
985
 
973
986
  - **Read-only** \u2014 \`secrets:read\` never grants \`upsert\`/\`delete\`, regardless
974
987
  of the client's writer relation on Vault/Files.
@@ -977,7 +990,9 @@ exchanged the same way. This path is:
977
990
  - **Workspace-wide, not per-key or per-environment** \u2014 granting the scope
978
991
  exposes every secret in the workspace across all environments to that
979
992
  client's code, same class of trade as the existing Vault/Files writer
980
- grant. Not a fit for isolating one secret to one Coderunner.
993
+ grant. \`environment_id\` only selects which environment's value is
994
+ returned; it is not an access boundary. Not a fit for isolating one secret
995
+ (or one environment) to one Coderunner.
981
996
 
982
997
  Prefer it over \`--secret\` injection when you want live rotation and can
983
998
  do the fetch yourself; keep using \`--secret\` injection for SDKs that only
@@ -1258,6 +1273,113 @@ active environments \xB7 400 bad or reserved key \xB7 422 value/value_type misma
1258
1273
  423 workspace frozen (writes).
1259
1274
  `.trim();
1260
1275
 
1276
+ // src/service-access-guide.ts
1277
+ var SERVICE_ACCESS_GUIDE = `
1278
+ # Who can reach a Service (and checking Shell tokens in a backend)
1279
+
1280
+ ## The rule
1281
+
1282
+ Every Coderunner Service is public. Its Deployment URL, and an App's domain
1283
+ routed to it, accept requests from anyone. Groundfloor routes the request but
1284
+ signs nobody in and checks no token. The Service decides who gets in.
1285
+
1286
+ | Service is\u2026 | Example | Do |
1287
+ |---|---|---|
1288
+ | Open to everyone | website, docs, public API, webhook receiver | Nothing (default) |
1289
+ | An app with its own logins | WordPress, Ghost, Metabase | Nothing from Groundfloor; its own login applies. Admin pages (\`/wp-admin\`) are public too |
1290
+ | Backend for a Shell app | API the Shell app calls with \`__GF_GET_ACCESS_TOKEN__()\` | Verify the user's token on **every** request |
1291
+
1292
+ Never tell a user that Shell sign-in, App authentication or Site access
1293
+ protects a Service. Anyone can call the Service directly.
1294
+
1295
+ ## Verifying a Shell user's token in a backend
1296
+
1297
+ 1. **Issuer and client ID**: \`GET {CONTROLPLANE_URL}/v1/public/workspaces/by-host?host=<shell-site-host>\`
1298
+ (unauthenticated) \u2192 \`{ workspace_id, mode, oidc: { issuer, client_id } | null, site_host }\`.
1299
+ Check \`workspace_id\` is the user's workspace. \`oidc\` is null in mode
1300
+ \`none\`. Groundfloor mode: per-workspace issuer, \`client_id\` \`shell\`.
1301
+ External mode: the provider's issuer and the client ID the customer
1302
+ entered. Look it up **once at setup**, never at runtime from the request's
1303
+ \`Host\` header (caller-controlled). Do not hard-code the issuer host (it
1304
+ differs per cell); copy the issuer **exactly, including any trailing \`/\`**
1305
+ (do not strip it before comparing \`iss\`) into non-secret env such as
1306
+ \`AUTH_ISSUER\` / \`AUTH_CLIENT_ID\` (Dockerfile \`ENV\`, since deploy-time
1307
+ env is not kept between deploys).
1308
+ 2. **Checks** (401 if any fails): RS256 signature against
1309
+ \`{issuer}/protocol/openid-connect/certs\` (External: the provider's
1310
+ \`jwks_uri\`), key chosen by the token's \`kid\` (the JWKS has several
1311
+ keys); \`iss\` equals the issuer exactly; \`exp\` present and in the future;
1312
+ the token was issued to the client ID:
1313
+ - Groundfloor mode: \`azp\` == client ID. Do not require an app-specific
1314
+ \`aud\` (none exists yet; tokens often carry \`aud: "account"\`, so with
1315
+ PyJWT set \`verify_aud: False\` and check the client yourself).
1316
+ - External mode: the client ID must appear in \`azp\` (or the provider's
1317
+ equivalent, e.g. \`cid\`, \`appid\`) **or** in \`aud\`. Never accept a
1318
+ token naming it in neither. If the provider allows open sign-up, "any
1319
+ signed-in user" means anyone: require a role.
1320
+ 403 when the token is valid but the role (or tenant) is wrong.
1321
+ 3. **Claims**: roles in \`realm_access.roles\` (built-in \`viewer\`/\`user\`/
1322
+ \`admin\` plus custom roles; ignore \`offline_access\`, \`uma_authorization\`,
1323
+ \`default-roles-*\`). Treat a missing or malformed \`realm_access\`/\`roles\`
1324
+ (not an object / not a list) as no roles \u2192 403, never a 500, and match
1325
+ whole role strings only. External mode: roles come from the customer's
1326
+ provider, in its own claim. For the user ID claim, follow the docs page
1327
+ /docs/apps/shell/sign-in#know-who-the-user-is.
1328
+ **Multi-tenant**: roles apply across every company (an \`admin\` in one
1329
+ company is admin everywhere), so filter every read and write by the
1330
+ \`organization\` claim too. It can hold more than one company: check the
1331
+ record's company is in it. Its exact form is unconfirmed; accept a list of
1332
+ names or an object keyed by name, anything else = no companies.
1333
+ 4. **Libraries**: Python PyJWT \`PyJWKClient(JWKS_URL, timeout=5)\` (it
1334
+ refetches the JWKS on every unknown \`kid\`, so keep the timeout short) +
1335
+ \`jwt.decode(token, key.key, algorithms=["RS256"], issuer=ISSUER,
1336
+ options={"require": ["exp", "iss"], "verify_aud": False})\`, then
1337
+ \`azp == CLIENT_ID\` (External: or \`cid\`/\`appid\`/\`aud\`), as a FastAPI
1338
+ dependency plus a \`require_role()\` dependency for 403. Node: \`jose\`
1339
+ \`createRemoteJWKSet\` + \`jwtVerify(token, JWKS, { issuer, algorithms:
1340
+ ["RS256"], requiredClaims: ["exp"] })\`, then the same client check; fail at
1341
+ startup if \`AUTH_ISSUER\` or \`AUTH_CLIENT_ID\` is unset (an undefined
1342
+ issuer disables jose's \`iss\` check). Full examples on the docs page.
1343
+ 5. **CORS**: the Shell site and the Service are different origins, so the
1344
+ browser preflights calls carrying \`Authorization\`. Allow the Shell site
1345
+ origin (and its custom domain), methods, and the \`Authorization\` and
1346
+ \`Content-Type\` headers (FastAPI \`CORSMiddleware\`; Express
1347
+ \`cors({ origin: ["https://<shell-site>"], allowedHeaders: ["Authorization", "Content-Type"] })\`,
1348
+ never bare \`cors()\`, which allows every origin), so preflight is answered
1349
+ before the token check. Never \`*\` with credentials; the bearer header
1350
+ needs no credentials mode.
1351
+
1352
+ ## Every app on the Shell site can use the user's token
1353
+
1354
+ Every app on a Shell site, including apps the customer didn't write, can get
1355
+ the signed-in user's token and call the customer's backends as that user, and
1356
+ a token from one app is accepted by every other app's backend in the
1357
+ workspace. Tell users to install only apps they trust and to check roles for
1358
+ anything sensitive. Per-app tokens, sign-in settings injected into the
1359
+ Service, and a verify helper are planned (no date). Do not promise per-app
1360
+ isolation.
1361
+
1362
+ ## Running off-the-shelf images as a Service
1363
+
1364
+ - Listen on 8080 (or \`PORT\`) on 0.0.0.0; many images default to 80, so
1365
+ reconfigure in the Dockerfile.
1366
+ - Managed databases are PostgreSQL only; MySQL/MariaDB/MongoDB apps
1367
+ (WordPress) need an external database.
1368
+ - **Finish the installer immediately**: apps with a first-visit installer
1369
+ (WordPress) can be claimed by anyone who opens the URL until it's completed.
1370
+ Tell the user to complete it as soon as the first deploy finishes.
1371
+ - Read secrets in an entrypoint script: pass the M2M credentials to curl on
1372
+ stdin (not argv), fail closed if the value is empty, \`unset\` the M2M
1373
+ credentials and token before \`exec\`ing the app.
1374
+ - Local disk is lost on restart, redeploy and scale-to-zero, and is per
1375
+ instance. Use the app's S3/object-storage plugin against external storage;
1376
+ Groundfloor Files is an API for code the customer writes, not an S3 endpoint.
1377
+ - More than one instance breaks in-memory/on-disk sessions; keep one instance
1378
+ or store sessions in the database. Set min instances \u2265 1 so the first visit
1379
+ doesn't wait.
1380
+ - Only the app's own login applies; Groundfloor sign-in can't front it today.
1381
+ `.trim();
1382
+
1261
1383
  // src/shell-guide.ts
1262
1384
  var SHELL_APP_GUIDE = `
1263
1385
  # Shell federated apps
@@ -1305,6 +1427,12 @@ outside Shell) so the breadcrumb does not show the raw id. Give that route
1305
1427
  routes (omit it or use \`false\` otherwise \u2014 never \`null\`). \`visibility:
1306
1428
  "public"\` makes a route reachable without login.
1307
1429
 
1430
+ Backends: secrets and server-side work go in a Coderunner Service the app
1431
+ calls with \`Authorization: Bearer \${await window.__GF_GET_ACCESS_TOKEN__()}\`.
1432
+ Shell sign-in does **not** protect that Service: its URL is public, so it must
1433
+ verify the token on every request and allow the Shell origin for CORS
1434
+ (\`docs_get\` id \`service-access\`).
1435
+
1308
1436
  ## suggest_cli
1309
1437
 
1310
1438
  - 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)
@@ -1513,6 +1641,14 @@ var DOC_CATALOG = [
1513
1641
  embedded: CODERUNNER_RUNTIME_CONTRACT,
1514
1642
  lastUpdated: "2026-10-08"
1515
1643
  },
1644
+ {
1645
+ id: "service-access",
1646
+ title: "Who can reach your Service",
1647
+ path: "/docs/deploy/securing-a-service",
1648
+ summary: "Every Service (Deployment URL or App domain) is public; Groundfloor signs nobody in and checks no token. Open sites and apps with their own logins need nothing (finish installers like WordPress immediately); a Shell app's backend must verify the user's JWT on every request: issuer and client_id copied once from GET /v1/public/workspaces/by-host (never from the request Host header), RS256 via the issuer's JWKS by kid, exact iss, exp, azp == client_id in Groundfloor mode (External: azp/cid/appid or aud), no app aud yet; roles in realm_access.roles (malformed = no roles = 403); with Multi-tenant also filter by organization. CORS for the Shell origin. Every app on the Shell site can use the user's token against any of the workspace's backends until per-app tokens ship. Off-the-shelf images: port 8080, PostgreSQL only, ephemeral disk, sessions, min instances.",
1649
+ embedded: SERVICE_ACCESS_GUIDE,
1650
+ lastUpdated: "2026-10-14"
1651
+ },
1516
1652
  {
1517
1653
  id: "authentication",
1518
1654
  title: "Workspace Authentication",
@@ -1545,7 +1681,7 @@ var DOC_CATALOG = [
1545
1681
  path: "/docs/apps/shell",
1546
1682
  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.",
1547
1683
  embedded: SHELL_APP_GUIDE,
1548
- lastUpdated: "2026-10-08"
1684
+ lastUpdated: "2026-10-14"
1549
1685
  },
1550
1686
  {
1551
1687
  id: "design-standard",
@@ -2356,7 +2492,7 @@ server.tool(
2356
2492
  "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.",
2357
2493
  {
2358
2494
  id: z.string().min(1).describe(
2359
- "Catalog id, e.g. coderunner | coderunner-runtime | shell | design-standard | css-and-styling | databases | agents | developers | authentication | access | m2m"
2495
+ "Catalog id, e.g. coderunner | coderunner-runtime | service-access | shell | design-standard | css-and-styling | databases | agents | developers | authentication | access | m2m"
2360
2496
  ),
2361
2497
  offset: z.number().int().min(0).optional().describe(
2362
2498
  "Live (non-embedded) pages only: character offset to resume from. Pass nextOffset from the previous result when truncated is true."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groundfloorcloud/mcp",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Groundfloor MCP — inspect workspace state and docs; take actions with the gf CLI.",
5
5
  "type": "module",
6
6
  "bin": {