@groundfloorcloud/mcp 0.2.4 → 0.2.5
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/dist/index.js +126 -13
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -179,12 +179,14 @@ async function getValidAccessToken() {
|
|
|
179
179
|
|
|
180
180
|
// ../cli/src/cp.ts
|
|
181
181
|
var CpError = class extends Error {
|
|
182
|
-
constructor(status, message) {
|
|
182
|
+
constructor(status, message, reason = null) {
|
|
183
183
|
super(message);
|
|
184
184
|
this.status = status;
|
|
185
|
+
this.reason = reason;
|
|
185
186
|
this.name = "CpError";
|
|
186
187
|
}
|
|
187
188
|
status;
|
|
189
|
+
reason;
|
|
188
190
|
};
|
|
189
191
|
function formatCpErrorDetail(detail, fallback) {
|
|
190
192
|
if (typeof detail === "string" && detail.trim()) return detail.trim();
|
|
@@ -204,6 +206,40 @@ function formatCpErrorDetail(detail, fallback) {
|
|
|
204
206
|
}
|
|
205
207
|
return fallback;
|
|
206
208
|
}
|
|
209
|
+
function parseRetryAfter(value, now = Date.now()) {
|
|
210
|
+
if (!value) return null;
|
|
211
|
+
const trimmed = value.trim();
|
|
212
|
+
if (/^\d+$/.test(trimmed)) return Number(trimmed);
|
|
213
|
+
const at = Date.parse(trimmed);
|
|
214
|
+
if (Number.isNaN(at)) return null;
|
|
215
|
+
return Math.max(0, Math.ceil((at - now) / 1e3));
|
|
216
|
+
}
|
|
217
|
+
function formatWait(seconds) {
|
|
218
|
+
if (seconds < 60) return `${seconds} second${seconds === 1 ? "" : "s"}`;
|
|
219
|
+
const minutes = Math.ceil(seconds / 60);
|
|
220
|
+
if (minutes < 60) return `${minutes} minute${minutes === 1 ? "" : "s"}`;
|
|
221
|
+
const hours = Math.ceil(minutes / 60);
|
|
222
|
+
return `${hours} hour${hours === 1 ? "" : "s"}`;
|
|
223
|
+
}
|
|
224
|
+
var REASON_HEADER = "x-groundfloor-reason";
|
|
225
|
+
function formatCpErrorMessage(status, detail, retryAfterSeconds, reason = null) {
|
|
226
|
+
let message = `${status} ${detail}`;
|
|
227
|
+
const wait = retryAfterSeconds !== null ? `try again in ${formatWait(retryAfterSeconds)}` : "try again later";
|
|
228
|
+
if (status === 429 && reason === "sending_limited") {
|
|
229
|
+
message += ` (workspace sign-in email limit reached: ${wait}, or give access with a copy-link or temporary password instead)`;
|
|
230
|
+
} else if (status === 429) {
|
|
231
|
+
message += ` (rate limited: ${wait})`;
|
|
232
|
+
} else if (status === 423 && reason === "sending_paused") {
|
|
233
|
+
message += " (sign-in email is paused: only Groundfloor support can resume it; meanwhile give access with a copy-link or temporary password, or switch to your own email server)";
|
|
234
|
+
} else if (status === 423 && !/support/i.test(detail)) {
|
|
235
|
+
message += " (locked: Groundfloor has paused or frozen this; contact Groundfloor support)";
|
|
236
|
+
}
|
|
237
|
+
return message;
|
|
238
|
+
}
|
|
239
|
+
function reasonHeader(res) {
|
|
240
|
+
const value = res.headers?.get?.(REASON_HEADER);
|
|
241
|
+
return value ? value.trim() : null;
|
|
242
|
+
}
|
|
207
243
|
async function parseError(res) {
|
|
208
244
|
let detail = res.statusText;
|
|
209
245
|
try {
|
|
@@ -213,7 +249,12 @@ async function parseError(res) {
|
|
|
213
249
|
}
|
|
214
250
|
} catch {
|
|
215
251
|
}
|
|
216
|
-
return
|
|
252
|
+
return formatCpErrorMessage(
|
|
253
|
+
res.status,
|
|
254
|
+
detail,
|
|
255
|
+
parseRetryAfter(res.headers?.get?.("retry-after")),
|
|
256
|
+
reasonHeader(res)
|
|
257
|
+
);
|
|
217
258
|
}
|
|
218
259
|
async function cpGet(apiUrl, path2) {
|
|
219
260
|
const token = await getValidAccessToken();
|
|
@@ -224,7 +265,7 @@ async function cpGet(apiUrl, path2) {
|
|
|
224
265
|
}
|
|
225
266
|
});
|
|
226
267
|
if (!res.ok) {
|
|
227
|
-
throw new CpError(res.status, await parseError(res));
|
|
268
|
+
throw new CpError(res.status, await parseError(res), reasonHeader(res));
|
|
228
269
|
}
|
|
229
270
|
return await res.json();
|
|
230
271
|
}
|
|
@@ -235,7 +276,7 @@ async function cpDelete(apiUrl, path2) {
|
|
|
235
276
|
headers: { Authorization: `Bearer ${token}`, Accept: "application/json" }
|
|
236
277
|
});
|
|
237
278
|
if (!res.ok) {
|
|
238
|
-
throw new CpError(res.status, await parseError(res));
|
|
279
|
+
throw new CpError(res.status, await parseError(res), reasonHeader(res));
|
|
239
280
|
}
|
|
240
281
|
}
|
|
241
282
|
function queryString(params) {
|
|
@@ -272,7 +313,7 @@ async function getDeployment(apiUrl, workspaceId, coderunnerId) {
|
|
|
272
313
|
);
|
|
273
314
|
if (res.status === 204) return null;
|
|
274
315
|
if (!res.ok) {
|
|
275
|
-
throw new CpError(res.status, await parseError(res));
|
|
316
|
+
throw new CpError(res.status, await parseError(res), reasonHeader(res));
|
|
276
317
|
}
|
|
277
318
|
return await res.json();
|
|
278
319
|
}
|
|
@@ -483,7 +524,16 @@ async function cpGetJson(apiUrl, token, path2) {
|
|
|
483
524
|
}
|
|
484
525
|
} catch {
|
|
485
526
|
}
|
|
486
|
-
throw new CpError(
|
|
527
|
+
throw new CpError(
|
|
528
|
+
res.status,
|
|
529
|
+
formatCpErrorMessage(
|
|
530
|
+
res.status,
|
|
531
|
+
detail,
|
|
532
|
+
parseRetryAfter(res.headers?.get?.("retry-after")),
|
|
533
|
+
reasonHeader(res)
|
|
534
|
+
),
|
|
535
|
+
reasonHeader(res)
|
|
536
|
+
);
|
|
487
537
|
}
|
|
488
538
|
return await res.json();
|
|
489
539
|
}
|
|
@@ -552,7 +602,13 @@ the provider.
|
|
|
552
602
|
port 25, 465, 587 or 2525), then **Send test email** (goes to their own
|
|
553
603
|
verified address). API: \`PUT /v1/workspaces/{id}/auth\` with
|
|
554
604
|
\`mail: {mode: "custom", ...}\`; test with
|
|
555
|
-
\`POST /v1/workspaces/{id}/auth/mail/test\`.
|
|
605
|
+
\`POST /v1/workspaces/{id}/auth/mail/test\`. Over the sending limit it
|
|
606
|
+
returns 429 with \`Retry-After\`; while Groundfloor email is paused, 423. If
|
|
607
|
+
your own address is suppressed, or sending could not be checked, it returns
|
|
608
|
+
200 with \`ok: false\` and a sentence in \`error\`. These answers carry an
|
|
609
|
+
\`X-Groundfloor-Reason\` header (\`sending_limited\`, \`sending_paused\`,
|
|
610
|
+
\`recipient_suppressed\`, \`send_failed\`); the per-user rate limit's 429
|
|
611
|
+
does not.
|
|
556
612
|
- A failing custom server does not fall back to Groundfloor email; the send
|
|
557
613
|
fails. Changing host or port needs the password again.
|
|
558
614
|
- **Warning:** saving a \`mail\` block with no \`host\` (or
|
|
@@ -569,7 +625,12 @@ reset responses, and every user in \`GET /v1/workspaces/{id}/users\`, carry:
|
|
|
569
625
|
- \`email_skipped_reason\`: \`smtp_not_configured\` (no email server set up for
|
|
570
626
|
the workspace's sign-in email), \`send_failed\` (the server refused or
|
|
571
627
|
failed), \`delivery_unconfirmed\` (timed out; it may still arrive),
|
|
572
|
-
\`not_requested\` (sent with \`send_email: false\`),
|
|
628
|
+
\`not_requested\` (sent with \`send_email: false\`), \`sending_limited\` (the
|
|
629
|
+
workspace or this address reached the sending limit; try later),
|
|
630
|
+
\`sending_paused\` (Groundfloor paused the workspace's sign-in email),
|
|
631
|
+
\`recipient_suppressed\` (the address bounced or reported spam), or \`null\`.
|
|
632
|
+
See "Email sending limits" below. Team invite responses carry the same
|
|
633
|
+
two fields.
|
|
573
634
|
\`keycloak_smtp_missing\` is retired and no longer returned.
|
|
574
635
|
- \`email_attempted_at\` (\`null\` = invited before delivery was recorded),
|
|
575
636
|
\`invite_pending\`, \`password_change_required\`,
|
|
@@ -582,7 +643,10 @@ app-user tokens get 403):
|
|
|
582
643
|
|
|
583
644
|
1. **Resend** (\`POST \u2026/users/{user_id}/resend-invite\`, pending users only):
|
|
584
645
|
at most once per 60 s and 5 times per 24 h; re-inviting a pending user
|
|
585
|
-
counts against the same limit (429 with \`Retry-After\`).
|
|
646
|
+
counts against the same limit (429 with \`Retry-After\`). Over the sending
|
|
647
|
+
limit resend also returns 429 with \`Retry-After\` (seconds), told apart by
|
|
648
|
+
the header \`X-Groundfloor-Reason: sending_limited\`: wait that long. When sending is paused or the address is suppressed it returns 200
|
|
649
|
+
with \`email_sent: false\` and the reason. If the reason is
|
|
586
650
|
\`send_failed\` or \`smtp_not_configured\`, fix email first (Branding \u2192
|
|
587
651
|
Email sending, Send test email) or resending will fail the same way.
|
|
588
652
|
2. **Temp password** (\`POST \u2026/users/{user_id}/temporary-password\`, pending
|
|
@@ -601,6 +665,40 @@ person who asked for them, tell them to share privately, and never write them
|
|
|
601
665
|
to files, commits, logs or chat with anyone else. Once a user has set their
|
|
602
666
|
own password, none of these apply (409): use reset-password.
|
|
603
667
|
|
|
668
|
+
## Email sending limits
|
|
669
|
+
|
|
670
|
+
Workspace sign-in email (invite, resend, added-user and reset emails, the test
|
|
671
|
+
email) is limited per workspace to 200 per hour and 1,000 per 24 hours, and per
|
|
672
|
+
address to 5 per hour and 10 per 24 hours (rolling windows; Groundfloor can set
|
|
673
|
+
other workspace limits). Limits apply with Groundfloor email and your own
|
|
674
|
+
server. Portal Team invites are not limited.
|
|
675
|
+
|
|
676
|
+
- **Limited** (\`sending_limited\`, or 429 with \`Retry-After\` and
|
|
677
|
+
\`X-Groundfloor-Reason: sending_limited\`): invite and add
|
|
678
|
+
still create the user, without email. Wait and resend, or give access now
|
|
679
|
+
with **Copy link** or **Temp password**.
|
|
680
|
+
- **Paused** (\`sending_paused\`, test email 423): Groundfloor stopped the
|
|
681
|
+
workspace's Groundfloor email after too many bounces or spam reports. Only
|
|
682
|
+
Groundfloor support can resume it. Meanwhile use Copy link or Temp password,
|
|
683
|
+
or switch to **Your own email server** (pause applies to Groundfloor email
|
|
684
|
+
only).
|
|
685
|
+
- **Suppressed** (\`recipient_suppressed\`): the address bounced or reported
|
|
686
|
+
spam, so Groundfloor email will not send to it. Check the address with the
|
|
687
|
+
user. A workspace Admin or Owner can list suppressed addresses (masked) with
|
|
688
|
+
\`GET /v1/workspaces/{id}/mail/suppressions\` and clear one with
|
|
689
|
+
\`DELETE \u2026/mail/suppressions/{suppression_id}\` (optional body
|
|
690
|
+
\`{"email": "<full address>"}\` to clear it from the mail provider's bounce
|
|
691
|
+
and block lists too; the response's \`provider\` says what happened). An
|
|
692
|
+
address that reported spam (\`provider: spam_report_kept\`) stays on the
|
|
693
|
+
provider's spam list until Groundfloor support removes it, so mail to it may
|
|
694
|
+
still be dropped. If the test email says your own address is suppressed,
|
|
695
|
+
clear it the same way. Only clear an address the user confirms is correct
|
|
696
|
+
and wants mail.
|
|
697
|
+
- \`GET /v1/workspaces/{id}/mail/status\` (Admin or Owner) shows the mode,
|
|
698
|
+
\`state\` (\`active\` / \`paused\`), the limits, how many were sent in the
|
|
699
|
+
last hour and 24 hours, and the number of suppressed addresses.
|
|
700
|
+
\`workspace_context\` includes it as \`sign_in_email\`.
|
|
701
|
+
|
|
604
702
|
## Multi-tenant (customer companies inside one workspace)
|
|
605
703
|
|
|
606
704
|
Turn on **Multi-tenant** in Settings to get a Tenants tab: one tenant per
|
|
@@ -1653,7 +1751,7 @@ var DOC_CATALOG = [
|
|
|
1653
1751
|
id: "authentication",
|
|
1654
1752
|
title: "Workspace Authentication",
|
|
1655
1753
|
path: "/docs/access/app-authentication",
|
|
1656
|
-
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, sign-in email (Groundfloor email by default, or your own server). Invite delivery (email_sent, email_skipped_reason) and what to do when an invite isn't sent: resend, generated temporary password, one-time invite link; 409 for people who can already sign in. App users' tokens work only on the end-user routes. Optional; not required to create Apps or Coderunners. Separate from portal Team.",
|
|
1754
|
+
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, sign-in email (Groundfloor email by default, or your own server). Invite delivery (email_sent, email_skipped_reason incl. sending_limited / sending_paused / recipient_suppressed) and what to do when an invite isn't sent: resend, generated temporary password, one-time invite link; 409 for people who can already sign in. Email sending limits (200/hour and 1,000/day per workspace, 5/hour and 10/day per address), 429 Retry-After and 423 paused (X-Groundfloor-Reason header), the test email's ok:false when suppressed, suppressed addresses (spam reports cleared at the provider only by support), mail status. App users' tokens work only on the end-user routes. Optional; not required to create Apps or Coderunners. Separate from portal Team.",
|
|
1657
1755
|
embedded: APP_AUTHENTICATION_GUIDE,
|
|
1658
1756
|
lastUpdated: "2026-10-14"
|
|
1659
1757
|
},
|
|
@@ -1986,19 +2084,25 @@ server.tool(
|
|
|
1986
2084
|
);
|
|
1987
2085
|
server.tool(
|
|
1988
2086
|
"workspace_context",
|
|
1989
|
-
"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.",
|
|
2087
|
+
"Live context for the selected workspace: session, coderunners, apps, environments (with the default one), feature flags, dataplane (access layer, not a database), sign_in_email (App authentication email mode, paused or active, limits and use; null without workspace administer), 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.",
|
|
1990
2088
|
{ workspace_id: wsId },
|
|
1991
2089
|
READ_ONLY,
|
|
1992
2090
|
async ({ workspace_id }) => {
|
|
1993
2091
|
try {
|
|
1994
2092
|
const c = await cfg(workspace_id);
|
|
1995
2093
|
const claims = decodeJwt(c.token) ?? {};
|
|
1996
|
-
const [coderunners, apps, dataplane, environments, flags] = await Promise.all([
|
|
2094
|
+
const [coderunners, apps, dataplane, environments, flags, mail] = await Promise.all([
|
|
1997
2095
|
listCoderunners(c.apiUrl, c.workspaceId),
|
|
1998
2096
|
listApps(c.apiUrl, c.workspaceId).catch(() => ({ apps: [] })),
|
|
1999
2097
|
getDataplaneStatus(c.apiUrl, c.workspaceId).catch(() => null),
|
|
2000
2098
|
listWorkspaceEnvironments(c.apiUrl, c.workspaceId).catch(() => []),
|
|
2001
|
-
listFlags(c.apiUrl, c.workspaceId).catch(() => null)
|
|
2099
|
+
listFlags(c.apiUrl, c.workspaceId).catch(() => null),
|
|
2100
|
+
// Workspace administer only; null for other callers or older cells.
|
|
2101
|
+
cpGetJson(
|
|
2102
|
+
c.apiUrl,
|
|
2103
|
+
c.token,
|
|
2104
|
+
`/v1/workspaces/${encodeURIComponent(c.workspaceId)}/mail/status`
|
|
2105
|
+
).catch(() => null)
|
|
2002
2106
|
]);
|
|
2003
2107
|
return text({
|
|
2004
2108
|
session: {
|
|
@@ -2020,6 +2124,15 @@ server.tool(
|
|
|
2020
2124
|
status: app.status
|
|
2021
2125
|
})),
|
|
2022
2126
|
dataplane,
|
|
2127
|
+
sign_in_email: mail ? {
|
|
2128
|
+
mode: mail.mode,
|
|
2129
|
+
state: mail.state,
|
|
2130
|
+
paused_reason: mail.paused_reason ?? null,
|
|
2131
|
+
limits: mail.limits,
|
|
2132
|
+
used: mail.used,
|
|
2133
|
+
suppressed_addresses: mail.suppressed_addresses,
|
|
2134
|
+
docs_get: "authentication"
|
|
2135
|
+
} : null,
|
|
2023
2136
|
environments: environments.map((env) => ({
|
|
2024
2137
|
id: env.id,
|
|
2025
2138
|
name: env.name,
|