codex-plugin-doctor 1.68.0 → 1.70.0
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 +24 -8
- package/dist/core/bounded-http-client.d.ts +5 -2
- package/dist/core/bounded-http-client.js +14 -0
- package/dist/core/mcp-discovery.d.ts +20 -0
- package/dist/core/mcp-discovery.js +31 -7
- package/dist/core/mcp-stdio-tool-catalog.d.ts +31 -0
- package/dist/core/mcp-stdio-tool-catalog.js +595 -0
- package/dist/core/mcp-tool-catalog.d.ts +37 -4
- package/dist/core/mcp-tool-catalog.js +103 -75
- package/dist/core/mcp-tool-impact.js +10 -6
- package/dist/core/output-contract.js +20 -0
- package/dist/core/runtime-probe.d.ts +3 -1
- package/dist/core/runtime-probe.js +2 -2
- package/dist/reporting/render-mcp-tool-catalog-report.js +7 -0
- package/dist/reporting/render-mcp-tool-diff-markdown.d.ts +2 -0
- package/dist/reporting/render-mcp-tool-diff-markdown.js +33 -0
- package/dist/run-cli.d.ts +2 -0
- package/dist/run-cli.js +182 -42
- package/package.json +1 -1
package/dist/run-cli.js
CHANGED
|
@@ -21,11 +21,14 @@ import { buildDoctorAttestation, renderDoctorAttestation, renderDoctorAttestatio
|
|
|
21
21
|
import { buildDoctorOutputContract, renderDoctorOutputContract, renderDoctorOutputContractJson } from "./core/output-contract.js";
|
|
22
22
|
import { buildSubmissionArchivePreflight, submissionArchiveExitCode } from "./core/submission-archive-preflight.js";
|
|
23
23
|
import { buildSubmissionPreflight } from "./core/submission-preflight.js";
|
|
24
|
+
import { isValidBearerToken } from "./core/bounded-http-client.js";
|
|
24
25
|
import { discoverMcpServer } from "./core/mcp-discovery.js";
|
|
25
26
|
import { captureMcpToolCatalog, inspectMcpToolCatalog } from "./core/mcp-tool-catalog.js";
|
|
27
|
+
import { captureStdioMcpToolCatalog, resolveMcpStdioServer } from "./core/mcp-stdio-tool-catalog.js";
|
|
26
28
|
import { inspectMcpToolFile, isBlockedOfflinePath } from "./core/mcp-tool-file.js";
|
|
27
29
|
import { saveMcpToolsListResponse } from "./core/mcp-tool-response-file.js";
|
|
28
30
|
import { compareMcpToolFiles } from "./core/mcp-tool-diff.js";
|
|
31
|
+
import { renderMcpToolDiffMarkdown } from "./reporting/render-mcp-tool-diff-markdown.js";
|
|
29
32
|
import { renderMcpToolDiffReport } from "./reporting/render-mcp-tool-diff-report.js";
|
|
30
33
|
import { renderMcpToolFileReport } from "./reporting/render-mcp-tool-file-report.js";
|
|
31
34
|
import { renderMcpToolCatalogReport } from "./reporting/render-mcp-tool-catalog-report.js";
|
|
@@ -108,10 +111,123 @@ function writeExactStdout(io, message) {
|
|
|
108
111
|
}
|
|
109
112
|
class CliUsageError extends Error {
|
|
110
113
|
}
|
|
111
|
-
const toolsDiffUsage = "Usage: codex-plugin-doctor doctor tools-diff --before <path> --after <path> [--json] [--fail-on any|breaking]";
|
|
114
|
+
const toolsDiffUsage = "Usage: codex-plugin-doctor doctor tools-diff --before <path> --after <path> [--json|--markdown] [--fail-on any|breaking]";
|
|
112
115
|
const toolsFileUsage = "Usage: codex-plugin-doctor doctor tools-file <path> [--json]";
|
|
113
|
-
const toolsUsage = "Usage: codex-plugin-doctor doctor tools <url> --allow-network [--allow-local-network] [--json] [--save-response <path>]";
|
|
114
|
-
const discoveryUsage = "Usage: codex-plugin-doctor doctor discover <url> --allow-network [--allow-local-network] [--json]";
|
|
116
|
+
const toolsUsage = "Usage: codex-plugin-doctor doctor tools <url> --allow-network [--allow-local-network] [--json] [--save-response <path>] [--bearer-token-env <NAME>]\n codex-plugin-doctor doctor tools <path> --server <name> --runtime [--sandbox docker] [--require-runtime-approval [--runtime-approval-digest <digest>]] [--json] [--save-response <path>]";
|
|
117
|
+
const discoveryUsage = "Usage: codex-plugin-doctor doctor discover <url> --allow-network [--allow-local-network] [--json] [--bearer-token-env <NAME>]";
|
|
118
|
+
const bearerTokenHelp = "--bearer-token-env reads a bearer token from the named environment variable and sends it only over HTTPS or to an approved loopback target; the token is never printed or saved.";
|
|
119
|
+
const ENVIRONMENT_VARIABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/u;
|
|
120
|
+
function resolveBearerToken(name, env) {
|
|
121
|
+
if (name === undefined)
|
|
122
|
+
return {};
|
|
123
|
+
if (!ENVIRONMENT_VARIABLE_NAME.test(name))
|
|
124
|
+
return { error: "--bearer-token-env requires an environment variable name." };
|
|
125
|
+
const token = env[name];
|
|
126
|
+
if (token === undefined || token === "")
|
|
127
|
+
return { error: `Bearer token environment variable is not set: ${name}` };
|
|
128
|
+
if (!isValidBearerToken(token))
|
|
129
|
+
return { error: `Bearer token environment variable does not hold a valid bearer token: ${name}` };
|
|
130
|
+
return { token };
|
|
131
|
+
}
|
|
132
|
+
async function finishToolCatalogCommand(io, snapshot, json, savePath) {
|
|
133
|
+
const report = snapshot.report;
|
|
134
|
+
io.writeStdout(json
|
|
135
|
+
? JSON.stringify(report, null, 2)
|
|
136
|
+
: renderMcpToolCatalogReport(report));
|
|
137
|
+
const exitCode = report.status === "pass" ? 0
|
|
138
|
+
: report.status === "incomplete" || report.status === "blocked" ? 2 : 1;
|
|
139
|
+
if (savePath === undefined)
|
|
140
|
+
return exitCode;
|
|
141
|
+
if (snapshot.tools === null || snapshot.cache === null) {
|
|
142
|
+
io.writeStderr("Tool catalog response was not saved because enumeration did not complete.");
|
|
143
|
+
return 2;
|
|
144
|
+
}
|
|
145
|
+
const saved = await saveMcpToolsListResponse(savePath, snapshot.tools, snapshot.cache);
|
|
146
|
+
if (saved.kind !== "saved") {
|
|
147
|
+
io.writeStderr({
|
|
148
|
+
"path-blocked": "Tool catalog response was not saved because the path is not a local file path.",
|
|
149
|
+
"not-regular": "Tool catalog response was not saved because the existing target is not a regular file.",
|
|
150
|
+
"too-large": "Tool catalog response was not saved because it exceeds the one MiB tools-file limit.",
|
|
151
|
+
"write-failed": "Tool catalog response could not be written."
|
|
152
|
+
}[saved.kind]);
|
|
153
|
+
return 2;
|
|
154
|
+
}
|
|
155
|
+
io.writeStderr(`Saved a complete tools/list response with ${saved.tools} tools (${saved.bytes} bytes).`);
|
|
156
|
+
return exitCode;
|
|
157
|
+
}
|
|
158
|
+
async function runStdioToolCatalogCommand(targetPath, flags, io, options) {
|
|
159
|
+
const allowedSwitches = new Set(["--runtime", "--json", "--require-runtime-approval"]);
|
|
160
|
+
const switches = [];
|
|
161
|
+
const values = new Map();
|
|
162
|
+
let invalid = false;
|
|
163
|
+
for (let index = 0; index < flags.length; index += 1) {
|
|
164
|
+
const flag = flags[index];
|
|
165
|
+
if (["--server", "--save-response", "--sandbox", "--runtime-approval-digest"].includes(flag) && !values.has(flag)) {
|
|
166
|
+
const value = flags[index + 1];
|
|
167
|
+
if (!value || value.startsWith("-")) {
|
|
168
|
+
invalid = true;
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
values.set(flag, value);
|
|
172
|
+
index += 1;
|
|
173
|
+
}
|
|
174
|
+
else if (allowedSwitches.has(flag) && !switches.includes(flag)) {
|
|
175
|
+
switches.push(flag);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
invalid = true;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const serverName = values.get("--server");
|
|
183
|
+
const savePath = values.get("--save-response");
|
|
184
|
+
const sandbox = values.get("--sandbox");
|
|
185
|
+
const approvalDigest = values.get("--runtime-approval-digest");
|
|
186
|
+
const requireApproval = switches.includes("--require-runtime-approval");
|
|
187
|
+
if (invalid || serverName === undefined || (sandbox !== undefined && sandbox !== "docker")) {
|
|
188
|
+
io.writeStderr(`Invalid or duplicate tool catalog arguments. ${toolsUsage}`);
|
|
189
|
+
return 2;
|
|
190
|
+
}
|
|
191
|
+
if (!switches.includes("--runtime")) {
|
|
192
|
+
io.writeStderr("doctor tools --server starts a local MCP server and requires explicit --runtime consent.");
|
|
193
|
+
return 2;
|
|
194
|
+
}
|
|
195
|
+
if (approvalDigest !== undefined && !requireApproval) {
|
|
196
|
+
io.writeStderr("--runtime-approval-digest requires --require-runtime-approval.");
|
|
197
|
+
return 2;
|
|
198
|
+
}
|
|
199
|
+
if (isBlockedOfflinePath(targetPath)) {
|
|
200
|
+
io.writeStderr("doctor tools --server requires a local package path.");
|
|
201
|
+
return 2;
|
|
202
|
+
}
|
|
203
|
+
if (savePath !== undefined && isBlockedOfflinePath(savePath)) {
|
|
204
|
+
io.writeStderr("doctor tools --save-response requires a local file path.");
|
|
205
|
+
return 2;
|
|
206
|
+
}
|
|
207
|
+
const resolved = await resolveMcpStdioServer(targetPath, serverName);
|
|
208
|
+
if (!resolved.ok) {
|
|
209
|
+
io.writeStderr(resolved.message);
|
|
210
|
+
return 2;
|
|
211
|
+
}
|
|
212
|
+
if (requireApproval) {
|
|
213
|
+
// Runtime plan digests cover only the manifest mcpServers file, so .mcp.json servers cannot be approved.
|
|
214
|
+
if (resolved.server.source !== "manifest") {
|
|
215
|
+
io.writeStderr("Runtime approval covers servers from the plugin manifest mcpServers file; this server comes from .mcp.json.");
|
|
216
|
+
return 2;
|
|
217
|
+
}
|
|
218
|
+
const runtimePlan = await buildDoctorRuntimePlan(targetPath, new Date().toISOString(), sandbox === "docker" ? { sandbox: "docker" } : {});
|
|
219
|
+
const approval = evaluateRuntimeApproval(runtimePlan, {
|
|
220
|
+
required: true,
|
|
221
|
+
approvedDigest: approvalDigest ?? null
|
|
222
|
+
});
|
|
223
|
+
if (!runtimeApprovalPassed(approval)) {
|
|
224
|
+
io.writeStderr(`${approval.message}\nCurrent runtime plan digest: ${runtimePlan.digest}`);
|
|
225
|
+
return 1;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const snapshot = await (options.captureStdioMcpToolCatalogImpl ?? captureStdioMcpToolCatalog)(resolved.server, sandbox === "docker" ? { sandbox: "docker" } : {});
|
|
229
|
+
return finishToolCatalogCommand(io, snapshot, switches.includes("--json"), savePath);
|
|
230
|
+
}
|
|
115
231
|
function parseRuntimeSandbox(flags, options = { requireRuntime: true }) {
|
|
116
232
|
const index = flags.indexOf("--sandbox");
|
|
117
233
|
if (index === -1) {
|
|
@@ -1160,9 +1276,10 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
1160
1276
|
(command === "release" && maybePath === "check") ||
|
|
1161
1277
|
(command === "doctor" &&
|
|
1162
1278
|
(["runtime-plan", "runtime-policy"].includes(maybePath ?? "") ||
|
|
1163
|
-
(maybePath === "release-evidence" && remainingArgs[0] !== "verify")
|
|
1279
|
+
(maybePath === "release-evidence" && remainingArgs[0] !== "verify") ||
|
|
1280
|
+
(maybePath === "tools" && remainingArgs.includes("--server"))));
|
|
1164
1281
|
if (args.includes("--sandbox") && !runtimeSandboxSupported) {
|
|
1165
|
-
io.writeStderr("--sandbox is supported only by single-package check, release check, runtime-plan, runtime-policy,
|
|
1282
|
+
io.writeStderr("--sandbox is supported only by single-package check, release check, runtime-plan, runtime-policy, release-evidence, and doctor tools --server.");
|
|
1166
1283
|
return 2;
|
|
1167
1284
|
}
|
|
1168
1285
|
if (command === "--version" || command === "-v" || command === "version") {
|
|
@@ -1215,13 +1332,13 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
1215
1332
|
}
|
|
1216
1333
|
let beforePath;
|
|
1217
1334
|
let afterPath;
|
|
1218
|
-
let
|
|
1335
|
+
let format;
|
|
1219
1336
|
let failOn;
|
|
1220
1337
|
let invalid = false;
|
|
1221
1338
|
for (let index = 0; index < remainingArgs.length; index += 1) {
|
|
1222
1339
|
const flag = remainingArgs[index];
|
|
1223
|
-
if (flag === "--json" &&
|
|
1224
|
-
|
|
1340
|
+
if ((flag === "--json" || flag === "--markdown") && format === undefined) {
|
|
1341
|
+
format = flag === "--json" ? "json" : "markdown";
|
|
1225
1342
|
}
|
|
1226
1343
|
else if (flag === "--fail-on" && failOn === undefined) {
|
|
1227
1344
|
const value = remainingArgs[index + 1];
|
|
@@ -1254,7 +1371,9 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
1254
1371
|
return 2;
|
|
1255
1372
|
}
|
|
1256
1373
|
const report = await (options.compareMcpToolFilesImpl ?? compareMcpToolFiles)(beforePath, afterPath);
|
|
1257
|
-
io.writeStdout(json ? JSON.stringify(report, null, 2)
|
|
1374
|
+
io.writeStdout(format === "json" ? JSON.stringify(report, null, 2)
|
|
1375
|
+
: format === "markdown" ? renderMcpToolDiffMarkdown(report)
|
|
1376
|
+
: renderMcpToolDiffReport(report));
|
|
1258
1377
|
if (report.status !== "pass" && report.status !== "warn")
|
|
1259
1378
|
return 2;
|
|
1260
1379
|
if (failOn === "breaking") {
|
|
@@ -1281,7 +1400,7 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
1281
1400
|
}
|
|
1282
1401
|
if (maybePath === "tools") {
|
|
1283
1402
|
if (remainingArgs.length === 1 && remainingArgs[0] === "--help") {
|
|
1284
|
-
io.writeStdout(`${toolsUsage}\
|
|
1403
|
+
io.writeStdout(`${toolsUsage}\nTool catalog structure only; tools are not executed.\n--server starts the named stdio server from the package MCP configuration after explicit --runtime consent and stops it after reading the tool list.\n--save-response writes a complete enumeration as one tools/list response for doctor tools-file and tools-diff; nothing is written when enumeration is incomplete.\n${bearerTokenHelp}`);
|
|
1285
1404
|
return 0;
|
|
1286
1405
|
}
|
|
1287
1406
|
const [url, ...flags] = remainingArgs;
|
|
@@ -1289,19 +1408,26 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
1289
1408
|
io.writeStderr(toolsUsage);
|
|
1290
1409
|
return 2;
|
|
1291
1410
|
}
|
|
1411
|
+
if (flags.includes("--server")) {
|
|
1412
|
+
return runStdioToolCatalogCommand(url, flags, io, options);
|
|
1413
|
+
}
|
|
1292
1414
|
const allowedFlags = new Set(["--allow-network", "--allow-local-network", "--json"]);
|
|
1293
1415
|
const switches = [];
|
|
1294
1416
|
let savePath;
|
|
1417
|
+
let tokenEnv;
|
|
1295
1418
|
let invalid = false;
|
|
1296
1419
|
for (let index = 0; index < flags.length; index += 1) {
|
|
1297
1420
|
const flag = flags[index];
|
|
1298
|
-
if (flag === "--save-response" && savePath === undefined) {
|
|
1421
|
+
if ((flag === "--save-response" && savePath === undefined) || (flag === "--bearer-token-env" && tokenEnv === undefined)) {
|
|
1299
1422
|
const value = flags[index + 1];
|
|
1300
1423
|
if (!value || value.startsWith("-")) {
|
|
1301
1424
|
invalid = true;
|
|
1302
1425
|
break;
|
|
1303
1426
|
}
|
|
1304
|
-
|
|
1427
|
+
if (flag === "--save-response")
|
|
1428
|
+
savePath = value;
|
|
1429
|
+
else
|
|
1430
|
+
tokenEnv = value;
|
|
1305
1431
|
index += 1;
|
|
1306
1432
|
}
|
|
1307
1433
|
else if (allowedFlags.has(flag) && !switches.includes(flag)) {
|
|
@@ -1324,38 +1450,24 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
1324
1450
|
io.writeStderr("doctor tools --save-response requires a local file path.");
|
|
1325
1451
|
return 2;
|
|
1326
1452
|
}
|
|
1327
|
-
const
|
|
1453
|
+
const bearer = resolveBearerToken(tokenEnv, terminalContext.env);
|
|
1454
|
+
if ("error" in bearer) {
|
|
1455
|
+
io.writeStderr(bearer.error);
|
|
1456
|
+
return 2;
|
|
1457
|
+
}
|
|
1458
|
+
const catalogOptions = {
|
|
1459
|
+
allowNetwork: true,
|
|
1460
|
+
allowLocalNetwork: switches.includes("--allow-local-network"),
|
|
1461
|
+
...(bearer.token === undefined ? {} : { bearerToken: bearer.token })
|
|
1462
|
+
};
|
|
1328
1463
|
const snapshot = savePath === undefined
|
|
1329
1464
|
? { report: await (options.inspectMcpToolCatalogImpl ?? inspectMcpToolCatalog)(url, catalogOptions), tools: null, cache: null }
|
|
1330
1465
|
: await (options.captureMcpToolCatalogImpl ?? captureMcpToolCatalog)(url, catalogOptions);
|
|
1331
|
-
|
|
1332
|
-
io.writeStdout(switches.includes("--json")
|
|
1333
|
-
? JSON.stringify(report, null, 2)
|
|
1334
|
-
: renderMcpToolCatalogReport(report));
|
|
1335
|
-
const exitCode = report.status === "pass" ? 0
|
|
1336
|
-
: report.status === "incomplete" || report.status === "blocked" ? 2 : 1;
|
|
1337
|
-
if (savePath === undefined)
|
|
1338
|
-
return exitCode;
|
|
1339
|
-
if (snapshot.tools === null || snapshot.cache === null) {
|
|
1340
|
-
io.writeStderr("Tool catalog response was not saved because enumeration did not complete.");
|
|
1341
|
-
return 2;
|
|
1342
|
-
}
|
|
1343
|
-
const saved = await saveMcpToolsListResponse(savePath, snapshot.tools, snapshot.cache);
|
|
1344
|
-
if (saved.kind !== "saved") {
|
|
1345
|
-
io.writeStderr({
|
|
1346
|
-
"path-blocked": "Tool catalog response was not saved because the path is not a local file path.",
|
|
1347
|
-
"not-regular": "Tool catalog response was not saved because the existing target is not a regular file.",
|
|
1348
|
-
"too-large": "Tool catalog response was not saved because it exceeds the one MiB tools-file limit.",
|
|
1349
|
-
"write-failed": "Tool catalog response could not be written."
|
|
1350
|
-
}[saved.kind]);
|
|
1351
|
-
return 2;
|
|
1352
|
-
}
|
|
1353
|
-
io.writeStderr(`Saved a complete tools/list response with ${saved.tools} tools (${saved.bytes} bytes).`);
|
|
1354
|
-
return exitCode;
|
|
1466
|
+
return finishToolCatalogCommand(io, snapshot, switches.includes("--json"), savePath);
|
|
1355
1467
|
}
|
|
1356
1468
|
if (maybePath === "discover") {
|
|
1357
1469
|
if (remainingArgs.length === 1 && remainingArgs[0] === "--help") {
|
|
1358
|
-
io.writeStdout(`${discoveryUsage}\nHTTP discovery only; runtime behavior is not tested
|
|
1470
|
+
io.writeStdout(`${discoveryUsage}\nHTTP discovery only; runtime behavior is not tested.\n${bearerTokenHelp}`);
|
|
1359
1471
|
return 0;
|
|
1360
1472
|
}
|
|
1361
1473
|
const [url, ...flags] = remainingArgs;
|
|
@@ -1364,19 +1476,47 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
1364
1476
|
return 2;
|
|
1365
1477
|
}
|
|
1366
1478
|
const allowedFlags = new Set(["--allow-network", "--allow-local-network", "--json"]);
|
|
1367
|
-
|
|
1479
|
+
const switches = [];
|
|
1480
|
+
let tokenEnv;
|
|
1481
|
+
let invalid = false;
|
|
1482
|
+
for (let index = 0; index < flags.length; index += 1) {
|
|
1483
|
+
const flag = flags[index];
|
|
1484
|
+
if (flag === "--bearer-token-env" && tokenEnv === undefined) {
|
|
1485
|
+
const value = flags[index + 1];
|
|
1486
|
+
if (!value || value.startsWith("-")) {
|
|
1487
|
+
invalid = true;
|
|
1488
|
+
break;
|
|
1489
|
+
}
|
|
1490
|
+
tokenEnv = value;
|
|
1491
|
+
index += 1;
|
|
1492
|
+
}
|
|
1493
|
+
else if (allowedFlags.has(flag) && !switches.includes(flag)) {
|
|
1494
|
+
switches.push(flag);
|
|
1495
|
+
}
|
|
1496
|
+
else {
|
|
1497
|
+
invalid = true;
|
|
1498
|
+
break;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
if (invalid) {
|
|
1368
1502
|
io.writeStderr(`Invalid or duplicate discovery arguments. ${discoveryUsage}`);
|
|
1369
1503
|
return 2;
|
|
1370
1504
|
}
|
|
1371
|
-
if (!
|
|
1505
|
+
if (!switches.includes("--allow-network")) {
|
|
1372
1506
|
io.writeStderr("doctor discover requires explicit --allow-network consent.");
|
|
1373
1507
|
return 2;
|
|
1374
1508
|
}
|
|
1509
|
+
const bearer = resolveBearerToken(tokenEnv, terminalContext.env);
|
|
1510
|
+
if ("error" in bearer) {
|
|
1511
|
+
io.writeStderr(bearer.error);
|
|
1512
|
+
return 2;
|
|
1513
|
+
}
|
|
1375
1514
|
const report = await (options.discoverMcpServerImpl ?? discoverMcpServer)(url, {
|
|
1376
1515
|
allowNetwork: true,
|
|
1377
|
-
allowLocalNetwork:
|
|
1516
|
+
allowLocalNetwork: switches.includes("--allow-local-network"),
|
|
1517
|
+
...(bearer.token === undefined ? {} : { bearerToken: bearer.token })
|
|
1378
1518
|
});
|
|
1379
|
-
io.writeStdout(
|
|
1519
|
+
io.writeStdout(switches.includes("--json")
|
|
1380
1520
|
? JSON.stringify(report, null, 2)
|
|
1381
1521
|
: renderMcpDiscoveryReport(report));
|
|
1382
1522
|
return report.status === "discovered" ? 0 : report.status === "unsupported" ? 1 : 2;
|
package/package.json
CHANGED