@cloudgrid-io/mcp 0.8.0 → 0.8.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/package.json +2 -1
- package/src/tools.js +203 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudgrid-io/mcp",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "MCP server for CloudGrid. Three editions: a local stdio server (full toolset), a hosted web server (light, CLI-free toolset), and a public docs server (search_cloudgrid_documentation) over MCP Streamable HTTP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"start:web": "node src/web.js",
|
|
18
18
|
"smoke": "node test/smoke.mjs",
|
|
19
19
|
"test:plug-wire": "node test/plug-wire.test.mjs",
|
|
20
|
+
"test:error-reporting": "node test/error-reporting.test.mjs",
|
|
20
21
|
"test:self-healing": "node test/self-healing.test.mjs",
|
|
21
22
|
"test:grid-picker": "node test/grid-picker.test.mjs",
|
|
22
23
|
"test:drift-guard": "node test/drift-guard.mjs",
|
package/src/tools.js
CHANGED
|
@@ -69,6 +69,7 @@ Operating rules:
|
|
|
69
69
|
5. Minimize questions. Use sensible defaults and build; don't front-load setup questions.
|
|
70
70
|
6. If a signed-in publish fails with a server error, do not fall back to anonymous publishing (it burns the anonymous quota and downgrades ownership); surface the error, use the CLI fallback if offered, or ask the user.
|
|
71
71
|
7. When signed in and the user has more than one grid, do not assume a target — the publish tools will ask; relay the choice to the user and pass the chosen grid.
|
|
72
|
+
8. When a build/deploy fails unexpectedly, offer to report it to the CloudGrid team — only with the user's explicit consent (ask first). Send just the error + the failed request by default (call gridctl_report), and never send the whole conversation unless the user agrees (include_conversation). Respect privacy.
|
|
72
73
|
|
|
73
74
|
Deploy is edition-dependent: on the hosted MCP call the drop tool with the HTML; on local MCP / CLI write the file and run the plug tool. An HTML page deploys synchronously, so you get a URL right away.`;
|
|
74
75
|
|
|
@@ -1132,6 +1133,128 @@ export async function runDrop(
|
|
|
1132
1133
|
};
|
|
1133
1134
|
}
|
|
1134
1135
|
|
|
1136
|
+
// ── Consent-gated error reporting (Task 34 / 0.8.1) ──────────────────────────
|
|
1137
|
+
// Key names that look like they carry a secret. Mirrors the server's
|
|
1138
|
+
// SECRET_KEY_PATTERNS (packages/api/src/routes/errors.ts) so the MCP scrubs the
|
|
1139
|
+
// same shapes client-side — defense-in-depth on top of the server redaction.
|
|
1140
|
+
const REPORT_SECRET_KEY_PATTERNS = [
|
|
1141
|
+
/password/i,
|
|
1142
|
+
/secret/i,
|
|
1143
|
+
/token/i,
|
|
1144
|
+
/api[_-]?key/i,
|
|
1145
|
+
/auth/i,
|
|
1146
|
+
/credential/i,
|
|
1147
|
+
/private[_-]?key/i,
|
|
1148
|
+
/access[_-]?key/i,
|
|
1149
|
+
];
|
|
1150
|
+
|
|
1151
|
+
// Light client-side scrub of obviously secret-looking values in the report
|
|
1152
|
+
// context. Redacts values under secret-looking KEYS; leaves everything else
|
|
1153
|
+
// intact (the server does the authoritative redaction). Bounded depth so a
|
|
1154
|
+
// pathological object can't loop.
|
|
1155
|
+
export function scrubReportContext(obj, depth = 0) {
|
|
1156
|
+
if (depth > 5 || obj === null || typeof obj !== "object") return obj;
|
|
1157
|
+
if (Array.isArray(obj)) return obj.map((item) => scrubReportContext(item, depth + 1));
|
|
1158
|
+
const out = {};
|
|
1159
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1160
|
+
const isSecret = REPORT_SECRET_KEY_PATTERNS.some((re) => re.test(key));
|
|
1161
|
+
out[key] = isSecret ? "[REDACTED]" : scrubReportContext(value, depth + 1);
|
|
1162
|
+
}
|
|
1163
|
+
return out;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// Send a consent-gated bug report to the CloudGrid team. The agent calls this
|
|
1167
|
+
// ONLY after the user explicitly agrees (see errorGuidance + the PLAYBOOK rule).
|
|
1168
|
+
// Posts to POST /api/v2/errors/feedback with {app:"mcp", message, context,
|
|
1169
|
+
// node_version, platform}. Authed → Bearer; anon+web → the trusted-server
|
|
1170
|
+
// headers (works once Gilad opens the endpoint; until then a 401 degrades to a
|
|
1171
|
+
// friendly "sign in to report"). Never throws — always returns friendly text.
|
|
1172
|
+
export async function runReport(ctx, { message, context, include_conversation } = {}) {
|
|
1173
|
+
const summary = typeof message === "string" ? message.trim() : "";
|
|
1174
|
+
if (!summary) {
|
|
1175
|
+
return okResult({
|
|
1176
|
+
text: "Nothing to report — provide a short `message` describing what failed.",
|
|
1177
|
+
structured: { status: "skipped" },
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// Belt-and-suspenders scrub before the value ever leaves the process.
|
|
1182
|
+
const safeContext =
|
|
1183
|
+
context && typeof context === "object" ? scrubReportContext(context) : undefined;
|
|
1184
|
+
// The full conversation is NEVER included unless the agent explicitly set the
|
|
1185
|
+
// flag (which the PLAYBOOK gates on the user's explicit yes). This tool only
|
|
1186
|
+
// records the flag alongside the report so intent is auditable — it does not
|
|
1187
|
+
// itself have the transcript.
|
|
1188
|
+
const body = {
|
|
1189
|
+
app: "mcp",
|
|
1190
|
+
message: summary.slice(0, 5000),
|
|
1191
|
+
context: safeContext,
|
|
1192
|
+
node_version: process.version,
|
|
1193
|
+
platform: process.platform,
|
|
1194
|
+
...(include_conversation === true ? { include_conversation: true } : {}),
|
|
1195
|
+
};
|
|
1196
|
+
|
|
1197
|
+
// Auth: signed-in → Bearer; anon + web edition → trusted-server headers.
|
|
1198
|
+
const headers = { "content-type": "application/json" };
|
|
1199
|
+
let token = null;
|
|
1200
|
+
try {
|
|
1201
|
+
token = await ctx.getToken();
|
|
1202
|
+
} catch {
|
|
1203
|
+
token = null;
|
|
1204
|
+
}
|
|
1205
|
+
let usedTrustedServer = false;
|
|
1206
|
+
if (token) {
|
|
1207
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
1208
|
+
} else if (ctx.trustedServer?.secret && ctx.trustedServer?.endUserId) {
|
|
1209
|
+
// Web edition anon path — works ONCE the endpoint accepts the trusted-server
|
|
1210
|
+
// credential (Gilad-side change). Until then the server 401s and we degrade.
|
|
1211
|
+
headers["X-CloudGrid-Trusted-Server-Auth"] = ctx.trustedServer.secret;
|
|
1212
|
+
headers["X-CloudGrid-Trusted-Server-End-User"] = ctx.trustedServer.endUserId;
|
|
1213
|
+
usedTrustedServer = true;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
let res;
|
|
1217
|
+
try {
|
|
1218
|
+
res = await fetch(`${API_BASE}/api/v2/errors/feedback`, {
|
|
1219
|
+
method: "POST",
|
|
1220
|
+
headers,
|
|
1221
|
+
body: JSON.stringify(body),
|
|
1222
|
+
});
|
|
1223
|
+
} catch (err) {
|
|
1224
|
+
return okResult({
|
|
1225
|
+
text: `Couldn't reach the CloudGrid team right now (${err.message}). Nothing was sent — you can try again later.`,
|
|
1226
|
+
structured: { status: "error" },
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
if (res.status === 201 || res.ok) {
|
|
1231
|
+
return okResult({
|
|
1232
|
+
text: "Reported to the CloudGrid team — thank you.",
|
|
1233
|
+
structured: { status: "recorded" },
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
if (res.status === 429) {
|
|
1237
|
+
return okResult({
|
|
1238
|
+
text: "Already reported a lot recently; try again later.",
|
|
1239
|
+
structured: { status: "rate_limited" },
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
if (res.status === 401) {
|
|
1243
|
+
// Anon reporting isn't accepted yet (needs the Gilad-side endpoint change);
|
|
1244
|
+
// degrade gracefully rather than erroring.
|
|
1245
|
+
return okResult({
|
|
1246
|
+
text: usedTrustedServer || !token
|
|
1247
|
+
? "Sign in to send a report to the CloudGrid team (gridctl_login), then try again."
|
|
1248
|
+
: "That didn't authorize a report. Sign in again (gridctl_login) and retry.",
|
|
1249
|
+
structured: { status: "unauthorized" },
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
return okResult({
|
|
1253
|
+
text: "Couldn't send the report to the CloudGrid team right now. Nothing else was sent — you can try again later.",
|
|
1254
|
+
structured: { status: "error" },
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1135
1258
|
async function runClaim(ctx, { claim_token, claim_url, entity_id }) {
|
|
1136
1259
|
const token = await ctx.getToken();
|
|
1137
1260
|
if (!token) {
|
|
@@ -1394,9 +1517,33 @@ export function errorGuidance({ status, code, edition, isEdit, isAnon, signedIn
|
|
|
1394
1517
|
if (status === 403) {
|
|
1395
1518
|
return "You lack the role to plug this target. To re-plug someone else's entity, pick it up first (gridctl_pickup / gridctl_claim).";
|
|
1396
1519
|
}
|
|
1520
|
+
// ── Consent-gated report offer (Task 34) ──────────────────────────────────
|
|
1521
|
+
// GENUINE bugs only: a build/deploy failure, any 5xx, INTERNAL_ERROR, or an
|
|
1522
|
+
// unknown/unmapped error. Everything above (429 rate-limit, needs_grid picker,
|
|
1523
|
+
// 401 sign-in prompts, 409 EDIT_REJECTED, 403) is an EXPECTED condition, not a
|
|
1524
|
+
// bug — those returned already and never reach here, so they never get the
|
|
1525
|
+
// offer. The offer tells the agent to ask permission first and never send the
|
|
1526
|
+
// full conversation without an explicit yes.
|
|
1527
|
+
const isServerError = typeof status === "number" && status >= 500;
|
|
1528
|
+
const isInternalError = code === "INTERNAL_ERROR";
|
|
1529
|
+
const isBuildFailure = code === "BUILD_FAILED" || code === "DEPLOY_FAILED";
|
|
1530
|
+
if (isServerError || isInternalError || isBuildFailure) {
|
|
1531
|
+
return REPORT_OFFER;
|
|
1532
|
+
}
|
|
1533
|
+
// A 4xx with an unknown/unmapped code is a client-side condition (validation,
|
|
1534
|
+
// bad input), not a server bug — pass through unchanged so callers don't
|
|
1535
|
+
// rewrite it or wrongly offer to report it.
|
|
1397
1536
|
return null;
|
|
1398
1537
|
}
|
|
1399
1538
|
|
|
1539
|
+
// The consent-gated report affordance appended to genuine-bug guidance. It
|
|
1540
|
+
// instructs the agent to get explicit permission before calling gridctl_report,
|
|
1541
|
+
// and to never send the whole conversation without an explicit yes.
|
|
1542
|
+
export const REPORT_OFFER =
|
|
1543
|
+
"If this looks like a CloudGrid bug, ASK the user for permission to report it to the CloudGrid team, " +
|
|
1544
|
+
"then call gridctl_report with the error + the failed request context. " +
|
|
1545
|
+
"Do NOT report without an explicit yes, and do NOT include the full conversation unless the user explicitly agrees.";
|
|
1546
|
+
|
|
1400
1547
|
// Map friendly plug error statuses to actionable messages (spec v2 §3.3).
|
|
1401
1548
|
// Appends errorGuidance() for known codes; unknown codes pass through as the
|
|
1402
1549
|
// bare `base` line, unchanged.
|
|
@@ -2527,6 +2674,62 @@ export function registerTools(server, ctx) {
|
|
|
2527
2674
|
},
|
|
2528
2675
|
);
|
|
2529
2676
|
|
|
2677
|
+
// ── Consent-gated error reporting (Task 34) ───────────────────────────────
|
|
2678
|
+
// Both editions. The agent calls this ONLY after the user explicitly agrees
|
|
2679
|
+
// to report a genuine failure (the errorGuidance offer + the PLAYBOOK rule
|
|
2680
|
+
// gate on consent). Posts the error + failed-request context to the CloudGrid
|
|
2681
|
+
// team; the full conversation is never sent unless include_conversation is
|
|
2682
|
+
// explicitly set true (which the agent only does on an explicit yes).
|
|
2683
|
+
reg(
|
|
2684
|
+
"gridctl_report",
|
|
2685
|
+
{
|
|
2686
|
+
description:
|
|
2687
|
+
"Report a genuine CloudGrid failure to the CloudGrid team — ONLY with the user's explicit consent. When a build/deploy or platform call fails unexpectedly, ASK the user first; call this only after they say yes. Send a short `message` (what failed) plus `context` (the tool, inputs, grid, original request, error code/detail). By default it does NOT include the conversation — set include_conversation:true ONLY if the user explicitly agreed to send the chat. Obvious secrets in context are scrubbed before sending. Never sends anything the user didn't agree to.",
|
|
2688
|
+
inputSchema: {
|
|
2689
|
+
message: z
|
|
2690
|
+
.string()
|
|
2691
|
+
.describe("Short summary of what failed (required). Do not paste the whole conversation here."),
|
|
2692
|
+
context: z
|
|
2693
|
+
.object({
|
|
2694
|
+
tool: z.string().optional().describe("The CloudGrid tool that failed, e.g. gridctl_drop."),
|
|
2695
|
+
inputs: z.any().optional().describe("The failing inputs (e.g. the HTML/args). Keep it minimal; secrets are scrubbed."),
|
|
2696
|
+
grid: z.string().optional().describe("The grid/org slug involved, if any."),
|
|
2697
|
+
original_request: z.string().optional().describe("What the user asked for, in one line."),
|
|
2698
|
+
error_code: z.string().optional().describe("The server error code, e.g. INTERNAL_ERROR."),
|
|
2699
|
+
error_detail: z.string().optional().describe("The error message / detail surfaced to the agent."),
|
|
2700
|
+
})
|
|
2701
|
+
.partial()
|
|
2702
|
+
.optional()
|
|
2703
|
+
.describe("The failed-request context. Secret-looking values are scrubbed client-side and again server-side."),
|
|
2704
|
+
include_conversation: z
|
|
2705
|
+
.boolean()
|
|
2706
|
+
.optional()
|
|
2707
|
+
.describe("Default false. Set true ONLY if the user explicitly agreed to include the full conversation."),
|
|
2708
|
+
},
|
|
2709
|
+
outputSchema: {
|
|
2710
|
+
status: z
|
|
2711
|
+
.enum(["recorded", "rate_limited", "unauthorized", "error", "skipped"])
|
|
2712
|
+
.describe("Outcome of the report."),
|
|
2713
|
+
},
|
|
2714
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
2715
|
+
},
|
|
2716
|
+
async (input) => {
|
|
2717
|
+
try {
|
|
2718
|
+
return await runReport(ctx, {
|
|
2719
|
+
message: input?.message,
|
|
2720
|
+
context: input?.context,
|
|
2721
|
+
include_conversation: input?.include_conversation === true,
|
|
2722
|
+
});
|
|
2723
|
+
} catch (err) {
|
|
2724
|
+
// Belt-and-suspenders: never throw noisily out of a report attempt.
|
|
2725
|
+
return okResult({
|
|
2726
|
+
text: "Couldn't send the report to the CloudGrid team right now. You can try again later.",
|
|
2727
|
+
structured: { status: "error" },
|
|
2728
|
+
});
|
|
2729
|
+
}
|
|
2730
|
+
},
|
|
2731
|
+
);
|
|
2732
|
+
|
|
2530
2733
|
if (ctx.edition !== "local") return; // web edition stops here — no CLI tools
|
|
2531
2734
|
|
|
2532
2735
|
// ── CLI-wrapping tools (local edition only) ───────────────────────────────
|