@inspectr/mcplab-mcp-server 1.1.3 → 1.2.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/dist/http-handler.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/mcp-run-calculations.d.ts +48 -0
- package/dist/mcp-run-calculations.d.ts.map +1 -0
- package/dist/mcp-run-calculations.js +334 -0
- package/dist/mcp-run-calculations.js.map +1 -0
- package/dist/prompts.d.ts +1 -1
- package/dist/runtime.d.ts +14 -21
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +1143 -77
- package/dist/runtime.js.map +1 -1
- package/dist/server-factory.d.ts +1 -1
- package/dist/tools.d.ts +1 -1
- package/package.json +8 -4
package/dist/runtime.js
CHANGED
|
@@ -9,11 +9,346 @@ import { loadConfig, runAll, selectScenarios } from '@inspectr/mcplab-core';
|
|
|
9
9
|
import { renderReport } from '@inspectr/mcplab-reporting';
|
|
10
10
|
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
11
11
|
import { z } from 'zod';
|
|
12
|
-
|
|
12
|
+
import { buildAggregateRunsReport, buildCompareRunsReport } from './mcp-run-calculations.js';
|
|
13
|
+
const PACKAGE_JSON = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
14
|
+
const SERVER_VERSION = typeof PACKAGE_JSON.version === 'string' && PACKAGE_JSON.version.trim().length > 0
|
|
15
|
+
? PACKAGE_JSON.version
|
|
16
|
+
: '0.0.0';
|
|
17
|
+
const SERVER_ICON_URL = 'https://mcplab.inspectr.dev/favicon.svg';
|
|
13
18
|
const DEFAULT_MCP_PATH = '/mcp';
|
|
14
19
|
const DEFAULT_MCP_PORT = 3011;
|
|
15
20
|
const DEFAULT_MCP_HOST = '127.0.0.1';
|
|
16
21
|
const MAX_MARKDOWN_REPORT_READ_BYTES = 2 * 1024 * 1024;
|
|
22
|
+
const GenericObjectSchema = z.object({}).passthrough();
|
|
23
|
+
const ResultsSummarySchema = z.object({
|
|
24
|
+
total_scenarios: z.number().int().nonnegative(),
|
|
25
|
+
total_runs: z.number().int().nonnegative(),
|
|
26
|
+
pass_rate: z.number(),
|
|
27
|
+
avg_tool_calls_per_run: z.number(),
|
|
28
|
+
avg_tool_latency_ms: z.number().nullable()
|
|
29
|
+
});
|
|
30
|
+
const ResultsMetadataSchema = z
|
|
31
|
+
.object({
|
|
32
|
+
run_id: z.string(),
|
|
33
|
+
timestamp: z.string(),
|
|
34
|
+
config_hash: z.string(),
|
|
35
|
+
cli_version: z.string(),
|
|
36
|
+
mcp_server_versions: z.record(z.string())
|
|
37
|
+
})
|
|
38
|
+
.passthrough();
|
|
39
|
+
const MetricSummarySchema = z.object({
|
|
40
|
+
total_runs: z.number().int().nonnegative(),
|
|
41
|
+
passed_runs: z.number().int().nonnegative(),
|
|
42
|
+
failed_runs: z.number().int().nonnegative(),
|
|
43
|
+
pass_rate: z.number(),
|
|
44
|
+
avg_tool_calls_per_run: z.number(),
|
|
45
|
+
avg_tool_latency_ms: z.number().nullable()
|
|
46
|
+
});
|
|
47
|
+
const AggregateRowSchema = z.object({
|
|
48
|
+
key: z.string(),
|
|
49
|
+
run_id: z.string().optional(),
|
|
50
|
+
scenario_id: z.string().optional(),
|
|
51
|
+
agent: z.string().optional(),
|
|
52
|
+
run_count: z.number().int().nonnegative(),
|
|
53
|
+
timestamp_range: z
|
|
54
|
+
.object({
|
|
55
|
+
min: z.string(),
|
|
56
|
+
max: z.string()
|
|
57
|
+
})
|
|
58
|
+
.optional(),
|
|
59
|
+
total_runs: z.number().int().nonnegative(),
|
|
60
|
+
passed_runs: z.number().int().nonnegative(),
|
|
61
|
+
failed_runs: z.number().int().nonnegative(),
|
|
62
|
+
pass_rate: z.number(),
|
|
63
|
+
avg_tool_calls_per_run: z.number(),
|
|
64
|
+
avg_tool_latency_ms: z.number().nullable()
|
|
65
|
+
});
|
|
66
|
+
const CompareRowSchema = z.object({
|
|
67
|
+
key: z.string(),
|
|
68
|
+
scenario_id: z.string(),
|
|
69
|
+
agent: z.string(),
|
|
70
|
+
classification: z.enum(['regressed', 'improved', 'unchanged', 'new', 'missing']),
|
|
71
|
+
left: MetricSummarySchema.nullable(),
|
|
72
|
+
right: MetricSummarySchema.nullable(),
|
|
73
|
+
deltas: z.object({
|
|
74
|
+
pass_rate: z.number().nullable(),
|
|
75
|
+
failed_runs: z.number().nullable(),
|
|
76
|
+
avg_tool_calls_per_run: z.number().nullable(),
|
|
77
|
+
avg_tool_latency_ms: z.number().nullable()
|
|
78
|
+
})
|
|
79
|
+
});
|
|
80
|
+
const RunListEntrySchema = z.object({
|
|
81
|
+
run_id: z.string(),
|
|
82
|
+
path: z.string(),
|
|
83
|
+
summary: ResultsSummarySchema.optional(),
|
|
84
|
+
metadata: ResultsMetadataSchema.optional(),
|
|
85
|
+
summary_error: z.string().optional()
|
|
86
|
+
});
|
|
87
|
+
const LibraryScenarioEntrySchema = z.object({
|
|
88
|
+
file: z.string(),
|
|
89
|
+
id: z.string().optional(),
|
|
90
|
+
content: GenericObjectSchema.optional(),
|
|
91
|
+
yaml: z.string().optional()
|
|
92
|
+
});
|
|
93
|
+
const AgentEntrySchema = z.object({
|
|
94
|
+
provider: z.enum(['openai', 'anthropic', 'azure_openai']),
|
|
95
|
+
model: z.string(),
|
|
96
|
+
temperature: z.number().optional(),
|
|
97
|
+
max_tokens: z.number().int().positive().optional(),
|
|
98
|
+
system: z.string().optional()
|
|
99
|
+
});
|
|
100
|
+
const LibraryEntrySchema = z.object({
|
|
101
|
+
bundleRoot: z.string(),
|
|
102
|
+
servers: z.union([z.array(z.object({ id: z.string() })), z.record(GenericObjectSchema)]),
|
|
103
|
+
agents: z.union([z.array(z.object({ id: z.string() })), z.record(AgentEntrySchema)]),
|
|
104
|
+
scenarios: z.array(LibraryScenarioEntrySchema)
|
|
105
|
+
});
|
|
106
|
+
const ServerAuthSchema = z.union([
|
|
107
|
+
z.object({
|
|
108
|
+
type: z.literal('bearer'),
|
|
109
|
+
token: z.string()
|
|
110
|
+
}),
|
|
111
|
+
z.object({
|
|
112
|
+
type: z.literal('api_key'),
|
|
113
|
+
header_name: z.string().optional(),
|
|
114
|
+
value: z.string()
|
|
115
|
+
}),
|
|
116
|
+
z.object({
|
|
117
|
+
type: z.literal('oauth_client_credentials'),
|
|
118
|
+
token_url: z.string(),
|
|
119
|
+
client_id_env: z.string(),
|
|
120
|
+
client_secret_env: z.string(),
|
|
121
|
+
scope: z.string().optional(),
|
|
122
|
+
audience: z.string().optional()
|
|
123
|
+
})
|
|
124
|
+
]);
|
|
125
|
+
const ServerEntrySchema = z.object({
|
|
126
|
+
transport: z.literal('http'),
|
|
127
|
+
url: z.string(),
|
|
128
|
+
auth: ServerAuthSchema.optional()
|
|
129
|
+
});
|
|
130
|
+
const ScenarioEntrySchema = z.object({
|
|
131
|
+
id: z.string(),
|
|
132
|
+
agent: z.string().optional(),
|
|
133
|
+
servers: z.array(z.string()),
|
|
134
|
+
prompt: z.string(),
|
|
135
|
+
snapshot_eval_enabled: z.boolean().optional(),
|
|
136
|
+
eval: GenericObjectSchema.optional(),
|
|
137
|
+
extract: z
|
|
138
|
+
.array(z.object({
|
|
139
|
+
name: z.string(),
|
|
140
|
+
from: z.string(),
|
|
141
|
+
regex: z.string()
|
|
142
|
+
}))
|
|
143
|
+
.optional()
|
|
144
|
+
});
|
|
145
|
+
const ConfigSummarySchema = z.object({
|
|
146
|
+
server_count: z.number().int().nonnegative(),
|
|
147
|
+
agent_count: z.number().int().nonnegative(),
|
|
148
|
+
scenario_count: z.number().int().nonnegative(),
|
|
149
|
+
servers: z.array(z.string()),
|
|
150
|
+
agents: z.array(z.string()),
|
|
151
|
+
scenarios: z.array(z.object({
|
|
152
|
+
id: z.string(),
|
|
153
|
+
servers: z.array(z.string()),
|
|
154
|
+
has_eval: z.boolean(),
|
|
155
|
+
extract_count: z.number().int().nonnegative()
|
|
156
|
+
}))
|
|
157
|
+
});
|
|
158
|
+
const ToolAnalysisListItemSchema = z.object({
|
|
159
|
+
report_id: z.string(),
|
|
160
|
+
path: z.string().optional(),
|
|
161
|
+
error: z.string().optional(),
|
|
162
|
+
reportId: z.string().optional(),
|
|
163
|
+
createdAt: z.string().optional(),
|
|
164
|
+
sourceJobId: z.string().optional(),
|
|
165
|
+
serverNames: z.array(z.string()).optional(),
|
|
166
|
+
assistantAgentName: z.string().optional(),
|
|
167
|
+
assistantAgentModel: z.string().optional(),
|
|
168
|
+
modes: GenericObjectSchema.optional(),
|
|
169
|
+
summary: GenericObjectSchema.optional()
|
|
170
|
+
});
|
|
171
|
+
const ToolAnalysisSummarySchema = z.object({
|
|
172
|
+
reportId: z.string().optional(),
|
|
173
|
+
createdAt: z.string().optional(),
|
|
174
|
+
sourceJobId: z.string().optional(),
|
|
175
|
+
serverNames: z.array(z.string()).optional(),
|
|
176
|
+
assistantAgentName: z.string().optional(),
|
|
177
|
+
assistantAgentModel: z.string().optional(),
|
|
178
|
+
modes: GenericObjectSchema.optional(),
|
|
179
|
+
summary: GenericObjectSchema.optional()
|
|
180
|
+
});
|
|
181
|
+
const FlattenedTraceItemSchema = z.union([
|
|
182
|
+
z.object({
|
|
183
|
+
type: z.literal('message'),
|
|
184
|
+
record_index: z.number().int().nonnegative(),
|
|
185
|
+
message_index: z.number().int().nonnegative(),
|
|
186
|
+
scenario_id: z.string(),
|
|
187
|
+
agent: z.string(),
|
|
188
|
+
role: z.enum(['user', 'assistant', 'tool']),
|
|
189
|
+
ts: z.string(),
|
|
190
|
+
usage: z.unknown().optional()
|
|
191
|
+
}),
|
|
192
|
+
z.object({
|
|
193
|
+
type: z.literal('text'),
|
|
194
|
+
record_index: z.number().int().nonnegative(),
|
|
195
|
+
message_index: z.number().int().nonnegative(),
|
|
196
|
+
block_index: z.number().int().nonnegative(),
|
|
197
|
+
scenario_id: z.string(),
|
|
198
|
+
agent: z.string(),
|
|
199
|
+
role: z.enum(['user', 'assistant', 'tool']),
|
|
200
|
+
ts: z.string(),
|
|
201
|
+
text: z.string()
|
|
202
|
+
}),
|
|
203
|
+
z.object({
|
|
204
|
+
type: z.literal('tool_use'),
|
|
205
|
+
record_index: z.number().int().nonnegative(),
|
|
206
|
+
message_index: z.number().int().nonnegative(),
|
|
207
|
+
block_index: z.number().int().nonnegative(),
|
|
208
|
+
scenario_id: z.string(),
|
|
209
|
+
agent: z.string(),
|
|
210
|
+
role: z.enum(['user', 'assistant', 'tool']),
|
|
211
|
+
ts: z.string(),
|
|
212
|
+
id: z.string(),
|
|
213
|
+
name: z.string(),
|
|
214
|
+
server: z.string(),
|
|
215
|
+
input: z.unknown()
|
|
216
|
+
}),
|
|
217
|
+
z.object({
|
|
218
|
+
type: z.literal('tool_result'),
|
|
219
|
+
record_index: z.number().int().nonnegative(),
|
|
220
|
+
message_index: z.number().int().nonnegative(),
|
|
221
|
+
block_index: z.number().int().nonnegative(),
|
|
222
|
+
scenario_id: z.string(),
|
|
223
|
+
agent: z.string(),
|
|
224
|
+
role: z.enum(['user', 'assistant', 'tool']),
|
|
225
|
+
ts: z.string(),
|
|
226
|
+
tool_use_id: z.string(),
|
|
227
|
+
name: z.string(),
|
|
228
|
+
server: z.string(),
|
|
229
|
+
is_error: z.boolean().optional(),
|
|
230
|
+
duration_ms: z.number().optional(),
|
|
231
|
+
content: z.unknown()
|
|
232
|
+
})
|
|
233
|
+
]);
|
|
234
|
+
const ConversationTimelineItemSchema = z.union([
|
|
235
|
+
z.object({
|
|
236
|
+
index: z.number().int().nonnegative(),
|
|
237
|
+
type: z.enum(['agent_message', 'user_message', 'tool_text']),
|
|
238
|
+
role: z.enum(['user', 'assistant', 'tool']),
|
|
239
|
+
ts: z.string(),
|
|
240
|
+
message_index: z.number().int().nonnegative(),
|
|
241
|
+
block_index: z.number().int().nonnegative(),
|
|
242
|
+
text: z.string()
|
|
243
|
+
}),
|
|
244
|
+
z.object({
|
|
245
|
+
index: z.number().int().nonnegative(),
|
|
246
|
+
type: z.literal('tool_call'),
|
|
247
|
+
role: z.enum(['user', 'assistant', 'tool']),
|
|
248
|
+
ts: z.string(),
|
|
249
|
+
message_index: z.number().int().nonnegative(),
|
|
250
|
+
block_index: z.number().int().nonnegative(),
|
|
251
|
+
id: z.string(),
|
|
252
|
+
server: z.string(),
|
|
253
|
+
tool: z.string(),
|
|
254
|
+
args: z.unknown()
|
|
255
|
+
}),
|
|
256
|
+
z.object({
|
|
257
|
+
index: z.number().int().nonnegative(),
|
|
258
|
+
type: z.literal('tool_result'),
|
|
259
|
+
role: z.enum(['user', 'assistant', 'tool']),
|
|
260
|
+
ts: z.string(),
|
|
261
|
+
message_index: z.number().int().nonnegative(),
|
|
262
|
+
block_index: z.number().int().nonnegative(),
|
|
263
|
+
tool_use_id: z.string(),
|
|
264
|
+
server: z.string(),
|
|
265
|
+
tool: z.string(),
|
|
266
|
+
ok: z.boolean(),
|
|
267
|
+
duration_ms: z.number().optional(),
|
|
268
|
+
content: z.array(z
|
|
269
|
+
.object({
|
|
270
|
+
text: z.string()
|
|
271
|
+
})
|
|
272
|
+
.passthrough())
|
|
273
|
+
})
|
|
274
|
+
]);
|
|
275
|
+
const WriteMarkdownReportSuccessSchema = z.object({
|
|
276
|
+
ok: z.literal(true),
|
|
277
|
+
path: z.string().describe('Resolved absolute path to the written markdown file.'),
|
|
278
|
+
bytes: z.number().int().nonnegative().describe('UTF-8 byte length written to disk.'),
|
|
279
|
+
chars: z.number().int().nonnegative().describe('Character count written to disk.'),
|
|
280
|
+
overwritten: z.boolean().describe('True when an existing file was replaced.'),
|
|
281
|
+
workspace_root: z.string().describe('Workspace root used for path safety validation.')
|
|
282
|
+
});
|
|
283
|
+
const WriteMarkdownReportErrorSchema = z.object({
|
|
284
|
+
ok: z.literal(false),
|
|
285
|
+
error_code: z.enum([
|
|
286
|
+
'PATH_ESCAPE',
|
|
287
|
+
'PERMISSION_DENIED',
|
|
288
|
+
'FILE_EXISTS',
|
|
289
|
+
'INVALID_EXTENSION',
|
|
290
|
+
'PARENT_DIR_MISSING',
|
|
291
|
+
'IO_ERROR'
|
|
292
|
+
]),
|
|
293
|
+
error_message: z.string(),
|
|
294
|
+
attempted_path: z.string().optional(),
|
|
295
|
+
violated_constraint: z.string().optional()
|
|
296
|
+
});
|
|
297
|
+
const GenerateServerEntryInputSchema = z
|
|
298
|
+
.object({
|
|
299
|
+
id: z.string(),
|
|
300
|
+
url: z.string(),
|
|
301
|
+
transport: z.enum(['http']).optional(),
|
|
302
|
+
auth_type: z.enum(['none', 'bearer', 'api_key', 'oauth_client_credentials']).optional(),
|
|
303
|
+
bearer_token: z.string().optional(),
|
|
304
|
+
bearer_env: z.string().optional(),
|
|
305
|
+
api_key_header_name: z.string().optional(),
|
|
306
|
+
api_key_value: z.string().optional(),
|
|
307
|
+
oauth_token_url: z.string().optional(),
|
|
308
|
+
oauth_client_id_env: z.string().optional(),
|
|
309
|
+
oauth_client_secret_env: z.string().optional(),
|
|
310
|
+
oauth_scope: z.string().optional(),
|
|
311
|
+
oauth_audience: z.string().optional()
|
|
312
|
+
})
|
|
313
|
+
.superRefine((value, ctx) => {
|
|
314
|
+
const authType = value.auth_type ?? 'none';
|
|
315
|
+
if (authType === 'bearer') {
|
|
316
|
+
if (!value.bearer_token && !value.bearer_env) {
|
|
317
|
+
ctx.addIssue({
|
|
318
|
+
code: z.ZodIssueCode.custom,
|
|
319
|
+
message: 'auth_type=bearer requires at least one of bearer_token or bearer_env.'
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (authType === 'api_key') {
|
|
324
|
+
if (!value.api_key_value) {
|
|
325
|
+
ctx.addIssue({
|
|
326
|
+
code: z.ZodIssueCode.custom,
|
|
327
|
+
message: 'auth_type=api_key requires api_key_value.'
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (authType === 'oauth_client_credentials') {
|
|
332
|
+
if (!value.oauth_token_url) {
|
|
333
|
+
ctx.addIssue({
|
|
334
|
+
code: z.ZodIssueCode.custom,
|
|
335
|
+
message: 'auth_type=oauth_client_credentials requires oauth_token_url.'
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
if (!value.oauth_client_id_env) {
|
|
339
|
+
ctx.addIssue({
|
|
340
|
+
code: z.ZodIssueCode.custom,
|
|
341
|
+
message: 'auth_type=oauth_client_credentials requires oauth_client_id_env.'
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
if (!value.oauth_client_secret_env) {
|
|
345
|
+
ctx.addIssue({
|
|
346
|
+
code: z.ZodIssueCode.custom,
|
|
347
|
+
message: 'auth_type=oauth_client_credentials requires oauth_client_secret_env.'
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
});
|
|
17
352
|
export async function startMcplabMcpServer(options) {
|
|
18
353
|
const logger = options.logger ?? console;
|
|
19
354
|
const sessions = new Map();
|
|
@@ -75,60 +410,180 @@ export function defaultMcplabMcpServerOptionsFromEnv() {
|
|
|
75
410
|
export function createConfiguredServer() {
|
|
76
411
|
const server = new McpServer({
|
|
77
412
|
name: 'mcplab-assistant-server',
|
|
78
|
-
version: SERVER_VERSION
|
|
413
|
+
version: SERVER_VERSION,
|
|
414
|
+
title: 'MCPLab Assistant Server',
|
|
415
|
+
description: 'MCPLab MCP tools for configs, runs, results, traces, and report workflows.',
|
|
416
|
+
websiteUrl: 'https://mcplab.inspectr.dev',
|
|
417
|
+
icons: [{ src: SERVER_ICON_URL, mimeType: 'image/svg+xml' }]
|
|
79
418
|
});
|
|
80
419
|
registerTools(server);
|
|
81
420
|
registerPrompts(server);
|
|
82
421
|
return server;
|
|
83
422
|
}
|
|
84
423
|
export function registerTools(server) {
|
|
85
|
-
|
|
86
|
-
|
|
424
|
+
const registerTool = (name, config, cb) => {
|
|
425
|
+
const resolvedTitle = resolveToolTitle(name, config.title, config.annotations?.title);
|
|
426
|
+
const outputSchema = config.outputSchema ?? z.object({}).passthrough().describe('Structured tool response.');
|
|
427
|
+
server.registerTool(name, {
|
|
428
|
+
...config,
|
|
429
|
+
title: resolvedTitle,
|
|
430
|
+
outputSchema,
|
|
431
|
+
annotations: inferToolAnnotations(name, resolvedTitle, config.annotations)
|
|
432
|
+
}, cb);
|
|
433
|
+
};
|
|
434
|
+
registerTool('mcplab_write_markdown_report', {
|
|
435
|
+
description: 'Write a Markdown (.md or .markdown) file to a path within the current workspace. Returns structured output with ok:true and the resolved path on success. On failure, returns ok:false with an error_code from: PATH_ESCAPE (path traversal attempt), FILE_EXISTS (file already exists and overwrite is false), PERMISSION_DENIED, PARENT_DIR_MISSING (create_dirs is false and parent does not exist), INVALID_EXTENSION (not .md or .markdown), IO_ERROR.',
|
|
436
|
+
outputSchema: z.union([WriteMarkdownReportSuccessSchema, WriteMarkdownReportErrorSchema]),
|
|
87
437
|
inputSchema: {
|
|
88
438
|
output_path: z
|
|
89
439
|
.string()
|
|
90
|
-
.describe(
|
|
91
|
-
markdown: z
|
|
440
|
+
.describe("Target .md/.markdown path. Use a relative path (e.g. mcplab/reports/my-report.md) — relative paths are always safe and resolve against the server's working directory (process.cwd()). Absolute paths are accepted only if they stay inside that directory; any path that escapes it is rejected with error_code PATH_ESCAPE."),
|
|
441
|
+
markdown: z
|
|
442
|
+
.string()
|
|
443
|
+
.min(1, 'Markdown content must not be empty.')
|
|
444
|
+
.max(10485760, 'Markdown must not exceed 10 MiB')
|
|
445
|
+
.describe('Markdown content to write. Maximum 10 MiB.'),
|
|
92
446
|
overwrite: z
|
|
93
447
|
.boolean()
|
|
94
|
-
.
|
|
448
|
+
.default(false)
|
|
95
449
|
.describe('Overwrite existing file if true. Defaults to false.'),
|
|
96
450
|
create_dirs: z
|
|
97
451
|
.boolean()
|
|
98
|
-
.
|
|
452
|
+
.default(true)
|
|
99
453
|
.describe('Create missing parent directories if true. Defaults to true.')
|
|
100
454
|
}
|
|
101
455
|
}, async ({ output_path, markdown, overwrite, create_dirs }) => {
|
|
102
|
-
|
|
456
|
+
try {
|
|
103
457
|
const targetPath = resolvePathInsideWorkspace(output_path);
|
|
104
458
|
const extension = extname(targetPath).toLowerCase();
|
|
105
459
|
if (extension !== '.md' && extension !== '.markdown') {
|
|
106
|
-
|
|
460
|
+
const structured = {
|
|
461
|
+
ok: false,
|
|
462
|
+
error_code: 'INVALID_EXTENSION',
|
|
463
|
+
error_message: 'output_path must end with .md or .markdown',
|
|
464
|
+
attempted_path: output_path,
|
|
465
|
+
violated_constraint: 'MARKDOWN_EXTENSION_REQUIRED'
|
|
466
|
+
};
|
|
467
|
+
return {
|
|
468
|
+
isError: true,
|
|
469
|
+
content: [{ type: 'text', text: `Error: ${structured.error_message}` }],
|
|
470
|
+
structuredContent: structured
|
|
471
|
+
};
|
|
107
472
|
}
|
|
108
473
|
const parentDir = dirname(targetPath);
|
|
109
|
-
if (
|
|
110
|
-
|
|
474
|
+
if (create_dirs) {
|
|
475
|
+
try {
|
|
476
|
+
mkdirSync(parentDir, { recursive: true });
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
const code = typeof error === 'object' && error !== null && 'code' in error
|
|
480
|
+
? String(error.code ?? '')
|
|
481
|
+
: '';
|
|
482
|
+
const structured = {
|
|
483
|
+
ok: false,
|
|
484
|
+
error_code: (code === 'EACCES' || code === 'EPERM'
|
|
485
|
+
? 'PERMISSION_DENIED'
|
|
486
|
+
: 'IO_ERROR'),
|
|
487
|
+
error_message: error instanceof Error ? error.message : String(error),
|
|
488
|
+
attempted_path: targetPath
|
|
489
|
+
};
|
|
490
|
+
return {
|
|
491
|
+
isError: true,
|
|
492
|
+
content: [{ type: 'text', text: `Error: ${structured.error_message}` }],
|
|
493
|
+
structuredContent: structured
|
|
494
|
+
};
|
|
495
|
+
}
|
|
111
496
|
}
|
|
112
497
|
else if (!existsSync(parentDir)) {
|
|
113
|
-
|
|
498
|
+
const structured = {
|
|
499
|
+
ok: false,
|
|
500
|
+
error_code: 'PARENT_DIR_MISSING',
|
|
501
|
+
error_message: `Parent directory does not exist: ${parentDir}`,
|
|
502
|
+
attempted_path: targetPath
|
|
503
|
+
};
|
|
504
|
+
return {
|
|
505
|
+
isError: true,
|
|
506
|
+
content: [{ type: 'text', text: `Error: ${structured.error_message}` }],
|
|
507
|
+
structuredContent: structured
|
|
508
|
+
};
|
|
114
509
|
}
|
|
115
510
|
const fileExists = existsSync(targetPath);
|
|
116
|
-
if (fileExists && !
|
|
117
|
-
|
|
511
|
+
if (fileExists && !overwrite) {
|
|
512
|
+
const structured = {
|
|
513
|
+
ok: false,
|
|
514
|
+
error_code: 'FILE_EXISTS',
|
|
515
|
+
error_message: `File already exists: ${targetPath} (set overwrite=true to replace it)`,
|
|
516
|
+
attempted_path: targetPath
|
|
517
|
+
};
|
|
518
|
+
return {
|
|
519
|
+
isError: true,
|
|
520
|
+
content: [{ type: 'text', text: `Error: ${structured.error_message}` }],
|
|
521
|
+
structuredContent: structured
|
|
522
|
+
};
|
|
118
523
|
}
|
|
119
524
|
const normalized = markdown.endsWith('\n') ? markdown : `${markdown}\n`;
|
|
120
|
-
|
|
525
|
+
try {
|
|
526
|
+
writeFileSync(targetPath, normalized, 'utf8');
|
|
527
|
+
}
|
|
528
|
+
catch (error) {
|
|
529
|
+
const code = typeof error === 'object' && error !== null && 'code' in error
|
|
530
|
+
? String(error.code ?? '')
|
|
531
|
+
: '';
|
|
532
|
+
const structured = {
|
|
533
|
+
ok: false,
|
|
534
|
+
error_code: (code === 'EACCES' || code === 'EPERM'
|
|
535
|
+
? 'PERMISSION_DENIED'
|
|
536
|
+
: 'IO_ERROR'),
|
|
537
|
+
error_message: error instanceof Error ? error.message : String(error),
|
|
538
|
+
attempted_path: targetPath
|
|
539
|
+
};
|
|
540
|
+
return {
|
|
541
|
+
isError: true,
|
|
542
|
+
content: [{ type: 'text', text: `Error: ${structured.error_message}` }],
|
|
543
|
+
structuredContent: structured
|
|
544
|
+
};
|
|
545
|
+
}
|
|
121
546
|
return ok(`Wrote Markdown report to ${targetPath}`, {
|
|
547
|
+
ok: true,
|
|
122
548
|
path: targetPath,
|
|
123
549
|
bytes: Buffer.byteLength(normalized, 'utf8'),
|
|
124
550
|
chars: normalized.length,
|
|
125
551
|
overwritten: fileExists,
|
|
126
552
|
workspace_root: process.cwd()
|
|
127
553
|
});
|
|
128
|
-
}
|
|
554
|
+
}
|
|
555
|
+
catch (error) {
|
|
556
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
557
|
+
const isPathEscape = message.toLowerCase().includes('escapes workspace root');
|
|
558
|
+
const structured = {
|
|
559
|
+
ok: false,
|
|
560
|
+
error_code: (isPathEscape ? 'PATH_ESCAPE' : 'IO_ERROR'),
|
|
561
|
+
error_message: message,
|
|
562
|
+
attempted_path: output_path,
|
|
563
|
+
violated_constraint: isPathEscape ? 'WORKSPACE_CONTAINMENT' : undefined
|
|
564
|
+
};
|
|
565
|
+
return {
|
|
566
|
+
isError: true,
|
|
567
|
+
content: [{ type: 'text', text: `Error: ${structured.error_message}` }],
|
|
568
|
+
structuredContent: removeUndefined(structured)
|
|
569
|
+
};
|
|
570
|
+
}
|
|
129
571
|
});
|
|
130
|
-
|
|
572
|
+
registerTool('mcplab_search_markdown_reports', {
|
|
131
573
|
description: 'List saved markdown reports under mcplab/reports. Supports filtering by run id substring to find reports linked to a result.',
|
|
574
|
+
outputSchema: {
|
|
575
|
+
reports_dir: z.string(),
|
|
576
|
+
run_id_filter: z.string().optional(),
|
|
577
|
+
query: z.string().optional(),
|
|
578
|
+
total_matching: z.number().int().nonnegative(),
|
|
579
|
+
items: z.array(z.object({
|
|
580
|
+
path: z.string(),
|
|
581
|
+
relativePath: z.string(),
|
|
582
|
+
name: z.string(),
|
|
583
|
+
sizeBytes: z.number().int().nonnegative(),
|
|
584
|
+
mtime: z.string()
|
|
585
|
+
}))
|
|
586
|
+
},
|
|
132
587
|
inputSchema: {
|
|
133
588
|
reports_dir: z
|
|
134
589
|
.string()
|
|
@@ -138,6 +593,10 @@ export function registerTools(server) {
|
|
|
138
593
|
.string()
|
|
139
594
|
.optional()
|
|
140
595
|
.describe('Optional run id substring filter (matches path/name).'),
|
|
596
|
+
query: z
|
|
597
|
+
.string()
|
|
598
|
+
.optional()
|
|
599
|
+
.describe('Optional case-insensitive search query across report path/name fields.'),
|
|
141
600
|
limit: z
|
|
142
601
|
.number()
|
|
143
602
|
.int()
|
|
@@ -146,25 +605,47 @@ export function registerTools(server) {
|
|
|
146
605
|
.optional()
|
|
147
606
|
.describe('Max reports to return (default 20).')
|
|
148
607
|
}
|
|
149
|
-
}, async ({ reports_dir, run_id, limit }) => {
|
|
608
|
+
}, async ({ reports_dir, run_id, query, limit }) => {
|
|
150
609
|
return withToolHandling(async () => {
|
|
151
610
|
const root = resolveMarkdownReportsDir(reports_dir);
|
|
152
611
|
const all = listMarkdownReportsFromDisk(root);
|
|
153
612
|
const runFilter = String(run_id ?? '').trim();
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
613
|
+
const searchQuery = String(query ?? '')
|
|
614
|
+
.trim()
|
|
615
|
+
.toLowerCase();
|
|
616
|
+
const filtered = all.filter((item) => {
|
|
617
|
+
if (runFilter &&
|
|
618
|
+
!item.relativePath.includes(runFilter) &&
|
|
619
|
+
!item.name.includes(runFilter)) {
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
if (!searchQuery)
|
|
623
|
+
return true;
|
|
624
|
+
const hay = `${item.path}\n${item.relativePath}\n${item.name}`.toLowerCase();
|
|
625
|
+
return hay.includes(searchQuery);
|
|
626
|
+
});
|
|
157
627
|
const capped = filtered.slice(0, limit ?? 20);
|
|
158
628
|
return ok(`Found ${capped.length}/${filtered.length} markdown report(s) in ${root}`, {
|
|
159
629
|
reports_dir: root,
|
|
160
630
|
run_id_filter: runFilter || undefined,
|
|
631
|
+
query: searchQuery || undefined,
|
|
161
632
|
total_matching: filtered.length,
|
|
162
633
|
items: capped
|
|
163
634
|
});
|
|
164
635
|
});
|
|
165
636
|
});
|
|
166
|
-
|
|
637
|
+
registerTool('mcplab_read_markdown_report', {
|
|
167
638
|
description: 'Read a saved markdown report by relative path (under mcplab/reports by default) or by workspace-relative path, with optional truncation.',
|
|
639
|
+
outputSchema: {
|
|
640
|
+
reports_dir: z.string(),
|
|
641
|
+
path: z.string(),
|
|
642
|
+
relativePath: z.string(),
|
|
643
|
+
name: z.string(),
|
|
644
|
+
sizeBytes: z.number().int().nonnegative(),
|
|
645
|
+
mtime: z.string(),
|
|
646
|
+
truncated: z.boolean(),
|
|
647
|
+
content: z.string()
|
|
648
|
+
},
|
|
168
649
|
inputSchema: {
|
|
169
650
|
path: z
|
|
170
651
|
.string()
|
|
@@ -207,8 +688,9 @@ export function registerTools(server) {
|
|
|
207
688
|
});
|
|
208
689
|
});
|
|
209
690
|
});
|
|
210
|
-
|
|
691
|
+
registerTool('mcplab_list_library', {
|
|
211
692
|
description: 'List reusable MCPLab library entries (servers, agents, scenarios) from a bundle root such as mcplab/ or examples/libraries/.',
|
|
693
|
+
outputSchema: LibraryEntrySchema,
|
|
212
694
|
inputSchema: {
|
|
213
695
|
bundleRoot: z
|
|
214
696
|
.string()
|
|
@@ -237,8 +719,16 @@ export function registerTools(server) {
|
|
|
237
719
|
return ok(`Loaded MCPLab library from ${root}`, structured);
|
|
238
720
|
});
|
|
239
721
|
});
|
|
240
|
-
|
|
722
|
+
registerTool('mcplab_get_library_item', {
|
|
241
723
|
description: 'Get a specific reusable server, agent, or scenario definition from a MCPLab library bundle and return both structured data and YAML.',
|
|
724
|
+
outputSchema: {
|
|
725
|
+
bundleRoot: z.string(),
|
|
726
|
+
kind: z.enum(['servers', 'agents', 'scenarios']),
|
|
727
|
+
id: z.string(),
|
|
728
|
+
file: z.string().optional(),
|
|
729
|
+
yaml: z.string(),
|
|
730
|
+
content: GenericObjectSchema
|
|
731
|
+
},
|
|
242
732
|
inputSchema: {
|
|
243
733
|
bundleRoot: z.string().optional().describe('Optional library bundle root path.'),
|
|
244
734
|
kind: z.enum(['servers', 'agents', 'scenarios']).describe('Library category.'),
|
|
@@ -251,8 +741,13 @@ export function registerTools(server) {
|
|
|
251
741
|
return ok(`Loaded ${kind.slice(0, -1)} '${id}' from ${root}`, item);
|
|
252
742
|
});
|
|
253
743
|
});
|
|
254
|
-
|
|
744
|
+
registerTool('mcplab_generate_server_entry', {
|
|
255
745
|
description: 'Generate a MCPLab servers.yaml entry (or inline config block) for an MCP server connection.',
|
|
746
|
+
outputSchema: {
|
|
747
|
+
id: z.string(),
|
|
748
|
+
entry: ServerEntrySchema,
|
|
749
|
+
yaml: z.string()
|
|
750
|
+
},
|
|
256
751
|
inputSchema: {
|
|
257
752
|
id: z.string().describe('Server id key (kebab-case recommended).'),
|
|
258
753
|
url: z.string().describe('MCP server URL (Streamable HTTP endpoint).'),
|
|
@@ -288,16 +783,22 @@ export function registerTools(server) {
|
|
|
288
783
|
}
|
|
289
784
|
}, async (input) => {
|
|
290
785
|
return withToolHandling(async () => {
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
786
|
+
const parsed = GenerateServerEntryInputSchema.parse(input);
|
|
787
|
+
const entry = buildServerEntry(parsed);
|
|
788
|
+
return ok(`Generated server entry '${parsed.id}'`, {
|
|
789
|
+
id: parsed.id,
|
|
294
790
|
entry,
|
|
295
|
-
yaml: stringifyYaml({ [
|
|
791
|
+
yaml: stringifyYaml({ [parsed.id]: entry }).trimEnd()
|
|
296
792
|
});
|
|
297
793
|
});
|
|
298
794
|
});
|
|
299
|
-
|
|
795
|
+
registerTool('mcplab_generate_agent_entry', {
|
|
300
796
|
description: 'Generate a MCPLab agents.yaml entry (provider/model/system settings) for evaluation runs.',
|
|
797
|
+
outputSchema: {
|
|
798
|
+
id: z.string(),
|
|
799
|
+
entry: AgentEntrySchema,
|
|
800
|
+
yaml: z.string()
|
|
801
|
+
},
|
|
301
802
|
inputSchema: {
|
|
302
803
|
id: z.string().describe('Agent id key (kebab-case recommended).'),
|
|
303
804
|
provider: z
|
|
@@ -318,8 +819,16 @@ export function registerTools(server) {
|
|
|
318
819
|
});
|
|
319
820
|
});
|
|
320
821
|
});
|
|
321
|
-
|
|
822
|
+
registerTool('mcplab_generate_scenario_entry', {
|
|
322
823
|
description: 'Generate a MCPLab scenario YAML snippet with prompt, server links, and optional evaluation/extract rules. Optimized for scenario authoring workflows.',
|
|
824
|
+
outputSchema: {
|
|
825
|
+
scenario: ScenarioEntrySchema,
|
|
826
|
+
yaml: z.string(),
|
|
827
|
+
yaml_library_file: z.string(),
|
|
828
|
+
yaml_inline_list_item: z.string(),
|
|
829
|
+
format: z.enum(['library-scenario-file', 'inline-scenarios-list-item']),
|
|
830
|
+
warnings: z.array(z.string())
|
|
831
|
+
},
|
|
323
832
|
inputSchema: {
|
|
324
833
|
id: z
|
|
325
834
|
.string()
|
|
@@ -337,7 +846,11 @@ export function registerTools(server) {
|
|
|
337
846
|
.array(z.string())
|
|
338
847
|
.min(1)
|
|
339
848
|
.describe('One or more server ids available to the scenario.'),
|
|
340
|
-
prompt: z
|
|
849
|
+
prompt: z
|
|
850
|
+
.string()
|
|
851
|
+
.min(1)
|
|
852
|
+
.max(4000)
|
|
853
|
+
.describe('The task prompt the evaluation agent should execute (1-4000 chars).'),
|
|
341
854
|
snapshot_eval_enabled: z
|
|
342
855
|
.boolean()
|
|
343
856
|
.optional()
|
|
@@ -381,8 +894,20 @@ export function registerTools(server) {
|
|
|
381
894
|
});
|
|
382
895
|
});
|
|
383
896
|
});
|
|
384
|
-
|
|
897
|
+
registerTool('mcplab_validate_config', {
|
|
385
898
|
description: 'Validate and expand a MCPLab config file via mcplab-core loadConfig(), including server/agent/scenario library references.',
|
|
899
|
+
outputSchema: {
|
|
900
|
+
configPath: z.string(),
|
|
901
|
+
bundleRoot: z.string(),
|
|
902
|
+
hash: z.string(),
|
|
903
|
+
summary: ConfigSummarySchema,
|
|
904
|
+
resolved_config: z.object({
|
|
905
|
+
servers: z.record(GenericObjectSchema),
|
|
906
|
+
agents: z.record(AgentEntrySchema),
|
|
907
|
+
scenarios: z.array(GenericObjectSchema),
|
|
908
|
+
run_defaults: GenericObjectSchema.optional()
|
|
909
|
+
})
|
|
910
|
+
},
|
|
386
911
|
inputSchema: {
|
|
387
912
|
config_path: z.string().describe('Path to MCPLab eval YAML config.'),
|
|
388
913
|
bundle_root: z
|
|
@@ -412,8 +937,26 @@ export function registerTools(server) {
|
|
|
412
937
|
});
|
|
413
938
|
});
|
|
414
939
|
});
|
|
415
|
-
|
|
940
|
+
registerTool('mcplab_run_eval', {
|
|
416
941
|
description: 'Run a MCPLab evaluation using mcplab-core runAll() from a config file and return the run directory plus summary metrics.',
|
|
942
|
+
outputSchema: {
|
|
943
|
+
run_dir: z.string(),
|
|
944
|
+
total_scenarios: z.number().int().nonnegative(),
|
|
945
|
+
total_runs: z.number().int().nonnegative(),
|
|
946
|
+
passed_runs: z.number().int().nonnegative(),
|
|
947
|
+
failed_runs: z.number().int().nonnegative(),
|
|
948
|
+
skipped_runs: z.number().int().nonnegative(),
|
|
949
|
+
duration_ms: z.number().nonnegative(),
|
|
950
|
+
summary: ResultsSummarySchema,
|
|
951
|
+
metadata: ResultsMetadataSchema,
|
|
952
|
+
scenarios: z.array(z.object({
|
|
953
|
+
scenario_id: z.string(),
|
|
954
|
+
agent: z.string(),
|
|
955
|
+
pass_rate: z.number(),
|
|
956
|
+
tool_usage_frequency: z.record(z.number())
|
|
957
|
+
})),
|
|
958
|
+
report_html_preview: z.string()
|
|
959
|
+
},
|
|
417
960
|
inputSchema: {
|
|
418
961
|
config_path: z.string().describe('Path to MCPLab eval YAML config.'),
|
|
419
962
|
bundle_root: z
|
|
@@ -447,8 +990,28 @@ export function registerTools(server) {
|
|
|
447
990
|
runsDir: runs_dir ?? 'mcplab/results/evaluation-runs'
|
|
448
991
|
});
|
|
449
992
|
const reportHtml = renderReport(results);
|
|
993
|
+
const allRuns = results.scenarios.flatMap((scenario) => scenario.runs);
|
|
994
|
+
const passedRuns = allRuns.filter((run) => run.pass === true).length;
|
|
995
|
+
const failedRuns = allRuns.filter((run) => run.pass === false).length;
|
|
996
|
+
const skippedRuns = Math.max(0, allRuns.length - passedRuns - failedRuns);
|
|
997
|
+
const durationMs = allRuns.reduce((sum, run) => {
|
|
998
|
+
const directRaw = run.duration_ms;
|
|
999
|
+
const direct = typeof directRaw === 'number' ? directRaw : null;
|
|
1000
|
+
if (direct !== null)
|
|
1001
|
+
return sum + Math.max(0, direct);
|
|
1002
|
+
const fromToolDurations = Array.isArray(run.tool_durations_ms)
|
|
1003
|
+
? run.tool_durations_ms.reduce((runSum, value) => runSum + (typeof value === 'number' && Number.isFinite(value) ? value : 0), 0)
|
|
1004
|
+
: 0;
|
|
1005
|
+
return sum + Math.max(0, fromToolDurations);
|
|
1006
|
+
}, 0);
|
|
450
1007
|
return ok(`MCPLab run completed: ${runDir}`, {
|
|
451
|
-
runDir,
|
|
1008
|
+
run_dir: runDir,
|
|
1009
|
+
total_scenarios: results.summary.total_scenarios,
|
|
1010
|
+
total_runs: results.summary.total_runs,
|
|
1011
|
+
passed_runs: passedRuns,
|
|
1012
|
+
failed_runs: failedRuns,
|
|
1013
|
+
skipped_runs: skippedRuns,
|
|
1014
|
+
duration_ms: durationMs,
|
|
452
1015
|
summary: results.summary,
|
|
453
1016
|
metadata: results.metadata,
|
|
454
1017
|
scenarios: results.scenarios.map((scenario) => ({
|
|
@@ -461,68 +1024,266 @@ export function registerTools(server) {
|
|
|
461
1024
|
});
|
|
462
1025
|
});
|
|
463
1026
|
});
|
|
464
|
-
|
|
465
|
-
description: '
|
|
1027
|
+
registerTool('mcplab_search_runs', {
|
|
1028
|
+
description: 'Search MCPLab run artifact directories with results.json summary metrics. Use the optional query parameter to filter by run_id, path, or summary fields (case-insensitive substring). Set include_summary=false to skip reading results.json for faster listing.',
|
|
1029
|
+
outputSchema: {
|
|
1030
|
+
runsDir: z.string(),
|
|
1031
|
+
query: z.string().optional(),
|
|
1032
|
+
total_matching: z.number().int().nonnegative(),
|
|
1033
|
+
runs: z.array(RunListEntrySchema)
|
|
1034
|
+
},
|
|
466
1035
|
inputSchema: {
|
|
467
1036
|
runs_dir: z
|
|
468
1037
|
.string()
|
|
469
1038
|
.optional()
|
|
470
1039
|
.describe('Runs directory (default mcplab/results/evaluation-runs).'),
|
|
1040
|
+
query: z
|
|
1041
|
+
.string()
|
|
1042
|
+
.optional()
|
|
1043
|
+
.describe('Optional case-insensitive search query across run id/path and summary fields.'),
|
|
471
1044
|
limit: z
|
|
472
1045
|
.number()
|
|
473
1046
|
.int()
|
|
474
1047
|
.positive()
|
|
475
1048
|
.max(100)
|
|
476
|
-
.
|
|
477
|
-
.describe('Max runs to return
|
|
1049
|
+
.default(10)
|
|
1050
|
+
.describe('Max runs to return. Defaults to 10.'),
|
|
478
1051
|
include_summary: z
|
|
479
1052
|
.boolean()
|
|
480
|
-
.
|
|
481
|
-
.describe('Read results.json summary for each run when available.')
|
|
1053
|
+
.default(true)
|
|
1054
|
+
.describe('Read results.json summary for each run when available. Defaults to true.')
|
|
482
1055
|
}
|
|
483
|
-
}, async ({ runs_dir, limit, include_summary }) => {
|
|
1056
|
+
}, async ({ runs_dir, query, limit, include_summary }) => {
|
|
484
1057
|
return withToolHandling(async () => {
|
|
485
1058
|
const base = resolveRunsDir(runs_dir);
|
|
486
|
-
const entries = listRunsWithFallback(base,
|
|
487
|
-
|
|
1059
|
+
const entries = listRunsWithFallback(base, undefined, include_summary);
|
|
1060
|
+
const searchQuery = String(query ?? '')
|
|
1061
|
+
.trim()
|
|
1062
|
+
.toLowerCase();
|
|
1063
|
+
const filtered = searchQuery
|
|
1064
|
+
? entries.filter((entry) => searchableText(entry).includes(searchQuery))
|
|
1065
|
+
: entries;
|
|
1066
|
+
const capped = filtered.slice(0, limit);
|
|
1067
|
+
return ok(`Found ${filtered.length} run(s) in ${base}`, {
|
|
488
1068
|
runsDir: base,
|
|
489
|
-
|
|
1069
|
+
query: searchQuery || undefined,
|
|
1070
|
+
total_matching: filtered.length,
|
|
1071
|
+
runs: capped
|
|
1072
|
+
});
|
|
1073
|
+
});
|
|
1074
|
+
});
|
|
1075
|
+
registerTool('mcplab_aggregate_runs', {
|
|
1076
|
+
description: 'Aggregate metrics across historical MCPLab runs with compact summary-first output.',
|
|
1077
|
+
outputSchema: z.object({
|
|
1078
|
+
runs: z.array(z.object({
|
|
1079
|
+
run_id: z.string(),
|
|
1080
|
+
timestamp: z.string(),
|
|
1081
|
+
config_hash: z.string()
|
|
1082
|
+
})),
|
|
1083
|
+
group_by: z.enum(['run', 'scenario', 'agent']),
|
|
1084
|
+
filters: z
|
|
1085
|
+
.object({
|
|
1086
|
+
scenario_ids: z.array(z.string()).optional(),
|
|
1087
|
+
agents: z.array(z.string()).optional()
|
|
1088
|
+
})
|
|
1089
|
+
.optional(),
|
|
1090
|
+
summary: MetricSummarySchema.extend({
|
|
1091
|
+
selected_run_count: z.number().int().nonnegative()
|
|
1092
|
+
}),
|
|
1093
|
+
top_worst: z.array(AggregateRowSchema),
|
|
1094
|
+
top_best: z.array(AggregateRowSchema),
|
|
1095
|
+
details: z.array(AggregateRowSchema).optional()
|
|
1096
|
+
}),
|
|
1097
|
+
inputSchema: {
|
|
1098
|
+
runs_dir: z
|
|
1099
|
+
.string()
|
|
1100
|
+
.optional()
|
|
1101
|
+
.describe('Runs directory (default mcplab/results/evaluation-runs).'),
|
|
1102
|
+
run_ids: z
|
|
1103
|
+
.array(z.string())
|
|
1104
|
+
.optional()
|
|
1105
|
+
.describe("Explicit run ids. If present, takes precedence over latest_n. Supports 'LATEST'."),
|
|
1106
|
+
latest_n: z
|
|
1107
|
+
.number()
|
|
1108
|
+
.int()
|
|
1109
|
+
.positive()
|
|
1110
|
+
.max(200)
|
|
1111
|
+
.optional()
|
|
1112
|
+
.describe('Number of latest runs to aggregate when run_ids are not provided (default 20).'),
|
|
1113
|
+
scenario_ids: z.array(z.string()).optional().describe('Optional scenario id filter.'),
|
|
1114
|
+
agents: z.array(z.string()).optional().describe('Optional agent filter.'),
|
|
1115
|
+
group_by: z
|
|
1116
|
+
.enum(['run', 'scenario', 'agent'])
|
|
1117
|
+
.optional()
|
|
1118
|
+
.describe('Grouping for row-level ranking (default run).'),
|
|
1119
|
+
top_n: z
|
|
1120
|
+
.number()
|
|
1121
|
+
.int()
|
|
1122
|
+
.positive()
|
|
1123
|
+
.max(50)
|
|
1124
|
+
.optional()
|
|
1125
|
+
.describe('Max rows for worst/best ranking output (default 10).'),
|
|
1126
|
+
include_details: z
|
|
1127
|
+
.boolean()
|
|
1128
|
+
.optional()
|
|
1129
|
+
.describe('Include full grouped rows. Defaults to false (summary-first).')
|
|
1130
|
+
}
|
|
1131
|
+
}, async ({ runs_dir, run_ids, latest_n, scenario_ids, agents, group_by, top_n, include_details }) => {
|
|
1132
|
+
return withToolHandling(async () => {
|
|
1133
|
+
const loaded = loadRunsForAnalysis({
|
|
1134
|
+
runsDirInput: runs_dir,
|
|
1135
|
+
runIds: run_ids,
|
|
1136
|
+
latestN: latest_n ?? 20
|
|
1137
|
+
});
|
|
1138
|
+
const report = buildAggregateRunsReport({
|
|
1139
|
+
runs: loaded,
|
|
1140
|
+
scenarioIds: scenario_ids,
|
|
1141
|
+
agents,
|
|
1142
|
+
groupBy: group_by ?? 'run',
|
|
1143
|
+
topN: top_n ?? 10,
|
|
1144
|
+
includeDetails: include_details ?? false
|
|
1145
|
+
});
|
|
1146
|
+
return ok(`Aggregated ${loaded.length} run(s)`, report);
|
|
1147
|
+
});
|
|
1148
|
+
});
|
|
1149
|
+
registerTool('mcplab_compare_runs', {
|
|
1150
|
+
description: 'Compare two MCPLab runs and surface compact deltas with regressions/improvements first.',
|
|
1151
|
+
outputSchema: z.object({
|
|
1152
|
+
left_run: z.object({
|
|
1153
|
+
run_id: z.string(),
|
|
1154
|
+
timestamp: z.string(),
|
|
1155
|
+
config_hash: z.string()
|
|
1156
|
+
}),
|
|
1157
|
+
right_run: z.object({
|
|
1158
|
+
run_id: z.string(),
|
|
1159
|
+
timestamp: z.string(),
|
|
1160
|
+
config_hash: z.string()
|
|
1161
|
+
}),
|
|
1162
|
+
filters: z
|
|
1163
|
+
.object({
|
|
1164
|
+
scenario_ids: z.array(z.string()).optional(),
|
|
1165
|
+
agents: z.array(z.string()).optional()
|
|
1166
|
+
})
|
|
1167
|
+
.optional(),
|
|
1168
|
+
summary: z.object({
|
|
1169
|
+
left: MetricSummarySchema,
|
|
1170
|
+
right: MetricSummarySchema,
|
|
1171
|
+
deltas: z.object({
|
|
1172
|
+
pass_rate: z.number(),
|
|
1173
|
+
failed_runs: z.number(),
|
|
1174
|
+
avg_tool_calls_per_run: z.number(),
|
|
1175
|
+
avg_tool_latency_ms: z.number().nullable()
|
|
1176
|
+
}),
|
|
1177
|
+
classification_counts: z.object({
|
|
1178
|
+
regressed: z.number().int().nonnegative(),
|
|
1179
|
+
improved: z.number().int().nonnegative(),
|
|
1180
|
+
unchanged: z.number().int().nonnegative(),
|
|
1181
|
+
new: z.number().int().nonnegative(),
|
|
1182
|
+
missing: z.number().int().nonnegative()
|
|
1183
|
+
})
|
|
1184
|
+
}),
|
|
1185
|
+
regressions: z.array(CompareRowSchema),
|
|
1186
|
+
improvements: z.array(CompareRowSchema),
|
|
1187
|
+
new_items: z.array(CompareRowSchema),
|
|
1188
|
+
missing_items: z.array(CompareRowSchema),
|
|
1189
|
+
details: z.array(CompareRowSchema).optional()
|
|
1190
|
+
}),
|
|
1191
|
+
inputSchema: {
|
|
1192
|
+
runs_dir: z
|
|
1193
|
+
.string()
|
|
1194
|
+
.optional()
|
|
1195
|
+
.describe('Runs directory (default mcplab/results/evaluation-runs).'),
|
|
1196
|
+
left_run_id: z.string().describe("Left run id or 'LATEST'."),
|
|
1197
|
+
right_run_id: z.string().describe("Right run id or 'LATEST'."),
|
|
1198
|
+
scenario_ids: z.array(z.string()).optional().describe('Optional scenario id filter.'),
|
|
1199
|
+
agents: z.array(z.string()).optional().describe('Optional agent filter.'),
|
|
1200
|
+
top_n: z
|
|
1201
|
+
.number()
|
|
1202
|
+
.int()
|
|
1203
|
+
.positive()
|
|
1204
|
+
.max(100)
|
|
1205
|
+
.optional()
|
|
1206
|
+
.describe('Max rows for regressions/improvements (default 20).'),
|
|
1207
|
+
include_details: z
|
|
1208
|
+
.boolean()
|
|
1209
|
+
.optional()
|
|
1210
|
+
.describe('Include full classification rows. Defaults to false (summary-first).')
|
|
1211
|
+
}
|
|
1212
|
+
}, async ({ runs_dir, left_run_id, right_run_id, scenario_ids, agents, top_n, include_details }) => {
|
|
1213
|
+
return withToolHandling(async () => {
|
|
1214
|
+
const base = resolveRunsDir(runs_dir);
|
|
1215
|
+
const left = loadSingleRunForAnalysis(base, left_run_id);
|
|
1216
|
+
const right = loadSingleRunForAnalysis(base, right_run_id);
|
|
1217
|
+
const report = buildCompareRunsReport({
|
|
1218
|
+
left,
|
|
1219
|
+
right,
|
|
1220
|
+
scenarioIds: scenario_ids,
|
|
1221
|
+
agents,
|
|
1222
|
+
topN: top_n ?? 20,
|
|
1223
|
+
includeDetails: include_details ?? false
|
|
490
1224
|
});
|
|
1225
|
+
return ok(`Compared run ${left.run_id} against ${right.run_id}`, report);
|
|
491
1226
|
});
|
|
492
1227
|
});
|
|
493
|
-
|
|
494
|
-
description: '
|
|
1228
|
+
registerTool('mcplab_search_tool_analysis_results', {
|
|
1229
|
+
description: 'Search saved MCP tool analysis reports from mcplab/results/tool-analysis. Use the optional query parameter to filter by report_id, server name, agent name, or summary metadata (case-insensitive substring).',
|
|
1230
|
+
outputSchema: {
|
|
1231
|
+
tool_analysis_results_dir: z.string(),
|
|
1232
|
+
total: z.number().int().nonnegative(),
|
|
1233
|
+
items: z.array(ToolAnalysisListItemSchema)
|
|
1234
|
+
},
|
|
495
1235
|
inputSchema: {
|
|
496
1236
|
tool_analysis_results_dir: z
|
|
497
1237
|
.string()
|
|
498
1238
|
.optional()
|
|
499
1239
|
.describe('Directory containing saved tool analysis report folders.'),
|
|
1240
|
+
query: z
|
|
1241
|
+
.string()
|
|
1242
|
+
.optional()
|
|
1243
|
+
.describe('Optional case-insensitive search query across report id/path/summary metadata.'),
|
|
500
1244
|
limit: z
|
|
501
1245
|
.number()
|
|
502
1246
|
.int()
|
|
503
1247
|
.positive()
|
|
504
1248
|
.max(100)
|
|
505
|
-
.
|
|
506
|
-
.describe('Max reports to return
|
|
1249
|
+
.default(20)
|
|
1250
|
+
.describe('Max reports to return. Defaults to 20.')
|
|
507
1251
|
}
|
|
508
|
-
}, async ({ tool_analysis_results_dir, limit }) => {
|
|
1252
|
+
}, async ({ tool_analysis_results_dir, query, limit }) => {
|
|
509
1253
|
return withToolHandling(async () => {
|
|
510
1254
|
const baseDir = resolveToolAnalysisResultsDir(tool_analysis_results_dir);
|
|
511
|
-
const reports = listToolAnalysisReportsFromDiskWithFallback(baseDir,
|
|
512
|
-
|
|
1255
|
+
const reports = listToolAnalysisReportsFromDiskWithFallback(baseDir, undefined);
|
|
1256
|
+
const searchQuery = String(query ?? '')
|
|
1257
|
+
.trim()
|
|
1258
|
+
.toLowerCase();
|
|
1259
|
+
const filtered = searchQuery
|
|
1260
|
+
? reports.filter((report) => searchableText(report).includes(searchQuery))
|
|
1261
|
+
: reports;
|
|
1262
|
+
const capped = filtered.slice(0, limit);
|
|
1263
|
+
return ok(`Found ${filtered.length} tool analysis report(s) in ${baseDir}`, {
|
|
513
1264
|
tool_analysis_results_dir: baseDir,
|
|
514
|
-
|
|
1265
|
+
query: searchQuery || undefined,
|
|
1266
|
+
total: filtered.length,
|
|
1267
|
+
items: capped
|
|
515
1268
|
});
|
|
516
1269
|
});
|
|
517
1270
|
});
|
|
518
|
-
|
|
1271
|
+
registerTool('mcplab_read_tool_analysis_result', {
|
|
519
1272
|
description: 'Read a saved MCP tool analysis report record (report.json) by report id and return parsed metadata plus optional raw JSON preview.',
|
|
1273
|
+
outputSchema: z.object({
|
|
1274
|
+
path: z.string(),
|
|
1275
|
+
report_id: z.string(),
|
|
1276
|
+
truncated: z.boolean(),
|
|
1277
|
+
raw_json_preview: z.string(),
|
|
1278
|
+
summary: ToolAnalysisSummarySchema,
|
|
1279
|
+
record: GenericObjectSchema.optional()
|
|
1280
|
+
}),
|
|
520
1281
|
inputSchema: {
|
|
521
1282
|
report_id: z.string().describe("Report id directory name (or 'LATEST')."),
|
|
522
1283
|
tool_analysis_results_dir: z
|
|
523
1284
|
.string()
|
|
524
1285
|
.optional()
|
|
525
|
-
.describe('Directory containing saved tool analysis reports.'),
|
|
1286
|
+
.describe('Directory containing saved tool analysis reports (default mcplab/results/tool-analysis).'),
|
|
526
1287
|
max_chars: z
|
|
527
1288
|
.number()
|
|
528
1289
|
.int()
|
|
@@ -562,32 +1323,83 @@ export function registerTools(server) {
|
|
|
562
1323
|
return ok(`Read tool analysis report ${resolvedReportId}`, structured);
|
|
563
1324
|
});
|
|
564
1325
|
});
|
|
565
|
-
|
|
1326
|
+
registerTool('mcplab_delete_tool_analysis_result', {
|
|
566
1327
|
description: 'Delete a saved MCP tool analysis report directory by report id (from mcplab/results/tool-analysis by default).',
|
|
1328
|
+
outputSchema: {
|
|
1329
|
+
status: z.enum(['deleted', 'not_found', 'dry_run']),
|
|
1330
|
+
report_id: z.string(),
|
|
1331
|
+
path: z.string(),
|
|
1332
|
+
tool_analysis_results_dir: z.string(),
|
|
1333
|
+
existed: z.boolean(),
|
|
1334
|
+
deleted: z.boolean(),
|
|
1335
|
+
would_delete: z.boolean()
|
|
1336
|
+
},
|
|
567
1337
|
inputSchema: {
|
|
568
1338
|
report_id: z.string().describe('Report id directory name to delete.'),
|
|
569
1339
|
tool_analysis_results_dir: z
|
|
570
1340
|
.string()
|
|
571
1341
|
.optional()
|
|
572
|
-
.describe('Directory containing saved tool analysis reports.')
|
|
1342
|
+
.describe('Directory containing saved tool analysis reports (default mcplab/results/tool-analysis).'),
|
|
1343
|
+
dry_run: z
|
|
1344
|
+
.boolean()
|
|
1345
|
+
.optional()
|
|
1346
|
+
.describe('If true, return what would be deleted without deleting anything.'),
|
|
1347
|
+
confirm: z
|
|
1348
|
+
.boolean()
|
|
1349
|
+
.optional()
|
|
1350
|
+
.describe('Must be true to execute deletion when dry_run is false.')
|
|
573
1351
|
}
|
|
574
|
-
}, async ({ report_id, tool_analysis_results_dir }) => {
|
|
1352
|
+
}, async ({ report_id, tool_analysis_results_dir, dry_run, confirm }) => {
|
|
575
1353
|
return withToolHandling(async () => {
|
|
576
1354
|
const baseDir = resolveToolAnalysisResultsDir(tool_analysis_results_dir);
|
|
577
1355
|
const dirPath = toolAnalysisReportDirPathWithFallback(baseDir, report_id.trim());
|
|
578
|
-
|
|
579
|
-
|
|
1356
|
+
const existed = existsSync(dirPath);
|
|
1357
|
+
const isDryRun = Boolean(dry_run);
|
|
1358
|
+
if (isDryRun) {
|
|
1359
|
+
return ok(`Dry run for delete tool analysis report ${report_id}`, {
|
|
1360
|
+
status: 'dry_run',
|
|
1361
|
+
report_id: report_id.trim(),
|
|
1362
|
+
path: dirPath,
|
|
1363
|
+
tool_analysis_results_dir: baseDir,
|
|
1364
|
+
existed,
|
|
1365
|
+
deleted: false,
|
|
1366
|
+
would_delete: existed
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
if (confirm !== true) {
|
|
1370
|
+
throw new Error('confirm=true is required to delete a tool analysis report');
|
|
1371
|
+
}
|
|
1372
|
+
if (!existed) {
|
|
1373
|
+
return ok(`Tool analysis report not found: ${report_id}`, {
|
|
1374
|
+
status: 'not_found',
|
|
1375
|
+
report_id: report_id.trim(),
|
|
1376
|
+
path: dirPath,
|
|
1377
|
+
tool_analysis_results_dir: baseDir,
|
|
1378
|
+
existed: false,
|
|
1379
|
+
deleted: false,
|
|
1380
|
+
would_delete: false
|
|
1381
|
+
});
|
|
580
1382
|
}
|
|
581
1383
|
rmSync(dirPath, { recursive: true, force: false });
|
|
582
1384
|
return ok(`Deleted tool analysis report ${report_id}`, {
|
|
1385
|
+
status: 'deleted',
|
|
583
1386
|
report_id: report_id.trim(),
|
|
584
1387
|
path: dirPath,
|
|
585
|
-
tool_analysis_results_dir: baseDir
|
|
1388
|
+
tool_analysis_results_dir: baseDir,
|
|
1389
|
+
existed: true,
|
|
1390
|
+
deleted: true,
|
|
1391
|
+
would_delete: true
|
|
586
1392
|
});
|
|
587
1393
|
});
|
|
588
1394
|
});
|
|
589
|
-
|
|
1395
|
+
registerTool('mcplab_trace_list_events', {
|
|
590
1396
|
description: 'List structured trace timeline items for a MCPLab run (flattened from scenario_run trace records) with optional type/scenario/agent filtering.',
|
|
1397
|
+
outputSchema: {
|
|
1398
|
+
run_id: z.string(),
|
|
1399
|
+
legacy_trace_detected: z.boolean().optional(),
|
|
1400
|
+
total_matching: z.number().int().nonnegative(),
|
|
1401
|
+
items: z.array(FlattenedTraceItemSchema)
|
|
1402
|
+
},
|
|
591
1403
|
inputSchema: {
|
|
592
1404
|
runs_dir: z
|
|
593
1405
|
.string()
|
|
@@ -637,8 +1449,20 @@ export function registerTools(server) {
|
|
|
637
1449
|
});
|
|
638
1450
|
});
|
|
639
1451
|
});
|
|
640
|
-
|
|
1452
|
+
registerTool('mcplab_trace_get_final_answers', {
|
|
641
1453
|
description: 'Extract final assistant answers from a run trace (scenario_run documents) for easy agent output comparison.',
|
|
1454
|
+
outputSchema: {
|
|
1455
|
+
run_id: z.string(),
|
|
1456
|
+
legacy_trace_detected: z.boolean().optional(),
|
|
1457
|
+
items: z.array(z.object({
|
|
1458
|
+
index: z.number().int().nonnegative(),
|
|
1459
|
+
scenario_id: z.string(),
|
|
1460
|
+
agent: z.string(),
|
|
1461
|
+
ts: z.string().optional(),
|
|
1462
|
+
truncated: z.boolean(),
|
|
1463
|
+
text: z.string()
|
|
1464
|
+
}))
|
|
1465
|
+
},
|
|
642
1466
|
inputSchema: {
|
|
643
1467
|
runs_dir: z
|
|
644
1468
|
.string()
|
|
@@ -684,8 +1508,15 @@ export function registerTools(server) {
|
|
|
684
1508
|
});
|
|
685
1509
|
});
|
|
686
1510
|
});
|
|
687
|
-
|
|
1511
|
+
registerTool('mcplab_trace_get_conversation', {
|
|
688
1512
|
description: 'Return a structured conversation timeline (messages + tool blocks) for a specific scenario+agent in a scenario_run trace.',
|
|
1513
|
+
outputSchema: {
|
|
1514
|
+
run_id: z.string(),
|
|
1515
|
+
scenario_id: z.string(),
|
|
1516
|
+
agent: z.string(),
|
|
1517
|
+
legacy_trace_detected: z.boolean().optional(),
|
|
1518
|
+
timeline: z.array(ConversationTimelineItemSchema)
|
|
1519
|
+
},
|
|
689
1520
|
inputSchema: {
|
|
690
1521
|
runs_dir: z
|
|
691
1522
|
.string()
|
|
@@ -726,8 +1557,14 @@ export function registerTools(server) {
|
|
|
726
1557
|
});
|
|
727
1558
|
});
|
|
728
1559
|
});
|
|
729
|
-
|
|
1560
|
+
registerTool('mcplab_trace_search', {
|
|
730
1561
|
description: 'Search scenario_run trace content for a text query and return matching message/block items.',
|
|
1562
|
+
outputSchema: {
|
|
1563
|
+
run_id: z.string(),
|
|
1564
|
+
query: z.string(),
|
|
1565
|
+
legacy_trace_detected: z.boolean().optional(),
|
|
1566
|
+
matches: z.array(FlattenedTraceItemSchema)
|
|
1567
|
+
},
|
|
731
1568
|
inputSchema: {
|
|
732
1569
|
runs_dir: z
|
|
733
1570
|
.string()
|
|
@@ -776,8 +1613,24 @@ export function registerTools(server) {
|
|
|
776
1613
|
});
|
|
777
1614
|
});
|
|
778
1615
|
});
|
|
779
|
-
|
|
1616
|
+
registerTool('mcplab_trace_stats', {
|
|
780
1617
|
description: 'Compute trace statistics for a run (message/block counts, tool usage, durations, and final-answer counts).',
|
|
1618
|
+
outputSchema: {
|
|
1619
|
+
run_id: z.string(),
|
|
1620
|
+
legacy_trace_detected: z.boolean().optional(),
|
|
1621
|
+
total_scenario_records: z.number().int().nonnegative(),
|
|
1622
|
+
message_role_counts: z.record(z.number()),
|
|
1623
|
+
block_type_counts: z.record(z.number()),
|
|
1624
|
+
scenario_agent_pairs: z.number().int().nonnegative(),
|
|
1625
|
+
tool_call_count: z.number().int().nonnegative(),
|
|
1626
|
+
tool_result_count: z.number().int().nonnegative(),
|
|
1627
|
+
final_answer_count: z.number().int().nonnegative(),
|
|
1628
|
+
avg_tool_result_duration_ms: z.number().nullable(),
|
|
1629
|
+
tool_usage: z.array(z.object({
|
|
1630
|
+
tool: z.string(),
|
|
1631
|
+
count: z.number().int().nonnegative()
|
|
1632
|
+
}))
|
|
1633
|
+
},
|
|
781
1634
|
inputSchema: {
|
|
782
1635
|
runs_dir: z
|
|
783
1636
|
.string()
|
|
@@ -832,8 +1685,31 @@ export function registerTools(server) {
|
|
|
832
1685
|
});
|
|
833
1686
|
});
|
|
834
1687
|
});
|
|
835
|
-
|
|
1688
|
+
registerTool('mcplab_read_run_artifact', {
|
|
836
1689
|
description: 'Read MCPLab run artifacts such as results.json, summary.md, trace.jsonl, resolved-config.yaml, or report.html.',
|
|
1690
|
+
outputSchema: {
|
|
1691
|
+
path: z.string(),
|
|
1692
|
+
run_id: z.string(),
|
|
1693
|
+
artifact: z.enum([
|
|
1694
|
+
'results.json',
|
|
1695
|
+
'summary.md',
|
|
1696
|
+
'trace.jsonl',
|
|
1697
|
+
'resolved-config.yaml',
|
|
1698
|
+
'report.html'
|
|
1699
|
+
]),
|
|
1700
|
+
line_range: z.string().optional(),
|
|
1701
|
+
truncated: z.boolean(),
|
|
1702
|
+
content: z.string(),
|
|
1703
|
+
summary: ResultsSummarySchema.optional(),
|
|
1704
|
+
metadata: ResultsMetadataSchema.optional(),
|
|
1705
|
+
scenarios: z
|
|
1706
|
+
.array(z.object({
|
|
1707
|
+
scenario_id: z.string(),
|
|
1708
|
+
agent: z.string(),
|
|
1709
|
+
pass_rate: z.number()
|
|
1710
|
+
}))
|
|
1711
|
+
.optional()
|
|
1712
|
+
},
|
|
837
1713
|
inputSchema: {
|
|
838
1714
|
runs_dir: z
|
|
839
1715
|
.string()
|
|
@@ -917,8 +1793,32 @@ export function registerTools(server) {
|
|
|
917
1793
|
return ok(`Read ${artifact} from run ${resolvedRunId}`, structured);
|
|
918
1794
|
});
|
|
919
1795
|
});
|
|
920
|
-
|
|
1796
|
+
registerTool('mcplab_grep_run_artifact', {
|
|
921
1797
|
description: 'Search for text within a MCPLab run artifact and return matching lines with surrounding context. Use this to find specific sections in large files (e.g. a tool name in report.html) without reading the full file. Returns line numbers so you can follow up with mcplab_read_run_artifact line_start/line_end to read the full section.',
|
|
1798
|
+
outputSchema: {
|
|
1799
|
+
run_id: z.string(),
|
|
1800
|
+
artifact: z.enum([
|
|
1801
|
+
'results.json',
|
|
1802
|
+
'summary.md',
|
|
1803
|
+
'trace.jsonl',
|
|
1804
|
+
'resolved-config.yaml',
|
|
1805
|
+
'report.html'
|
|
1806
|
+
]),
|
|
1807
|
+
query: z.string(),
|
|
1808
|
+
total_lines: z.number().int().nonnegative(),
|
|
1809
|
+
match_count: z.number().int().nonnegative(),
|
|
1810
|
+
truncated_at_limit: z.boolean(),
|
|
1811
|
+
matches: z.array(z.object({
|
|
1812
|
+
match_line: z.number().int().positive(),
|
|
1813
|
+
context_start_line: z.number().int().positive(),
|
|
1814
|
+
context_end_line: z.number().int().positive(),
|
|
1815
|
+
lines: z.array(z.object({
|
|
1816
|
+
line: z.number().int().positive(),
|
|
1817
|
+
text: z.string(),
|
|
1818
|
+
is_match: z.boolean()
|
|
1819
|
+
}))
|
|
1820
|
+
}))
|
|
1821
|
+
},
|
|
922
1822
|
inputSchema: {
|
|
923
1823
|
runs_dir: z
|
|
924
1824
|
.string()
|
|
@@ -1068,6 +1968,75 @@ export function registerPrompts(server) {
|
|
|
1068
1968
|
};
|
|
1069
1969
|
});
|
|
1070
1970
|
}
|
|
1971
|
+
const DESTRUCTIVE_TOOLS = new Set([
|
|
1972
|
+
'mcplab_delete_tool_analysis_result',
|
|
1973
|
+
'mcplab_write_markdown_report',
|
|
1974
|
+
'mcplab_run_eval'
|
|
1975
|
+
]);
|
|
1976
|
+
const MUTATING_TOOLS = new Set(['mcplab_write_markdown_report', 'mcplab_run_eval']);
|
|
1977
|
+
const OPEN_WORLD_TOOLS = new Set(['mcplab_run_eval']);
|
|
1978
|
+
const PREFERRED_TOOL_TITLES = {
|
|
1979
|
+
mcplab_write_markdown_report: 'Write Markdown Report to Disk',
|
|
1980
|
+
mcplab_search_markdown_reports: 'Search Markdown Reports',
|
|
1981
|
+
mcplab_list_library: 'Search Library Entries',
|
|
1982
|
+
mcplab_generate_agent_entry: 'Generate MCPLab agents.yaml Entry',
|
|
1983
|
+
mcplab_search_runs: 'Search Evaluation Runs',
|
|
1984
|
+
mcplab_search_tool_analysis_results: 'Search Tool Analysis Results',
|
|
1985
|
+
mcplab_trace_search: 'Search Trace Events',
|
|
1986
|
+
mcplab_grep_run_artifact: 'Search Run Artifact Text'
|
|
1987
|
+
};
|
|
1988
|
+
function normalizeOptionalNonEmpty(value) {
|
|
1989
|
+
if (typeof value !== 'string')
|
|
1990
|
+
return undefined;
|
|
1991
|
+
const trimmed = value.trim();
|
|
1992
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
1993
|
+
}
|
|
1994
|
+
function resolveToolTitle(toolName, explicitTitle, annotationTitle) {
|
|
1995
|
+
return (normalizeOptionalNonEmpty(explicitTitle) ??
|
|
1996
|
+
normalizeOptionalNonEmpty(annotationTitle) ??
|
|
1997
|
+
PREFERRED_TOOL_TITLES[toolName] ??
|
|
1998
|
+
humanizeToolName(toolName));
|
|
1999
|
+
}
|
|
2000
|
+
function inferToolAnnotations(toolName, resolvedTitle, override) {
|
|
2001
|
+
const readOnly = !MUTATING_TOOLS.has(toolName) && !DESTRUCTIVE_TOOLS.has(toolName);
|
|
2002
|
+
const openWorld = OPEN_WORLD_TOOLS.has(toolName);
|
|
2003
|
+
const title = normalizeOptionalNonEmpty(override?.title) ?? resolvedTitle;
|
|
2004
|
+
const baseHints = readOnly
|
|
2005
|
+
? {
|
|
2006
|
+
title,
|
|
2007
|
+
readOnlyHint: true,
|
|
2008
|
+
idempotentHint: true,
|
|
2009
|
+
openWorldHint: openWorld
|
|
2010
|
+
}
|
|
2011
|
+
: DESTRUCTIVE_TOOLS.has(toolName)
|
|
2012
|
+
? {
|
|
2013
|
+
title,
|
|
2014
|
+
readOnlyHint: false,
|
|
2015
|
+
destructiveHint: true,
|
|
2016
|
+
idempotentHint: false,
|
|
2017
|
+
openWorldHint: openWorld
|
|
2018
|
+
}
|
|
2019
|
+
: {
|
|
2020
|
+
title,
|
|
2021
|
+
readOnlyHint: false,
|
|
2022
|
+
destructiveHint: false,
|
|
2023
|
+
idempotentHint: false,
|
|
2024
|
+
openWorldHint: openWorld
|
|
2025
|
+
};
|
|
2026
|
+
return {
|
|
2027
|
+
...baseHints,
|
|
2028
|
+
...override,
|
|
2029
|
+
title
|
|
2030
|
+
};
|
|
2031
|
+
}
|
|
2032
|
+
function humanizeToolName(toolName) {
|
|
2033
|
+
const base = toolName.startsWith('mcplab_') ? toolName.slice('mcplab_'.length) : toolName;
|
|
2034
|
+
return base
|
|
2035
|
+
.split('_')
|
|
2036
|
+
.filter(Boolean)
|
|
2037
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
2038
|
+
.join(' ');
|
|
2039
|
+
}
|
|
1071
2040
|
function resolveBundleRoot(bundleRoot) {
|
|
1072
2041
|
if (bundleRoot?.trim())
|
|
1073
2042
|
return resolve(bundleRoot);
|
|
@@ -1282,6 +2251,74 @@ function summarizeConfig(config) {
|
|
|
1282
2251
|
}))
|
|
1283
2252
|
};
|
|
1284
2253
|
}
|
|
2254
|
+
function normalizeOptionalFilterSet(values) {
|
|
2255
|
+
if (!values || values.length === 0)
|
|
2256
|
+
return null;
|
|
2257
|
+
const normalized = values.map((value) => value.trim()).filter(Boolean);
|
|
2258
|
+
return normalized.length > 0 ? new Set(normalized) : null;
|
|
2259
|
+
}
|
|
2260
|
+
function filterScenarios(scenarios, scenarioFilter, agentFilter) {
|
|
2261
|
+
if (!scenarioFilter && !agentFilter)
|
|
2262
|
+
return scenarios;
|
|
2263
|
+
return scenarios.filter((scenario) => {
|
|
2264
|
+
const scenarioMatch = !scenarioFilter || scenarioFilter.has(scenario.scenario_id);
|
|
2265
|
+
const agentMatch = !agentFilter || agentFilter.has(scenario.agent);
|
|
2266
|
+
return scenarioMatch && agentMatch;
|
|
2267
|
+
});
|
|
2268
|
+
}
|
|
2269
|
+
function loadRunsForAnalysis(params) {
|
|
2270
|
+
const base = resolveRunsDir(params.runsDirInput);
|
|
2271
|
+
const ids = selectRunIdsForAnalysis(base, params.runIds, params.latestN);
|
|
2272
|
+
return ids.map((id) => loadSingleRunForAnalysis(base, id));
|
|
2273
|
+
}
|
|
2274
|
+
function loadSingleRunForAnalysis(primaryRunsDir, runIdInput) {
|
|
2275
|
+
const resolvedRunId = resolveRunIdToken(primaryRunsDir, runIdInput);
|
|
2276
|
+
const readBase = resolveExistingRunReadDir(primaryRunsDir, resolvedRunId);
|
|
2277
|
+
const runPath = join(readBase, resolvedRunId);
|
|
2278
|
+
const resultsPath = join(runPath, 'results.json');
|
|
2279
|
+
if (!existsSync(resultsPath)) {
|
|
2280
|
+
throw new Error(`results.json not found for run '${resolvedRunId}' at ${resultsPath}`);
|
|
2281
|
+
}
|
|
2282
|
+
const parsed = JSON.parse(readFileSync(resultsPath, 'utf8'));
|
|
2283
|
+
return {
|
|
2284
|
+
run_id: resolvedRunId,
|
|
2285
|
+
path: runPath,
|
|
2286
|
+
results: parsed
|
|
2287
|
+
};
|
|
2288
|
+
}
|
|
2289
|
+
function selectRunIdsForAnalysis(primaryRunsDir, runIds, latestN) {
|
|
2290
|
+
if (runIds && runIds.length > 0) {
|
|
2291
|
+
const out = [];
|
|
2292
|
+
const seen = new Set();
|
|
2293
|
+
for (const token of runIds) {
|
|
2294
|
+
const resolved = resolveRunIdToken(primaryRunsDir, token);
|
|
2295
|
+
if (!seen.has(resolved)) {
|
|
2296
|
+
out.push(resolved);
|
|
2297
|
+
seen.add(resolved);
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
return out;
|
|
2301
|
+
}
|
|
2302
|
+
const discovered = listRunsWithFallback(primaryRunsDir, latestN, false)
|
|
2303
|
+
.map((entry) => String(entry.run_id ?? '').trim())
|
|
2304
|
+
.filter(Boolean);
|
|
2305
|
+
if (discovered.length === 0) {
|
|
2306
|
+
throw new Error(`No runs found in ${primaryRunsDir}`);
|
|
2307
|
+
}
|
|
2308
|
+
return discovered;
|
|
2309
|
+
}
|
|
2310
|
+
function resolveRunIdToken(primaryRunsDir, runIdInput) {
|
|
2311
|
+
const token = String(runIdInput ?? '').trim();
|
|
2312
|
+
if (!token)
|
|
2313
|
+
throw new Error('run id is required');
|
|
2314
|
+
if (token !== 'LATEST')
|
|
2315
|
+
return token;
|
|
2316
|
+
const latest = latestRunId(primaryRunsDir);
|
|
2317
|
+
if (!latest) {
|
|
2318
|
+
throw new Error(`No runs found in ${primaryRunsDir}`);
|
|
2319
|
+
}
|
|
2320
|
+
return latest;
|
|
2321
|
+
}
|
|
1285
2322
|
function listRuns(runsDir, limit, includeSummary) {
|
|
1286
2323
|
if (!existsSync(runsDir))
|
|
1287
2324
|
return [];
|
|
@@ -1296,9 +2333,9 @@ function listRuns(runsDir, limit, includeSummary) {
|
|
|
1296
2333
|
}
|
|
1297
2334
|
})
|
|
1298
2335
|
.sort()
|
|
1299
|
-
.reverse()
|
|
1300
|
-
|
|
1301
|
-
return
|
|
2336
|
+
.reverse();
|
|
2337
|
+
const cappedDirNames = typeof limit === 'number' ? dirNames.slice(0, limit) : dirNames;
|
|
2338
|
+
return cappedDirNames.map((runId) => {
|
|
1302
2339
|
const out = {
|
|
1303
2340
|
run_id: runId,
|
|
1304
2341
|
path: join(runsDir, runId)
|
|
@@ -1349,7 +2386,7 @@ function listRunsWithFallback(primaryRunsDir, limit, includeSummary) {
|
|
|
1349
2386
|
}
|
|
1350
2387
|
return Array.from(merged.values())
|
|
1351
2388
|
.sort((a, b) => String(b.run_id ?? '').localeCompare(String(a.run_id ?? '')))
|
|
1352
|
-
.slice(0, limit);
|
|
2389
|
+
.slice(0, typeof limit === 'number' ? limit : Number.MAX_SAFE_INTEGER);
|
|
1353
2390
|
}
|
|
1354
2391
|
function resolveExistingRunReadDir(primaryRunsDir, runId) {
|
|
1355
2392
|
if (!runId)
|
|
@@ -1546,10 +2583,10 @@ function listToolAnalysisReportsFromDisk(baseDir, limit) {
|
|
|
1546
2583
|
}
|
|
1547
2584
|
})
|
|
1548
2585
|
.sort()
|
|
1549
|
-
.reverse()
|
|
1550
|
-
|
|
2586
|
+
.reverse();
|
|
2587
|
+
const cappedIds = typeof limit === 'number' ? ids.slice(0, limit) : ids;
|
|
1551
2588
|
const out = [];
|
|
1552
|
-
for (const reportId of
|
|
2589
|
+
for (const reportId of cappedIds) {
|
|
1553
2590
|
try {
|
|
1554
2591
|
const filePath = toolAnalysisReportFilePath(baseDir, reportId);
|
|
1555
2592
|
if (!existsSync(filePath))
|
|
@@ -1582,7 +2619,7 @@ function listToolAnalysisReportsFromDiskWithFallback(baseDir, limit) {
|
|
|
1582
2619
|
}
|
|
1583
2620
|
return Array.from(merged.values())
|
|
1584
2621
|
.sort((a, b) => String(b.report_id ?? '').localeCompare(String(a.report_id ?? '')))
|
|
1585
|
-
.slice(0, limit);
|
|
2622
|
+
.slice(0, typeof limit === 'number' ? limit : Number.MAX_SAFE_INTEGER);
|
|
1586
2623
|
}
|
|
1587
2624
|
function toolAnalysisReportDirPathWithFallback(baseDir, reportId) {
|
|
1588
2625
|
for (const dir of toolAnalysisReadDirs(baseDir)) {
|
|
@@ -1836,6 +2873,35 @@ async function withToolHandling(fn) {
|
|
|
1836
2873
|
function removeUndefined(value) {
|
|
1837
2874
|
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined));
|
|
1838
2875
|
}
|
|
2876
|
+
function searchableText(value) {
|
|
2877
|
+
const tokens = [];
|
|
2878
|
+
const visit = (node) => {
|
|
2879
|
+
if (node === null || node === undefined)
|
|
2880
|
+
return;
|
|
2881
|
+
if (typeof node === 'string') {
|
|
2882
|
+
const trimmed = node.trim();
|
|
2883
|
+
if (trimmed)
|
|
2884
|
+
tokens.push(trimmed.toLowerCase());
|
|
2885
|
+
return;
|
|
2886
|
+
}
|
|
2887
|
+
if (typeof node === 'number' || typeof node === 'boolean') {
|
|
2888
|
+
tokens.push(String(node).toLowerCase());
|
|
2889
|
+
return;
|
|
2890
|
+
}
|
|
2891
|
+
if (Array.isArray(node)) {
|
|
2892
|
+
for (const item of node)
|
|
2893
|
+
visit(item);
|
|
2894
|
+
return;
|
|
2895
|
+
}
|
|
2896
|
+
if (typeof node === 'object') {
|
|
2897
|
+
for (const entryValue of Object.values(node)) {
|
|
2898
|
+
visit(entryValue);
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
};
|
|
2902
|
+
visit(value);
|
|
2903
|
+
return tokens.join(' ');
|
|
2904
|
+
}
|
|
1839
2905
|
function normalizeStringArray(values) {
|
|
1840
2906
|
if (!values)
|
|
1841
2907
|
return undefined;
|