@cloudgrid-io/mcp 0.8.1 → 0.8.2
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 +1 -1
- package/src/index.js +12 -0
- package/src/tools.js +107 -11
- package/src/web.js +12 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudgrid-io/mcp",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
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": {
|
package/src/index.js
CHANGED
|
@@ -31,5 +31,17 @@ const ctx = {
|
|
|
31
31
|
const server = new McpServer({ name: "cloudgrid-mcp", version });
|
|
32
32
|
registerTools(server, ctx);
|
|
33
33
|
|
|
34
|
+
// Capture the calling agent's clientInfo (name+version) once the MCP handshake
|
|
35
|
+
// completes, so gridctl_report can attribute the report's origin (which agent).
|
|
36
|
+
// getClientVersion() is populated by the SDK from the initialize request's
|
|
37
|
+
// clientInfo. Never fatal — a missing client just falls back to "unknown".
|
|
38
|
+
server.server.oninitialized = () => {
|
|
39
|
+
try {
|
|
40
|
+
ctx.state.client = server.server.getClientVersion() ?? null;
|
|
41
|
+
} catch {
|
|
42
|
+
ctx.state.client = null;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
34
46
|
const transport = new StdioServerTransport();
|
|
35
47
|
await server.connect(transport);
|
package/src/tools.js
CHANGED
|
@@ -25,6 +25,16 @@ export const API_BASE = (process.env.CLOUDGRID_API_URL || "https://api.cloudgrid
|
|
|
25
25
|
"",
|
|
26
26
|
);
|
|
27
27
|
|
|
28
|
+
// This MCP server's version — mirrors the CLI's cli_version in a report's origin.
|
|
29
|
+
// Read once from package.json; never throw (a report must never fail on this).
|
|
30
|
+
export const MCP_VERSION = (() => {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8")).version;
|
|
33
|
+
} catch {
|
|
34
|
+
return "unknown";
|
|
35
|
+
}
|
|
36
|
+
})();
|
|
37
|
+
|
|
28
38
|
const ANON_HTML_MAX_BYTES = 2_000_000;
|
|
29
39
|
const CONSOLE_URL = "https://console.cloudgrid.io/";
|
|
30
40
|
|
|
@@ -1165,11 +1175,27 @@ export function scrubReportContext(obj, depth = 0) {
|
|
|
1165
1175
|
|
|
1166
1176
|
// Send a consent-gated bug report to the CloudGrid team. The agent calls this
|
|
1167
1177
|
// ONLY after the user explicitly agrees (see errorGuidance + the PLAYBOOK rule).
|
|
1168
|
-
//
|
|
1169
|
-
//
|
|
1170
|
-
//
|
|
1171
|
-
//
|
|
1172
|
-
|
|
1178
|
+
//
|
|
1179
|
+
// Matches the CLI reporter (packages/cli error-reporter.ts → client.reportError):
|
|
1180
|
+
// POST /api/v2/errors with the CLI payload shape
|
|
1181
|
+
// { type:'error', category, app, message, stack?, context, trace_id?,
|
|
1182
|
+
// failed_step?, http_status?, cli_version, node_version, platform }
|
|
1183
|
+
// so CLI + MCP reports land uniformly in the `errors` collection.
|
|
1184
|
+
//
|
|
1185
|
+
// Source attribution (Gilad's ask): every report says WHERE it came from —
|
|
1186
|
+
// source (mcp-stdio | mcp-hosted), client (the calling agent from MCP clientInfo),
|
|
1187
|
+
// platform, mcp_version. Sent BOTH as top-level fields AND mirrored in
|
|
1188
|
+
// `context.origin`. The POST /errors handler only persists known top-level keys
|
|
1189
|
+
// (it drops unknown ones), and `context` is stored + secret-stripped server-side,
|
|
1190
|
+
// so `context.origin` is the durable carrier — belt-and-suspenders.
|
|
1191
|
+
//
|
|
1192
|
+
// Auth: signed-in → Bearer; anon+web → the trusted-server headers (works once the
|
|
1193
|
+
// endpoint accepts the credential; until then a 401 degrades to "sign in to
|
|
1194
|
+
// report"). Honors CLOUDGRID_TELEMETRY=off (matches the CLI). Never throws.
|
|
1195
|
+
export async function runReport(
|
|
1196
|
+
ctx,
|
|
1197
|
+
{ message, context, include_conversation, category, trace_id, failed_step, http_status } = {},
|
|
1198
|
+
) {
|
|
1173
1199
|
const summary = typeof message === "string" ? message.trim() : "";
|
|
1174
1200
|
if (!summary) {
|
|
1175
1201
|
return okResult({
|
|
@@ -1178,19 +1204,69 @@ export async function runReport(ctx, { message, context, include_conversation }
|
|
|
1178
1204
|
});
|
|
1179
1205
|
}
|
|
1180
1206
|
|
|
1181
|
-
//
|
|
1182
|
-
|
|
1183
|
-
|
|
1207
|
+
// Privacy escape hatch — no telemetry when explicitly disabled (matches the CLI
|
|
1208
|
+
// reporter's CLOUDGRID_TELEMETRY=off). Consent still gates the call regardless;
|
|
1209
|
+
// this is the belt-and-suspenders global opt-out. Nothing leaves the process.
|
|
1210
|
+
if (process.env.CLOUDGRID_TELEMETRY === "off") {
|
|
1211
|
+
return okResult({
|
|
1212
|
+
text: "Error reporting is disabled (CLOUDGRID_TELEMETRY=off) — nothing was sent.",
|
|
1213
|
+
structured: { status: "disabled" },
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// ── Source attribution ──────────────────────────────────────────────────────
|
|
1218
|
+
// source: mcp-stdio (local edition) | mcp-hosted (web edition).
|
|
1219
|
+
const source = ctx.edition === "web" ? "mcp-hosted" : "mcp-stdio";
|
|
1220
|
+
// client: the calling agent captured from the MCP clientInfo at initialize.
|
|
1221
|
+
// Falls back to "unknown" — a report must never fail on missing client info.
|
|
1222
|
+
const ci = ctx.state?.client;
|
|
1223
|
+
const client =
|
|
1224
|
+
ci && ci.name
|
|
1225
|
+
? ci.version
|
|
1226
|
+
? `${ci.name} ${ci.version}`
|
|
1227
|
+
: String(ci.name)
|
|
1228
|
+
: "unknown";
|
|
1229
|
+
const platform = `${process.platform} ${process.arch}`;
|
|
1230
|
+
|
|
1231
|
+
// Belt-and-suspenders scrub before the value ever leaves the process. The
|
|
1232
|
+
// origin block is authored by us (not user/agent input), so it is appended
|
|
1233
|
+
// AFTER the scrub — it carries no secrets and must survive verbatim.
|
|
1234
|
+
const scrubbed =
|
|
1235
|
+
context && typeof context === "object" ? scrubReportContext(context) : {};
|
|
1236
|
+
const safeContext = {
|
|
1237
|
+
...scrubbed,
|
|
1238
|
+
origin: {
|
|
1239
|
+
source,
|
|
1240
|
+
client,
|
|
1241
|
+
platform,
|
|
1242
|
+
mcp_version: MCP_VERSION,
|
|
1243
|
+
},
|
|
1244
|
+
};
|
|
1245
|
+
|
|
1184
1246
|
// The full conversation is NEVER included unless the agent explicitly set the
|
|
1185
1247
|
// flag (which the PLAYBOOK gates on the user's explicit yes). This tool only
|
|
1186
1248
|
// records the flag alongside the report so intent is auditable — it does not
|
|
1187
1249
|
// itself have the transcript.
|
|
1188
1250
|
const body = {
|
|
1251
|
+
type: "error",
|
|
1252
|
+
// category: default "mcp" (or the failing tool name the agent passes).
|
|
1253
|
+
category: typeof category === "string" && category.trim() ? category.trim() : "mcp",
|
|
1189
1254
|
app: "mcp",
|
|
1190
1255
|
message: summary.slice(0, 5000),
|
|
1191
1256
|
context: safeContext,
|
|
1257
|
+
// Diagnostic pivots (match the CLI) — only when the agent forwards them.
|
|
1258
|
+
...(typeof trace_id === "string" && trace_id ? { trace_id } : {}),
|
|
1259
|
+
...(typeof failed_step === "string" && failed_step ? { failed_step } : {}),
|
|
1260
|
+
...(typeof http_status === "number" && Number.isFinite(http_status) ? { http_status } : {}),
|
|
1261
|
+
// Attribution, ALSO top-level (persisted once the handler accepts these keys;
|
|
1262
|
+
// context.origin is the fallback until then).
|
|
1263
|
+
source,
|
|
1264
|
+
client,
|
|
1265
|
+
platform,
|
|
1266
|
+
// cli_version stays null for MCP-originated reports; mcp_version carries our
|
|
1267
|
+
// version in context.origin (the CLI-analog lives there).
|
|
1268
|
+
cli_version: null,
|
|
1192
1269
|
node_version: process.version,
|
|
1193
|
-
platform: process.platform,
|
|
1194
1270
|
...(include_conversation === true ? { include_conversation: true } : {}),
|
|
1195
1271
|
};
|
|
1196
1272
|
|
|
@@ -1215,7 +1291,7 @@ export async function runReport(ctx, { message, context, include_conversation }
|
|
|
1215
1291
|
|
|
1216
1292
|
let res;
|
|
1217
1293
|
try {
|
|
1218
|
-
res = await fetch(`${API_BASE}/api/v2/errors
|
|
1294
|
+
res = await fetch(`${API_BASE}/api/v2/errors`, {
|
|
1219
1295
|
method: "POST",
|
|
1220
1296
|
headers,
|
|
1221
1297
|
body: JSON.stringify(body),
|
|
@@ -2705,10 +2781,26 @@ export function registerTools(server, ctx) {
|
|
|
2705
2781
|
.boolean()
|
|
2706
2782
|
.optional()
|
|
2707
2783
|
.describe("Default false. Set true ONLY if the user explicitly agreed to include the full conversation."),
|
|
2784
|
+
category: z
|
|
2785
|
+
.string()
|
|
2786
|
+
.optional()
|
|
2787
|
+
.describe("Optional category, e.g. the failing tool name (\"deploy\"). Defaults to \"mcp\"."),
|
|
2788
|
+
trace_id: z
|
|
2789
|
+
.string()
|
|
2790
|
+
.optional()
|
|
2791
|
+
.describe("The server's trace/deploy id from the failed response, if any (helps support pivot to the trace)."),
|
|
2792
|
+
failed_step: z
|
|
2793
|
+
.string()
|
|
2794
|
+
.optional()
|
|
2795
|
+
.describe("The server-side pipeline step that failed, if known."),
|
|
2796
|
+
http_status: z
|
|
2797
|
+
.number()
|
|
2798
|
+
.optional()
|
|
2799
|
+
.describe("The HTTP status of the final failed request, if applicable."),
|
|
2708
2800
|
},
|
|
2709
2801
|
outputSchema: {
|
|
2710
2802
|
status: z
|
|
2711
|
-
.enum(["recorded", "rate_limited", "unauthorized", "error", "skipped"])
|
|
2803
|
+
.enum(["recorded", "rate_limited", "unauthorized", "error", "skipped", "disabled"])
|
|
2712
2804
|
.describe("Outcome of the report."),
|
|
2713
2805
|
},
|
|
2714
2806
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
@@ -2719,6 +2811,10 @@ export function registerTools(server, ctx) {
|
|
|
2719
2811
|
message: input?.message,
|
|
2720
2812
|
context: input?.context,
|
|
2721
2813
|
include_conversation: input?.include_conversation === true,
|
|
2814
|
+
category: input?.category,
|
|
2815
|
+
trace_id: input?.trace_id,
|
|
2816
|
+
failed_step: input?.failed_step,
|
|
2817
|
+
http_status: input?.http_status,
|
|
2722
2818
|
});
|
|
2723
2819
|
} catch (err) {
|
|
2724
2820
|
// Belt-and-suspenders: never throw noisily out of a report attempt.
|
package/src/web.js
CHANGED
|
@@ -132,7 +132,18 @@ app.post("/mcp", async (req, res) => {
|
|
|
132
132
|
}
|
|
133
133
|
};
|
|
134
134
|
const server = new McpServer({ name: "cloudgrid-mcp-web", version });
|
|
135
|
-
|
|
135
|
+
const webCtx = makeWebContext(newSessionId);
|
|
136
|
+
registerTools(server, webCtx);
|
|
137
|
+
// Capture the calling agent's clientInfo (name+version) for this session once
|
|
138
|
+
// the MCP handshake completes, so gridctl_report can attribute the origin
|
|
139
|
+
// (which agent: Claude/ChatGPT/Cursor/…). Never fatal — missing → "unknown".
|
|
140
|
+
server.server.oninitialized = () => {
|
|
141
|
+
try {
|
|
142
|
+
webCtx.state.client = server.server.getClientVersion() ?? null;
|
|
143
|
+
} catch {
|
|
144
|
+
webCtx.state.client = null;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
136
147
|
await server.connect(transport);
|
|
137
148
|
await transport.handleRequest(req, res, req.body);
|
|
138
149
|
});
|