codex-plugin-doctor 0.1.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/LICENSE +21 -0
- package/README.md +215 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +9 -0
- package/dist/core/discover-package.d.ts +2 -0
- package/dist/core/discover-package.js +18 -0
- package/dist/core/runtime-probe.d.ts +5 -0
- package/dist/core/runtime-probe.js +917 -0
- package/dist/core/runtime-transcript.d.ts +5 -0
- package/dist/core/runtime-transcript.js +139 -0
- package/dist/core/validate-plugin.d.ts +2 -0
- package/dist/core/validate-plugin.js +341 -0
- package/dist/domain/types.d.ts +64 -0
- package/dist/domain/types.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/release/release-notes.d.ts +9 -0
- package/dist/release/release-notes.js +46 -0
- package/dist/reporting/render-json-report.d.ts +7 -0
- package/dist/reporting/render-json-report.js +26 -0
- package/dist/reporting/render-markdown-report.d.ts +4 -0
- package/dist/reporting/render-markdown-report.js +44 -0
- package/dist/reporting/render-text-report.d.ts +4 -0
- package/dist/reporting/render-text-report.js +77 -0
- package/dist/run-cli.d.ts +15 -0
- package/dist/run-cli.js +87 -0
- package/dist/terminal/live-status-renderer.d.ts +9 -0
- package/dist/terminal/live-status-renderer.js +38 -0
- package/dist/terminal/output-policy.d.ts +16 -0
- package/dist/terminal/output-policy.js +50 -0
- package/dist/terminal/spinner-registry.d.ts +8 -0
- package/dist/terminal/spinner-registry.js +30 -0
- package/package.json +61 -0
|
@@ -0,0 +1,917 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import readline from "node:readline";
|
|
5
|
+
import { formatRequestTranscript as formatRequestTranscriptForLog, formatResponseTranscript as formatResponseTranscriptForLog } from "./runtime-transcript.js";
|
|
6
|
+
const MCP_PROTOCOL_VERSION = "2025-11-25";
|
|
7
|
+
const PROMPT_PROBE_PLACEHOLDER = "codex-plugin-doctor-probe";
|
|
8
|
+
const METHOD_NOT_FOUND = -32601;
|
|
9
|
+
const MAX_TOOL_CALL_CONTENT_LENGTH = 4096;
|
|
10
|
+
const MAX_RESOURCE_READ_CONTENT_LENGTH = 4096;
|
|
11
|
+
const MAX_PROMPT_GET_CONTENT_LENGTH = 4096;
|
|
12
|
+
function createRuntimeScorecard() {
|
|
13
|
+
return {
|
|
14
|
+
initialize: "skipped",
|
|
15
|
+
toolsList: "unsupported",
|
|
16
|
+
toolsCall: "unsupported",
|
|
17
|
+
resourcesList: "unsupported",
|
|
18
|
+
resourceRead: "unsupported",
|
|
19
|
+
resourceTemplatesList: "unsupported",
|
|
20
|
+
promptsList: "unsupported",
|
|
21
|
+
promptGet: "unsupported"
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function buildFailure(id, message, impact, suggestedFix) {
|
|
25
|
+
return {
|
|
26
|
+
id,
|
|
27
|
+
severity: "fail",
|
|
28
|
+
message,
|
|
29
|
+
impact,
|
|
30
|
+
suggestedFix
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function buildWarning(id, message, impact, suggestedFix) {
|
|
34
|
+
return {
|
|
35
|
+
id,
|
|
36
|
+
severity: "warn",
|
|
37
|
+
message,
|
|
38
|
+
impact,
|
|
39
|
+
suggestedFix
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function isPlainObject(value) {
|
|
43
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
44
|
+
}
|
|
45
|
+
function isFinding(value) {
|
|
46
|
+
return (isPlainObject(value) &&
|
|
47
|
+
typeof value.id === "string" &&
|
|
48
|
+
(value.severity === "fail" || value.severity === "warn") &&
|
|
49
|
+
typeof value.message === "string" &&
|
|
50
|
+
typeof value.impact === "string" &&
|
|
51
|
+
typeof value.suggestedFix === "string");
|
|
52
|
+
}
|
|
53
|
+
function isErrorResponse(message) {
|
|
54
|
+
return isPlainObject(message.error);
|
|
55
|
+
}
|
|
56
|
+
function getErrorObject(message) {
|
|
57
|
+
return isErrorResponse(message) ? message.error : null;
|
|
58
|
+
}
|
|
59
|
+
function getErrorCode(message) {
|
|
60
|
+
const error = getErrorObject(message);
|
|
61
|
+
return error && typeof error.code === "number" ? error.code : null;
|
|
62
|
+
}
|
|
63
|
+
function sanitizeTranscriptValue(value, pathSegments = []) {
|
|
64
|
+
const currentKey = pathSegments[pathSegments.length - 1];
|
|
65
|
+
if (typeof value === "string") {
|
|
66
|
+
if (currentKey === "text" ||
|
|
67
|
+
currentKey === "blob" ||
|
|
68
|
+
currentKey === "data" ||
|
|
69
|
+
currentKey === "diff" ||
|
|
70
|
+
currentKey === "arguments" ||
|
|
71
|
+
/(token|secret|password|api[_-]?key|private[_-]?key)/i.test(currentKey ?? "") ||
|
|
72
|
+
pathSegments.includes("arguments")) {
|
|
73
|
+
return "[REDACTED]";
|
|
74
|
+
}
|
|
75
|
+
if (value.length > 80) {
|
|
76
|
+
return "[REDACTED]";
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
if (Array.isArray(value)) {
|
|
81
|
+
return value.map((entry, index) => sanitizeTranscriptValue(entry, [...pathSegments, String(index)]));
|
|
82
|
+
}
|
|
83
|
+
if (isPlainObject(value)) {
|
|
84
|
+
return Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [
|
|
85
|
+
key,
|
|
86
|
+
sanitizeTranscriptValue(entryValue, [...pathSegments, key])
|
|
87
|
+
]));
|
|
88
|
+
}
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
function formatRequestTranscript(method, params) {
|
|
92
|
+
if (!params) {
|
|
93
|
+
return `-> ${method}`;
|
|
94
|
+
}
|
|
95
|
+
return `-> ${method} ${JSON.stringify(sanitizeTranscriptValue(params))}`;
|
|
96
|
+
}
|
|
97
|
+
function formatResponseTranscript(method, message) {
|
|
98
|
+
const error = getErrorObject(message);
|
|
99
|
+
if (error) {
|
|
100
|
+
const code = typeof error.code === "number" ? error.code : "?";
|
|
101
|
+
const messageText = typeof error.message === "string" ? error.message : "error";
|
|
102
|
+
return `<- ${method} error {"code":${code},"message":"${messageText}"}`;
|
|
103
|
+
}
|
|
104
|
+
if (!isPlainObject(message.result)) {
|
|
105
|
+
return `<- ${method} result`;
|
|
106
|
+
}
|
|
107
|
+
const result = message.result;
|
|
108
|
+
switch (method) {
|
|
109
|
+
case "initialize":
|
|
110
|
+
return `<- initialize ${JSON.stringify({
|
|
111
|
+
protocolVersion: result.protocolVersion,
|
|
112
|
+
capabilities: isPlainObject(result.capabilities)
|
|
113
|
+
? Object.keys(result.capabilities)
|
|
114
|
+
: []
|
|
115
|
+
})}`;
|
|
116
|
+
case "tools/list":
|
|
117
|
+
return `<- tools/list ${JSON.stringify({
|
|
118
|
+
tools: Array.isArray(result.tools) ? result.tools.length : 0,
|
|
119
|
+
nextCursor: typeof result.nextCursor === "string" ? "[CURSOR]" : undefined
|
|
120
|
+
})}`;
|
|
121
|
+
case "tools/call":
|
|
122
|
+
return `<- tools/call ${JSON.stringify({
|
|
123
|
+
content: Array.isArray(result.content) ? result.content.length : 0
|
|
124
|
+
})}`;
|
|
125
|
+
case "resources/list":
|
|
126
|
+
return `<- resources/list ${JSON.stringify({
|
|
127
|
+
resources: Array.isArray(result.resources) ? result.resources.length : 0,
|
|
128
|
+
nextCursor: typeof result.nextCursor === "string" ? "[CURSOR]" : undefined
|
|
129
|
+
})}`;
|
|
130
|
+
case "resources/read":
|
|
131
|
+
return `<- resources/read ${JSON.stringify({
|
|
132
|
+
contents: Array.isArray(result.contents) ? result.contents.length : 0
|
|
133
|
+
})}`;
|
|
134
|
+
case "resources/templates/list":
|
|
135
|
+
return `<- resources/templates/list ${JSON.stringify({
|
|
136
|
+
resourceTemplates: Array.isArray(result.resourceTemplates)
|
|
137
|
+
? result.resourceTemplates.length
|
|
138
|
+
: 0,
|
|
139
|
+
nextCursor: typeof result.nextCursor === "string" ? "[CURSOR]" : undefined
|
|
140
|
+
})}`;
|
|
141
|
+
case "prompts/list":
|
|
142
|
+
return `<- prompts/list ${JSON.stringify({
|
|
143
|
+
prompts: Array.isArray(result.prompts) ? result.prompts.length : 0,
|
|
144
|
+
nextCursor: typeof result.nextCursor === "string" ? "[CURSOR]" : undefined
|
|
145
|
+
})}`;
|
|
146
|
+
case "prompts/get":
|
|
147
|
+
return `<- prompts/get ${JSON.stringify({
|
|
148
|
+
messages: Array.isArray(result.messages) ? result.messages.length : 0
|
|
149
|
+
})}`;
|
|
150
|
+
default:
|
|
151
|
+
return `<- ${method}`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function fileExists(targetPath) {
|
|
155
|
+
try {
|
|
156
|
+
const details = await stat(targetPath);
|
|
157
|
+
return details.isFile();
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async function loadMcpServers(discoveredPackage) {
|
|
164
|
+
const { manifest, rootPath } = discoveredPackage;
|
|
165
|
+
if (!manifest.mcpServers) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
const mcpConfigPath = path.resolve(rootPath, manifest.mcpServers);
|
|
169
|
+
const exists = await fileExists(mcpConfigPath);
|
|
170
|
+
if (!exists) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
let parsedConfig;
|
|
174
|
+
try {
|
|
175
|
+
parsedConfig = JSON.parse(await readFile(mcpConfigPath, "utf8"));
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
if (!isPlainObject(parsedConfig)) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
const servers = parsedConfig.mcpServers;
|
|
184
|
+
if (!isPlainObject(servers)) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
return servers;
|
|
188
|
+
}
|
|
189
|
+
function getCapabilities(message) {
|
|
190
|
+
if (!isPlainObject(message.result)) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
const capabilities = message.result.capabilities;
|
|
194
|
+
return isPlainObject(capabilities) ? capabilities : null;
|
|
195
|
+
}
|
|
196
|
+
function hasToolsCapability(message) {
|
|
197
|
+
const capabilities = getCapabilities(message);
|
|
198
|
+
return capabilities !== null && isPlainObject(capabilities.tools);
|
|
199
|
+
}
|
|
200
|
+
function hasResourcesCapability(message) {
|
|
201
|
+
const capabilities = getCapabilities(message);
|
|
202
|
+
return capabilities !== null && isPlainObject(capabilities.resources);
|
|
203
|
+
}
|
|
204
|
+
function hasPromptsCapability(message) {
|
|
205
|
+
const capabilities = getCapabilities(message);
|
|
206
|
+
return capabilities !== null && isPlainObject(capabilities.prompts);
|
|
207
|
+
}
|
|
208
|
+
function getNextCursor(result) {
|
|
209
|
+
if (result.nextCursor === undefined) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
return typeof result.nextCursor === "string" ? result.nextCursor : "__invalid__";
|
|
213
|
+
}
|
|
214
|
+
function extractToolsPage(message) {
|
|
215
|
+
if (!isPlainObject(message.result)) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
const tools = message.result.tools;
|
|
219
|
+
if (!Array.isArray(tools)) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
const parsedTools = [];
|
|
223
|
+
for (const tool of tools) {
|
|
224
|
+
if (!isPlainObject(tool) ||
|
|
225
|
+
typeof tool.name !== "string" ||
|
|
226
|
+
!isPlainObject(tool.inputSchema) ||
|
|
227
|
+
tool.inputSchema.type !== "object") {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
parsedTools.push({
|
|
231
|
+
name: tool.name,
|
|
232
|
+
inputSchema: tool.inputSchema
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
const nextCursor = getNextCursor(message.result);
|
|
236
|
+
if (nextCursor === "__invalid__") {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
items: parsedTools,
|
|
241
|
+
nextCursor
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function extractResourcesPage(message) {
|
|
245
|
+
if (!isPlainObject(message.result)) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
const resources = message.result.resources;
|
|
249
|
+
if (!Array.isArray(resources)) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
const parsedResources = [];
|
|
253
|
+
for (const resource of resources) {
|
|
254
|
+
if (!isPlainObject(resource) ||
|
|
255
|
+
typeof resource.name !== "string" ||
|
|
256
|
+
typeof resource.uri !== "string") {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
parsedResources.push({
|
|
260
|
+
name: resource.name,
|
|
261
|
+
uri: resource.uri
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
const nextCursor = getNextCursor(message.result);
|
|
265
|
+
if (nextCursor === "__invalid__") {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
items: parsedResources,
|
|
270
|
+
nextCursor
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
function extractResourceTemplatesPage(message) {
|
|
274
|
+
if (!isPlainObject(message.result)) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
const resourceTemplates = message.result.resourceTemplates;
|
|
278
|
+
if (!Array.isArray(resourceTemplates)) {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
const parsedTemplates = [];
|
|
282
|
+
for (const template of resourceTemplates) {
|
|
283
|
+
if (!isPlainObject(template) ||
|
|
284
|
+
typeof template.name !== "string" ||
|
|
285
|
+
typeof template.uriTemplate !== "string") {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
parsedTemplates.push({
|
|
289
|
+
name: template.name,
|
|
290
|
+
uriTemplate: template.uriTemplate
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
const nextCursor = getNextCursor(message.result);
|
|
294
|
+
if (nextCursor === "__invalid__") {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
items: parsedTemplates,
|
|
299
|
+
nextCursor
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function extractPromptsPage(message) {
|
|
303
|
+
if (!isPlainObject(message.result)) {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
const prompts = message.result.prompts;
|
|
307
|
+
if (!Array.isArray(prompts)) {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
const parsedPrompts = [];
|
|
311
|
+
for (const prompt of prompts) {
|
|
312
|
+
if (!isPlainObject(prompt) || typeof prompt.name !== "string") {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
let promptArguments;
|
|
316
|
+
if (prompt.arguments !== undefined) {
|
|
317
|
+
if (!Array.isArray(prompt.arguments)) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
promptArguments = [];
|
|
321
|
+
for (const argument of prompt.arguments) {
|
|
322
|
+
if (!isPlainObject(argument) ||
|
|
323
|
+
typeof argument.name !== "string" ||
|
|
324
|
+
(argument.required !== undefined &&
|
|
325
|
+
typeof argument.required !== "boolean")) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
promptArguments.push({
|
|
329
|
+
name: argument.name,
|
|
330
|
+
required: typeof argument.required === "boolean"
|
|
331
|
+
? argument.required
|
|
332
|
+
: undefined
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
parsedPrompts.push({
|
|
337
|
+
name: prompt.name,
|
|
338
|
+
arguments: promptArguments
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
const nextCursor = getNextCursor(message.result);
|
|
342
|
+
if (nextCursor === "__invalid__") {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
return {
|
|
346
|
+
items: parsedPrompts,
|
|
347
|
+
nextCursor
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function isDestructiveTool(tool) {
|
|
351
|
+
return /(delete|remove|drop|destroy|erase|wipe|purge|send|deploy|refund|payment|charge|merge|push)/i.test(tool.name);
|
|
352
|
+
}
|
|
353
|
+
function buildSchemaValue(schema) {
|
|
354
|
+
if (!isPlainObject(schema)) {
|
|
355
|
+
return undefined;
|
|
356
|
+
}
|
|
357
|
+
if (Array.isArray(schema.enum) && schema.enum.length > 0) {
|
|
358
|
+
return schema.enum[0];
|
|
359
|
+
}
|
|
360
|
+
switch (schema.type) {
|
|
361
|
+
case "string":
|
|
362
|
+
return "codex-plugin-doctor-sample";
|
|
363
|
+
case "integer":
|
|
364
|
+
return 1;
|
|
365
|
+
case "number":
|
|
366
|
+
return 1.5;
|
|
367
|
+
case "boolean":
|
|
368
|
+
return false;
|
|
369
|
+
case "array": {
|
|
370
|
+
const itemValue = buildSchemaValue(schema.items);
|
|
371
|
+
return itemValue === undefined ? undefined : [itemValue];
|
|
372
|
+
}
|
|
373
|
+
case "object": {
|
|
374
|
+
const properties = isPlainObject(schema.properties) ? schema.properties : {};
|
|
375
|
+
const required = Array.isArray(schema.required)
|
|
376
|
+
? schema.required.filter((value) => typeof value === "string")
|
|
377
|
+
: [];
|
|
378
|
+
const value = {};
|
|
379
|
+
for (const key of required) {
|
|
380
|
+
const propertyValue = buildSchemaValue(properties[key]);
|
|
381
|
+
if (propertyValue === undefined) {
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
value[key] = propertyValue;
|
|
385
|
+
}
|
|
386
|
+
return value;
|
|
387
|
+
}
|
|
388
|
+
default:
|
|
389
|
+
return undefined;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
function buildToolArguments(tool) {
|
|
393
|
+
const schemaValue = buildSchemaValue(tool.inputSchema);
|
|
394
|
+
if (schemaValue === undefined) {
|
|
395
|
+
return undefined;
|
|
396
|
+
}
|
|
397
|
+
return isPlainObject(schemaValue) ? schemaValue : undefined;
|
|
398
|
+
}
|
|
399
|
+
function findCallableTool(tools) {
|
|
400
|
+
for (const tool of tools) {
|
|
401
|
+
if (isDestructiveTool(tool)) {
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
const args = buildToolArguments(tool);
|
|
405
|
+
if (args !== undefined) {
|
|
406
|
+
return {
|
|
407
|
+
tool,
|
|
408
|
+
args
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
function buildPromptArguments(prompt) {
|
|
415
|
+
if (!prompt.arguments || prompt.arguments.length === 0) {
|
|
416
|
+
return undefined;
|
|
417
|
+
}
|
|
418
|
+
const requiredArguments = prompt.arguments.filter((argument) => argument.required);
|
|
419
|
+
if (requiredArguments.length === 0) {
|
|
420
|
+
return undefined;
|
|
421
|
+
}
|
|
422
|
+
return Object.fromEntries(requiredArguments.map((argument) => [argument.name, PROMPT_PROBE_PLACEHOLDER]));
|
|
423
|
+
}
|
|
424
|
+
function findPromptForGet(prompts) {
|
|
425
|
+
return prompts[0] ?? null;
|
|
426
|
+
}
|
|
427
|
+
function isValidContentBlock(value) {
|
|
428
|
+
if (!isPlainObject(value) || typeof value.type !== "string") {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
switch (value.type) {
|
|
432
|
+
case "text":
|
|
433
|
+
return typeof value.text === "string";
|
|
434
|
+
case "image":
|
|
435
|
+
case "audio":
|
|
436
|
+
return typeof value.data === "string" && typeof value.mimeType === "string";
|
|
437
|
+
case "resource":
|
|
438
|
+
return (isPlainObject(value.resource) &&
|
|
439
|
+
typeof value.resource.uri === "string" &&
|
|
440
|
+
(typeof value.resource.text === "string" ||
|
|
441
|
+
typeof value.resource.blob === "string"));
|
|
442
|
+
case "resource_link":
|
|
443
|
+
return typeof value.uri === "string";
|
|
444
|
+
default:
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
function isValidCallToolResult(message) {
|
|
449
|
+
if (!isPlainObject(message.result)) {
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
const result = message.result;
|
|
453
|
+
if (!Array.isArray(result.content)) {
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
if (result.structuredContent !== undefined &&
|
|
457
|
+
!isPlainObject(result.structuredContent)) {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
if (result.isError !== undefined && typeof result.isError !== "boolean") {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
return result.content.every((content) => isValidContentBlock(content));
|
|
464
|
+
}
|
|
465
|
+
function isValidReadResourceResult(message) {
|
|
466
|
+
if (!isPlainObject(message.result)) {
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
const contents = message.result.contents;
|
|
470
|
+
if (!Array.isArray(contents)) {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
return contents.every((content) => {
|
|
474
|
+
if (!isPlainObject(content) || typeof content.uri !== "string") {
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
const hasText = typeof content.text === "string";
|
|
478
|
+
const hasBlob = typeof content.blob === "string";
|
|
479
|
+
return hasText || hasBlob;
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
function isValidPromptGetResult(message) {
|
|
483
|
+
if (!isPlainObject(message.result)) {
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
const messages = message.result.messages;
|
|
487
|
+
if (!Array.isArray(messages)) {
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
return messages.every((promptMessage) => {
|
|
491
|
+
return (isPlainObject(promptMessage) &&
|
|
492
|
+
(promptMessage.role === "user" || promptMessage.role === "assistant") &&
|
|
493
|
+
isValidContentBlock(promptMessage.content));
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
function getContentBlockLength(content) {
|
|
497
|
+
if (content.type === "text" && typeof content.text === "string") {
|
|
498
|
+
return content.text.length;
|
|
499
|
+
}
|
|
500
|
+
if ((content.type === "image" || content.type === "audio") &&
|
|
501
|
+
typeof content.data === "string") {
|
|
502
|
+
return content.data.length;
|
|
503
|
+
}
|
|
504
|
+
if (content.type === "resource" && isPlainObject(content.resource)) {
|
|
505
|
+
if (typeof content.resource.text === "string") {
|
|
506
|
+
return content.resource.text.length;
|
|
507
|
+
}
|
|
508
|
+
if (typeof content.resource.blob === "string") {
|
|
509
|
+
return content.resource.blob.length;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return 0;
|
|
513
|
+
}
|
|
514
|
+
function collectOversizedToolCallWarnings(message) {
|
|
515
|
+
if (!isPlainObject(message.result) || !Array.isArray(message.result.content)) {
|
|
516
|
+
return [];
|
|
517
|
+
}
|
|
518
|
+
const hasOversizedContent = message.result.content.some((content) => {
|
|
519
|
+
return isPlainObject(content) && getContentBlockLength(content) > MAX_TOOL_CALL_CONTENT_LENGTH;
|
|
520
|
+
});
|
|
521
|
+
if (!hasOversizedContent) {
|
|
522
|
+
return [];
|
|
523
|
+
}
|
|
524
|
+
return [
|
|
525
|
+
buildWarning("plugin.runtime.tool_call.content_too_large", "The tools/call response contains unusually large content payloads.", "Large tool call payloads increase runtime cost, transcript size, and the risk of degraded model performance.", "Keep tool responses concise or move large payloads into resource references instead of inline content.")
|
|
526
|
+
];
|
|
527
|
+
}
|
|
528
|
+
function collectOversizedResourceReadWarnings(message) {
|
|
529
|
+
if (!isPlainObject(message.result) || !Array.isArray(message.result.contents)) {
|
|
530
|
+
return [];
|
|
531
|
+
}
|
|
532
|
+
const hasOversizedContent = message.result.contents.some((content) => {
|
|
533
|
+
if (!isPlainObject(content)) {
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
const textLength = typeof content.text === "string" ? content.text.length : 0;
|
|
537
|
+
const blobLength = typeof content.blob === "string" ? content.blob.length : 0;
|
|
538
|
+
return (textLength > MAX_RESOURCE_READ_CONTENT_LENGTH ||
|
|
539
|
+
blobLength > MAX_RESOURCE_READ_CONTENT_LENGTH);
|
|
540
|
+
});
|
|
541
|
+
if (!hasOversizedContent) {
|
|
542
|
+
return [];
|
|
543
|
+
}
|
|
544
|
+
return [
|
|
545
|
+
buildWarning("plugin.runtime.resource_read.content_too_large", "The resources/read response contains unusually large resource payloads.", "Large resource contents increase runtime cost and can make downstream model use less reliable.", "Reduce inline resource size or break large resources into smaller references.")
|
|
546
|
+
];
|
|
547
|
+
}
|
|
548
|
+
function collectOversizedPromptGetWarnings(message) {
|
|
549
|
+
if (!isPlainObject(message.result) || !Array.isArray(message.result.messages)) {
|
|
550
|
+
return [];
|
|
551
|
+
}
|
|
552
|
+
const hasOversizedContent = message.result.messages.some((promptMessage) => {
|
|
553
|
+
if (!isPlainObject(promptMessage) || !isPlainObject(promptMessage.content)) {
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
return getContentBlockLength(promptMessage.content) > MAX_PROMPT_GET_CONTENT_LENGTH;
|
|
557
|
+
});
|
|
558
|
+
if (!hasOversizedContent) {
|
|
559
|
+
return [];
|
|
560
|
+
}
|
|
561
|
+
return [
|
|
562
|
+
buildWarning("plugin.runtime.prompt_get.content_too_large", "The prompts/get response contains unusually large prompt message payloads.", "Large prompt bodies increase token cost and reduce the usefulness of prompt probing as a lightweight validation step.", "Keep prompt message content concise or move large context into resources.")
|
|
563
|
+
];
|
|
564
|
+
}
|
|
565
|
+
async function probeCommandServer(input) {
|
|
566
|
+
const { serverName, command, args, cwd, startupTimeoutMs, transcript } = input;
|
|
567
|
+
return new Promise((resolve) => {
|
|
568
|
+
const scorecard = createRuntimeScorecard();
|
|
569
|
+
const warnings = [];
|
|
570
|
+
let settled = false;
|
|
571
|
+
let stderrPreview = "";
|
|
572
|
+
let finalizeRequested = false;
|
|
573
|
+
let nextRequestId = 1;
|
|
574
|
+
const child = spawn(command, args, {
|
|
575
|
+
cwd,
|
|
576
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
577
|
+
});
|
|
578
|
+
const stdoutReader = readline.createInterface({
|
|
579
|
+
input: child.stdout,
|
|
580
|
+
crlfDelay: Infinity
|
|
581
|
+
});
|
|
582
|
+
const pendingRequests = new Map();
|
|
583
|
+
const settle = (finding) => {
|
|
584
|
+
if (settled) {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
settled = true;
|
|
588
|
+
stdoutReader.close();
|
|
589
|
+
for (const pendingRequest of pendingRequests.values()) {
|
|
590
|
+
clearTimeout(pendingRequest.timer);
|
|
591
|
+
}
|
|
592
|
+
pendingRequests.clear();
|
|
593
|
+
if (child.exitCode === null && !child.killed) {
|
|
594
|
+
finalizeRequested = true;
|
|
595
|
+
child.stdin.end();
|
|
596
|
+
child.kill("SIGTERM");
|
|
597
|
+
}
|
|
598
|
+
resolve({
|
|
599
|
+
findings: [...warnings, ...(finding ? [finding] : [])],
|
|
600
|
+
scorecard
|
|
601
|
+
});
|
|
602
|
+
};
|
|
603
|
+
const sendRequest = (method, params, timeoutFinding) => new Promise((requestResolve, requestReject) => {
|
|
604
|
+
const id = nextRequestId++;
|
|
605
|
+
transcript?.(formatRequestTranscriptForLog(method, params));
|
|
606
|
+
const timer = setTimeout(() => {
|
|
607
|
+
pendingRequests.delete(id);
|
|
608
|
+
requestReject(timeoutFinding);
|
|
609
|
+
}, startupTimeoutMs);
|
|
610
|
+
pendingRequests.set(id, {
|
|
611
|
+
method,
|
|
612
|
+
resolve: requestResolve,
|
|
613
|
+
reject: requestReject,
|
|
614
|
+
timer
|
|
615
|
+
});
|
|
616
|
+
child.stdin.write(`${JSON.stringify({
|
|
617
|
+
jsonrpc: "2.0",
|
|
618
|
+
id,
|
|
619
|
+
method,
|
|
620
|
+
...(params ? { params } : {})
|
|
621
|
+
})}\n`);
|
|
622
|
+
});
|
|
623
|
+
const sendNotification = (method, params) => {
|
|
624
|
+
transcript?.(formatRequestTranscriptForLog(method, params));
|
|
625
|
+
child.stdin.write(`${JSON.stringify({
|
|
626
|
+
jsonrpc: "2.0",
|
|
627
|
+
method,
|
|
628
|
+
...(params ? { params } : {})
|
|
629
|
+
})}\n`);
|
|
630
|
+
};
|
|
631
|
+
const fetchPaginated = async (input) => {
|
|
632
|
+
let cursor = null;
|
|
633
|
+
const items = [];
|
|
634
|
+
do {
|
|
635
|
+
const response = await sendRequest(input.method, cursor ? { cursor } : undefined, input.timeoutFinding);
|
|
636
|
+
transcript?.(formatResponseTranscriptForLog(input.method, response));
|
|
637
|
+
if (isErrorResponse(response)) {
|
|
638
|
+
if (getErrorCode(response) === METHOD_NOT_FOUND && input.onMethodNotFound) {
|
|
639
|
+
input.onMethodNotFound();
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
return null;
|
|
643
|
+
}
|
|
644
|
+
const page = input.extractPage(response);
|
|
645
|
+
if (!page) {
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
items.push(...page.items);
|
|
649
|
+
cursor = page.nextCursor;
|
|
650
|
+
} while (cursor);
|
|
651
|
+
return items;
|
|
652
|
+
};
|
|
653
|
+
child.stderr?.on("data", (chunk) => {
|
|
654
|
+
if (stderrPreview.length >= 160) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
stderrPreview += chunk.toString();
|
|
658
|
+
});
|
|
659
|
+
stdoutReader.on("line", (line) => {
|
|
660
|
+
if (settled) {
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
let message;
|
|
664
|
+
try {
|
|
665
|
+
message = JSON.parse(line);
|
|
666
|
+
}
|
|
667
|
+
catch {
|
|
668
|
+
settle(buildFailure("plugin.runtime.protocol.invalid_message", `The MCP server \`${serverName}\` wrote a non-JSON line to stdout.`, "MCP stdio transport requires newline-delimited JSON-RPC messages on stdout, so non-JSON output breaks protocol communication.", "Ensure the server only writes valid MCP messages to stdout and sends logs to stderr instead."));
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
if (!isPlainObject(message)) {
|
|
672
|
+
settle(buildFailure("plugin.runtime.protocol.invalid_message", `The MCP server \`${serverName}\` wrote a non-object message to stdout.`, "MCP stdio transport requires JSON-RPC objects for requests, responses, and notifications.", "Ensure the server writes JSON-RPC objects to stdout."));
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
const id = message.id;
|
|
676
|
+
if (typeof id === "number" && pendingRequests.has(id)) {
|
|
677
|
+
const pendingRequest = pendingRequests.get(id);
|
|
678
|
+
if (!pendingRequest) {
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
clearTimeout(pendingRequest.timer);
|
|
682
|
+
pendingRequests.delete(id);
|
|
683
|
+
transcript?.(formatResponseTranscriptForLog(pendingRequest.method, message));
|
|
684
|
+
pendingRequest.resolve(message);
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
child.on("error", () => {
|
|
688
|
+
scorecard.initialize = "fail";
|
|
689
|
+
settle(buildFailure("plugin.runtime.startup.failed", `The MCP server \`${serverName}\` could not be started.`, "The configured stdio server could not be launched, so runtime validation cannot proceed.", `Verify the command \`${command}\` is installed and executable from \`${cwd}\`.`));
|
|
690
|
+
});
|
|
691
|
+
child.on("exit", () => {
|
|
692
|
+
if (settled || finalizeRequested) {
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
if (scorecard.initialize === "skipped") {
|
|
696
|
+
scorecard.initialize = "fail";
|
|
697
|
+
}
|
|
698
|
+
settle(buildFailure("plugin.runtime.exited_early", `The MCP server \`${serverName}\` exited before the startup probe completed.`, "A server that exits immediately is unlikely to remain available for Codex during normal use.", stderrPreview.trim().length > 0
|
|
699
|
+
? `Inspect the startup error output: ${stderrPreview.trim()}`
|
|
700
|
+
: `Keep the \`${serverName}\` process running after startup and inspect its command or arguments.`));
|
|
701
|
+
});
|
|
702
|
+
const runProtocolProbe = async () => {
|
|
703
|
+
const initializeResponse = await sendRequest("initialize", {
|
|
704
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
705
|
+
capabilities: {},
|
|
706
|
+
clientInfo: {
|
|
707
|
+
name: "codex-plugin-doctor",
|
|
708
|
+
version: "0.1.0"
|
|
709
|
+
}
|
|
710
|
+
}, buildFailure("plugin.runtime.initialize.timeout", `The MCP server \`${serverName}\` did not answer the initialize request in time.`, "If initialize never completes, Codex cannot negotiate protocol capabilities with this server.", "Reduce server startup latency or inspect why the initialize request is not being handled."));
|
|
711
|
+
if (!isPlainObject(initializeResponse.result)) {
|
|
712
|
+
scorecard.initialize = "fail";
|
|
713
|
+
settle(buildFailure("plugin.runtime.initialize.invalid", `The MCP server \`${serverName}\` returned an invalid initialize result.`, "A malformed initialize response means the server is not completing the MCP handshake correctly.", "Return `protocolVersion`, `capabilities`, and `serverInfo` in the initialize result."));
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
const result = initializeResponse.result;
|
|
717
|
+
if (typeof result.protocolVersion !== "string" ||
|
|
718
|
+
!isPlainObject(result.capabilities) ||
|
|
719
|
+
!isPlainObject(result.serverInfo) ||
|
|
720
|
+
typeof result.serverInfo.name !== "string" ||
|
|
721
|
+
typeof result.serverInfo.version !== "string") {
|
|
722
|
+
scorecard.initialize = "fail";
|
|
723
|
+
settle(buildFailure("plugin.runtime.initialize.invalid", `The MCP server \`${serverName}\` returned an invalid initialize result.`, "A malformed initialize response means the server is not completing the MCP handshake correctly.", "Return `protocolVersion`, `capabilities`, and `serverInfo` in the initialize result."));
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
scorecard.initialize = "pass";
|
|
727
|
+
sendNotification("notifications/initialized");
|
|
728
|
+
if (!hasToolsCapability(initializeResponse)) {
|
|
729
|
+
scorecard.toolsList = "unsupported";
|
|
730
|
+
scorecard.toolsCall = "unsupported";
|
|
731
|
+
settle(buildWarning("plugin.runtime.tools.unsupported", `The MCP server \`${serverName}\` does not advertise tools capability.`, "Codex cannot use `tools/list` for deeper validation if the server does not declare tools support.", "Expose `capabilities.tools` during initialize if this server is expected to provide tools."));
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
const tools = await fetchPaginated({
|
|
735
|
+
method: "tools/list",
|
|
736
|
+
timeoutFinding: buildFailure("plugin.runtime.tools_list.timeout", `The MCP server \`${serverName}\` did not answer the tools/list request in time.`, "A server that cannot return its tool catalog in time will feel broken or invisible in Codex.", "Inspect the tool discovery path and reduce latency before returning the tool list."),
|
|
737
|
+
extractPage: extractToolsPage
|
|
738
|
+
});
|
|
739
|
+
if (!tools) {
|
|
740
|
+
scorecard.toolsList = "fail";
|
|
741
|
+
settle(buildFailure("plugin.runtime.tools_list.invalid", `The MCP server \`${serverName}\` returned an invalid tools/list result.`, "Codex cannot safely consume malformed tool definitions from `tools/list`.", "Return a `tools` array where every tool has a string `name` and an object-shaped `inputSchema` with `type: \"object\"`."));
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
scorecard.toolsList = "pass";
|
|
745
|
+
const callableTool = findCallableTool(tools);
|
|
746
|
+
if (!callableTool) {
|
|
747
|
+
scorecard.toolsCall = "skipped";
|
|
748
|
+
settle(buildWarning("plugin.runtime.tool_call.skipped", `The MCP server \`${serverName}\` does not expose a safely callable tool for probing.`, "The validator confirmed tool discovery but could not safely perform a non-destructive `tools/call` probe.", "Expose at least one non-destructive tool with a JSON schema the validator can generate arguments for."));
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
else {
|
|
752
|
+
const toolCallResponse = await sendRequest("tools/call", {
|
|
753
|
+
name: callableTool.tool.name,
|
|
754
|
+
arguments: callableTool.args
|
|
755
|
+
}, buildFailure("plugin.runtime.tool_call.timeout", `The MCP server \`${serverName}\` did not answer the tools/call request in time.`, "A server that cannot complete a basic tool invocation in time will feel broken in real Codex usage.", "Inspect the selected tool implementation and reduce its response latency."));
|
|
756
|
+
if (isErrorResponse(toolCallResponse) || !isValidCallToolResult(toolCallResponse)) {
|
|
757
|
+
scorecard.toolsCall = "fail";
|
|
758
|
+
settle(buildFailure("plugin.runtime.tool_call.invalid", `The MCP server \`${serverName}\` returned an invalid tools/call result.`, "Codex cannot safely consume malformed tool call results from the server.", "Return a CallToolResult with a `content` array containing valid MCP content blocks."));
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
scorecard.toolsCall = "pass";
|
|
762
|
+
warnings.push(...collectOversizedToolCallWarnings(toolCallResponse));
|
|
763
|
+
}
|
|
764
|
+
if (!hasResourcesCapability(initializeResponse)) {
|
|
765
|
+
scorecard.resourcesList = "unsupported";
|
|
766
|
+
scorecard.resourceRead = "unsupported";
|
|
767
|
+
scorecard.resourceTemplatesList = "unsupported";
|
|
768
|
+
}
|
|
769
|
+
else {
|
|
770
|
+
const resources = await fetchPaginated({
|
|
771
|
+
method: "resources/list",
|
|
772
|
+
timeoutFinding: buildFailure("plugin.runtime.resources_list.timeout", `The MCP server \`${serverName}\` did not answer the resources/list request in time.`, "A server that advertises resources support but cannot list resources in time will feel incomplete or broken in Codex.", "Inspect the resources implementation and reduce latency before returning the resource list."),
|
|
773
|
+
extractPage: extractResourcesPage
|
|
774
|
+
});
|
|
775
|
+
if (!resources) {
|
|
776
|
+
scorecard.resourcesList = "fail";
|
|
777
|
+
settle(buildFailure("plugin.runtime.resources_list.invalid", `The MCP server \`${serverName}\` returned an invalid resources/list result.`, "Codex cannot safely consume malformed resource definitions from `resources/list`.", "Return a `resources` array where every resource has at least a string `name` and string `uri`."));
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
scorecard.resourcesList = "pass";
|
|
781
|
+
if (resources.length > 0) {
|
|
782
|
+
const resourceReadResponse = await sendRequest("resources/read", {
|
|
783
|
+
uri: resources[0].uri
|
|
784
|
+
}, buildFailure("plugin.runtime.resource_read.timeout", `The MCP server \`${serverName}\` did not answer the resources/read request in time.`, "A server that lists resources but cannot read them in time will feel broken in Codex.", "Inspect the resource read path and reduce latency before returning resource contents."));
|
|
785
|
+
if (isErrorResponse(resourceReadResponse) ||
|
|
786
|
+
!isValidReadResourceResult(resourceReadResponse)) {
|
|
787
|
+
scorecard.resourceRead = "fail";
|
|
788
|
+
settle(buildFailure("plugin.runtime.resource_read.invalid", `The MCP server \`${serverName}\` returned an invalid resources/read result.`, "Codex cannot safely consume malformed resource contents from `resources/read`.", "Return a `contents` array where each entry has a string `uri` plus either string `text` or string `blob`."));
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
scorecard.resourceRead = "pass";
|
|
792
|
+
warnings.push(...collectOversizedResourceReadWarnings(resourceReadResponse));
|
|
793
|
+
}
|
|
794
|
+
else {
|
|
795
|
+
scorecard.resourceRead = "skipped";
|
|
796
|
+
}
|
|
797
|
+
scorecard.resourceTemplatesList = "skipped";
|
|
798
|
+
let resourceTemplatesUnsupported = false;
|
|
799
|
+
const resourceTemplates = await fetchPaginated({
|
|
800
|
+
method: "resources/templates/list",
|
|
801
|
+
timeoutFinding: buildFailure("plugin.runtime.resource_templates_list.timeout", `The MCP server \`${serverName}\` did not answer the resources/templates/list request in time.`, "A server that advertises resources support but cannot list resource templates in time will feel incomplete in Codex.", "Inspect the resource template implementation and reduce latency before returning template definitions."),
|
|
802
|
+
extractPage: extractResourceTemplatesPage,
|
|
803
|
+
onMethodNotFound: () => {
|
|
804
|
+
resourceTemplatesUnsupported = true;
|
|
805
|
+
scorecard.resourceTemplatesList = "unsupported";
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
if (!resourceTemplates) {
|
|
809
|
+
if (!resourceTemplatesUnsupported) {
|
|
810
|
+
scorecard.resourceTemplatesList = "fail";
|
|
811
|
+
settle(buildFailure("plugin.runtime.resource_templates_list.invalid", `The MCP server \`${serverName}\` returned an invalid resources/templates/list result.`, "Codex cannot safely consume malformed resource template definitions from `resources/templates/list`.", "Return a `resourceTemplates` array where every template has string `name` and `uriTemplate` fields."));
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
else {
|
|
816
|
+
scorecard.resourceTemplatesList = "pass";
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
if (!hasPromptsCapability(initializeResponse)) {
|
|
820
|
+
scorecard.promptsList = "unsupported";
|
|
821
|
+
scorecard.promptGet = "unsupported";
|
|
822
|
+
}
|
|
823
|
+
else {
|
|
824
|
+
const prompts = await fetchPaginated({
|
|
825
|
+
method: "prompts/list",
|
|
826
|
+
timeoutFinding: buildFailure("plugin.runtime.prompts_list.timeout", `The MCP server \`${serverName}\` did not answer the prompts/list request in time.`, "A server that advertises prompts support but cannot list prompts in time will feel incomplete or broken in Codex.", "Inspect the prompts implementation and reduce latency before returning the prompt list."),
|
|
827
|
+
extractPage: extractPromptsPage
|
|
828
|
+
});
|
|
829
|
+
if (!prompts) {
|
|
830
|
+
scorecard.promptsList = "fail";
|
|
831
|
+
settle(buildFailure("plugin.runtime.prompts_list.invalid", `The MCP server \`${serverName}\` returned an invalid prompts/list result.`, "Codex cannot safely consume malformed prompt definitions from `prompts/list`.", "Return a `prompts` array where every prompt has a string `name`, and any prompt arguments have string `name` fields."));
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
scorecard.promptsList = "pass";
|
|
835
|
+
const promptForGet = findPromptForGet(prompts);
|
|
836
|
+
if (!promptForGet) {
|
|
837
|
+
scorecard.promptGet = "skipped";
|
|
838
|
+
}
|
|
839
|
+
else {
|
|
840
|
+
const promptGetResponse = await sendRequest("prompts/get", {
|
|
841
|
+
name: promptForGet.name,
|
|
842
|
+
...(buildPromptArguments(promptForGet)
|
|
843
|
+
? { arguments: buildPromptArguments(promptForGet) }
|
|
844
|
+
: {})
|
|
845
|
+
}, buildFailure("plugin.runtime.prompt_get.timeout", `The MCP server \`${serverName}\` did not answer the prompts/get request in time.`, "A server that lists prompts but cannot return prompt content in time will feel broken in Codex.", "Inspect the prompt retrieval path and reduce latency before returning prompt messages."));
|
|
846
|
+
if (isErrorResponse(promptGetResponse) ||
|
|
847
|
+
!isValidPromptGetResult(promptGetResponse)) {
|
|
848
|
+
scorecard.promptGet = "fail";
|
|
849
|
+
settle(buildFailure("plugin.runtime.prompt_get.invalid", `The MCP server \`${serverName}\` returned an invalid prompts/get result.`, "Codex cannot safely consume malformed prompt messages from `prompts/get`.", "Return a `messages` array where each entry has a valid `role` and a valid MCP content block."));
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
scorecard.promptGet = "pass";
|
|
853
|
+
warnings.push(...collectOversizedPromptGetWarnings(promptGetResponse));
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
settle(null);
|
|
857
|
+
};
|
|
858
|
+
runProtocolProbe().catch((error) => {
|
|
859
|
+
if (settled) {
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
if (isFinding(error)) {
|
|
863
|
+
settle(error);
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
if (scorecard.initialize === "skipped") {
|
|
867
|
+
scorecard.initialize = "fail";
|
|
868
|
+
}
|
|
869
|
+
settle(buildFailure("plugin.runtime.protocol.unhandled", `The MCP server \`${serverName}\` triggered an unexpected protocol probe failure.`, "Unexpected probe failures reduce confidence in runtime validation results.", stderrPreview.trim().length > 0
|
|
870
|
+
? `Inspect stderr for details: ${stderrPreview.trim()}`
|
|
871
|
+
: "Inspect the server output and protocol implementation for unexpected runtime errors."));
|
|
872
|
+
});
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
export async function probeRuntime(discoveredPackage, options = {}) {
|
|
876
|
+
const startupTimeoutMs = options.startupTimeoutMs ?? 400;
|
|
877
|
+
const servers = await loadMcpServers(discoveredPackage);
|
|
878
|
+
if (!servers) {
|
|
879
|
+
return {
|
|
880
|
+
findings: [],
|
|
881
|
+
scorecard: createRuntimeScorecard()
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
const findings = [];
|
|
885
|
+
let scorecard = createRuntimeScorecard();
|
|
886
|
+
for (const [serverName, config] of Object.entries(servers)) {
|
|
887
|
+
if (!isPlainObject(config)) {
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
const command = config.command;
|
|
891
|
+
if (typeof command !== "string") {
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
const args = Array.isArray(config.args)
|
|
895
|
+
? config.args.filter((value) => typeof value === "string")
|
|
896
|
+
: [];
|
|
897
|
+
const cwd = typeof config.cwd === "string"
|
|
898
|
+
? path.resolve(discoveredPackage.rootPath, config.cwd)
|
|
899
|
+
: discoveredPackage.rootPath;
|
|
900
|
+
const result = await probeCommandServer({
|
|
901
|
+
serverName,
|
|
902
|
+
command,
|
|
903
|
+
args,
|
|
904
|
+
cwd,
|
|
905
|
+
startupTimeoutMs,
|
|
906
|
+
transcript: options.transcript
|
|
907
|
+
});
|
|
908
|
+
scorecard = result.scorecard;
|
|
909
|
+
if (result.findings.length > 0) {
|
|
910
|
+
findings.push(...result.findings);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
return {
|
|
914
|
+
findings,
|
|
915
|
+
scorecard
|
|
916
|
+
};
|
|
917
|
+
}
|