@inspectr/mcplab-mcp-server 1.7.2 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/runtime.contracts.test.js +184 -3
- package/dist/runtime.contracts.test.js.map +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +820 -97
- package/dist/runtime.js.map +1 -1
- package/dist/skills/mcplab-assistant/SKILL.md +335 -0
- package/dist/skills/mcplab-assistant/references/cli-recipes.md +104 -0
- package/dist/skills/mcplab-assistant/references/config-recipes.md +253 -0
- package/dist/skills/mcplab-assistant/references/examples-map.md +33 -0
- package/dist/skills/mcplab-assistant/references/troubleshooting.md +35 -0
- package/dist/skills.d.ts +47 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/skills.js +190 -0
- package/dist/skills.js.map +1 -0
- package/dist/skills.test.d.ts +2 -0
- package/dist/skills.test.d.ts.map +1 -0
- package/dist/skills.test.js +150 -0
- package/dist/skills.test.js.map +1 -0
- package/package.json +4 -4
package/dist/runtime.js
CHANGED
|
@@ -5,10 +5,11 @@ import { basename, dirname, extname, resolve, join, sep, relative } from 'node:p
|
|
|
5
5
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
6
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
7
7
|
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
8
|
-
import { loadConfig, hashConfig, runAll, selectScenarios, loadOrBuildSearchIndex, indexNeedsRefresh, getResultsIndexPaths, resolveRunArtifactPath, searchDocs, getContext, applyRuntimeServerOverrides, readLibraryAgentsAndServers } from '@inspectr/mcplab-core';
|
|
8
|
+
import { loadConfig, hashConfig, runAll, selectScenarios, loadOrBuildSearchIndex, indexNeedsRefresh, getResultsIndexPaths, resolveRunArtifactPath, searchDocs, getContext, applyRuntimeServerOverrides, readLibraryAgentsAndServers, resolveScenarioLibraryDir, createEvaluationConfigFile, updateEvaluationConfigFile, readEvaluationConfigFile, createTestCaseFile, readTestCaseFile, updateTestCaseFile, buildScenarioEntry } 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
|
+
import { loadSkills, readSkillFile, registerSkills } from './skills.js';
|
|
12
13
|
import { buildAggregateRunsReport, buildCompareRunsReport } from './mcp-run-calculations.js';
|
|
13
14
|
const PACKAGE_JSON = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
14
15
|
const SERVER_VERSION = typeof PACKAGE_JSON.version === 'string' && PACKAGE_JSON.version.trim().length > 0
|
|
@@ -59,12 +60,7 @@ const AggregateRowSchema = z.object({
|
|
|
59
60
|
max: z.string()
|
|
60
61
|
})
|
|
61
62
|
.optional(),
|
|
62
|
-
|
|
63
|
-
passed_runs: z.number().int().nonnegative(),
|
|
64
|
-
failed_runs: z.number().int().nonnegative(),
|
|
65
|
-
pass_rate: z.number(),
|
|
66
|
-
avg_tool_calls_per_run: z.number(),
|
|
67
|
-
avg_tool_latency_ms: z.number().nullable()
|
|
63
|
+
summary: MetricSummarySchema
|
|
68
64
|
});
|
|
69
65
|
const CompareRowSchema = z.object({
|
|
70
66
|
key: z.string(),
|
|
@@ -122,8 +118,28 @@ const LibraryEntrySchema = z.object({
|
|
|
122
118
|
bundleRoot: z.string(),
|
|
123
119
|
servers: z.array(LibraryServerEntrySchema),
|
|
124
120
|
agents: z.array(LibraryAgentEntrySchema),
|
|
121
|
+
test_cases: z.array(LibraryScenarioEntrySchema),
|
|
125
122
|
scenarios: z.array(LibraryScenarioEntrySchema)
|
|
126
123
|
});
|
|
124
|
+
const EvaluationConfigListItemSchema = z.object({
|
|
125
|
+
id: z.string(),
|
|
126
|
+
name: z.string(),
|
|
127
|
+
relative_path: z.string(),
|
|
128
|
+
config_path: z.string(),
|
|
129
|
+
suite_path: z.string(),
|
|
130
|
+
updated_at: z.string(),
|
|
131
|
+
scenario_count: z.number().int().nonnegative(),
|
|
132
|
+
agent_count: z.number().int().nonnegative(),
|
|
133
|
+
error: z.string().optional()
|
|
134
|
+
});
|
|
135
|
+
const EvaluationConfigListSchema = z.object({
|
|
136
|
+
evals_dir: z.string(),
|
|
137
|
+
total: z.number().int().nonnegative(),
|
|
138
|
+
matching: z.number().int().nonnegative(),
|
|
139
|
+
filters: GenericObjectSchema,
|
|
140
|
+
sort: z.object({ by: z.string(), direction: z.enum(['asc', 'desc']) }),
|
|
141
|
+
configs: z.array(EvaluationConfigListItemSchema)
|
|
142
|
+
});
|
|
127
143
|
const ServerAuthSchema = z.union([
|
|
128
144
|
z.object({
|
|
129
145
|
type: z.literal('bearer'),
|
|
@@ -148,12 +164,148 @@ const ServerEntrySchema = z.object({
|
|
|
148
164
|
url: z.string(),
|
|
149
165
|
auth: ServerAuthSchema.optional()
|
|
150
166
|
});
|
|
167
|
+
const ResponseAssertionSchema = z
|
|
168
|
+
.discriminatedUnion('type', [
|
|
169
|
+
z.object({
|
|
170
|
+
type: z.literal('regex').describe('Match the final response against a regular expression.'),
|
|
171
|
+
pattern: z.string().describe('JavaScript regex pattern; matching is case-insensitive.')
|
|
172
|
+
}),
|
|
173
|
+
z.object({
|
|
174
|
+
type: z.literal('contains').describe('Require literal text in the final response.'),
|
|
175
|
+
value: z.string().describe('Case-insensitive text that must appear.')
|
|
176
|
+
}),
|
|
177
|
+
z.object({
|
|
178
|
+
type: z
|
|
179
|
+
.literal('not_contains')
|
|
180
|
+
.describe('Require literal text to be absent from the final response.'),
|
|
181
|
+
value: z.string().describe('Case-insensitive text that must not appear.')
|
|
182
|
+
}),
|
|
183
|
+
z.object({
|
|
184
|
+
type: z
|
|
185
|
+
.literal('starts_with')
|
|
186
|
+
.describe('Require the final response to start with literal text.'),
|
|
187
|
+
value: z.string().describe('Case-insensitive prefix.')
|
|
188
|
+
}),
|
|
189
|
+
z.object({
|
|
190
|
+
type: z.literal('ends_with').describe('Require the final response to end with literal text.'),
|
|
191
|
+
value: z.string().describe('Case-insensitive suffix.')
|
|
192
|
+
}),
|
|
193
|
+
z.object({
|
|
194
|
+
type: z
|
|
195
|
+
.literal('equals')
|
|
196
|
+
.describe('Require the entire final response to equal literal text.'),
|
|
197
|
+
value: z.string().describe('Case-insensitive expected response.')
|
|
198
|
+
}),
|
|
199
|
+
z.object({
|
|
200
|
+
type: z.literal('jsonpath').describe('Inspect a value in a JSON final response.'),
|
|
201
|
+
path: z.string().describe('JSONPath expression.'),
|
|
202
|
+
equals: z
|
|
203
|
+
.union([z.string(), z.number(), z.boolean()])
|
|
204
|
+
.optional()
|
|
205
|
+
.describe('Optional primitive value that must match.')
|
|
206
|
+
}),
|
|
207
|
+
z.object({
|
|
208
|
+
type: z
|
|
209
|
+
.literal('jsonpath_exists')
|
|
210
|
+
.describe('Require a JSONPath to exist in a JSON final response.'),
|
|
211
|
+
path: z.string().describe('JSONPath expression.')
|
|
212
|
+
}),
|
|
213
|
+
z.object({
|
|
214
|
+
type: z
|
|
215
|
+
.literal('jsonpath_not_exists')
|
|
216
|
+
.describe('Require a JSONPath to be absent from a JSON final response.'),
|
|
217
|
+
path: z.string().describe('JSONPath expression.')
|
|
218
|
+
})
|
|
219
|
+
])
|
|
220
|
+
.describe('Assertions evaluated against the final response text or JSON.');
|
|
221
|
+
const ToolInputAssertionSchema = z
|
|
222
|
+
.discriminatedUnion('type', [
|
|
223
|
+
z.object({
|
|
224
|
+
type: z.literal('contains').describe('Require text in serialized tool arguments.'),
|
|
225
|
+
tool: z.string().describe('Raw MCP tool name.'),
|
|
226
|
+
value: z.string().describe('Case-insensitive text that must appear in arguments.')
|
|
227
|
+
}),
|
|
228
|
+
z.object({
|
|
229
|
+
type: z.literal('regex').describe('Match serialized tool arguments with a regex.'),
|
|
230
|
+
tool: z.string().describe('Raw MCP tool name.'),
|
|
231
|
+
pattern: z.string().describe('JavaScript regex pattern.')
|
|
232
|
+
}),
|
|
233
|
+
z.object({
|
|
234
|
+
type: z.literal('jsonpath').describe('Inspect a value in tool arguments.'),
|
|
235
|
+
tool: z.string().describe('Raw MCP tool name.'),
|
|
236
|
+
path: z.string().describe('JSONPath expression over the tool arguments.'),
|
|
237
|
+
equals: z
|
|
238
|
+
.union([z.string(), z.number(), z.boolean()])
|
|
239
|
+
.optional()
|
|
240
|
+
.describe('Optional primitive value that must match.')
|
|
241
|
+
})
|
|
242
|
+
])
|
|
243
|
+
.describe('Assertions evaluated against calls to a named MCP tool.');
|
|
244
|
+
const AgentAssertionSchema = z
|
|
245
|
+
.object({
|
|
246
|
+
label: z.string().describe('Stable name for the semantic check.'),
|
|
247
|
+
prompt: z.string().describe('Question the judge should answer about the scenario result.')
|
|
248
|
+
})
|
|
249
|
+
.describe('Semantic checks for requirements that are not reliably deterministic.');
|
|
250
|
+
const AgentContextSchema = z
|
|
251
|
+
.object({
|
|
252
|
+
include_prompt: z
|
|
253
|
+
.boolean()
|
|
254
|
+
.optional()
|
|
255
|
+
.describe('Include the scenario prompt in judge context.'),
|
|
256
|
+
include_tool_sequence: z
|
|
257
|
+
.boolean()
|
|
258
|
+
.optional()
|
|
259
|
+
.describe('Include called raw MCP tool names in judge context.'),
|
|
260
|
+
include_tool_inputs: z
|
|
261
|
+
.boolean()
|
|
262
|
+
.optional()
|
|
263
|
+
.describe('Include raw MCP tool arguments in judge context.')
|
|
264
|
+
})
|
|
265
|
+
.describe('Shared context sent to every agent assertion.');
|
|
266
|
+
const EvalRulesSchema = z
|
|
267
|
+
.object({
|
|
268
|
+
tool_constraints: z
|
|
269
|
+
.object({
|
|
270
|
+
required_tools: z
|
|
271
|
+
.array(z.string())
|
|
272
|
+
.optional()
|
|
273
|
+
.describe('Raw MCP tools that must be called.'),
|
|
274
|
+
forbidden_tools: z
|
|
275
|
+
.array(z.string())
|
|
276
|
+
.optional()
|
|
277
|
+
.describe('Raw MCP tools that must not be called.')
|
|
278
|
+
})
|
|
279
|
+
.strict()
|
|
280
|
+
.describe('Deterministic MCP tool-usage constraints.')
|
|
281
|
+
.optional(),
|
|
282
|
+
tool_sequence: z
|
|
283
|
+
.array(z.string())
|
|
284
|
+
.optional()
|
|
285
|
+
.describe('Flat ordered list of raw MCP tool names that must be called.'),
|
|
286
|
+
tool_input_assertions: z
|
|
287
|
+
.array(ToolInputAssertionSchema)
|
|
288
|
+
.optional()
|
|
289
|
+
.describe('Checks over MCP tool arguments.'),
|
|
290
|
+
response_assertions: z
|
|
291
|
+
.array(ResponseAssertionSchema)
|
|
292
|
+
.optional()
|
|
293
|
+
.describe('Checks over the final response.'),
|
|
294
|
+
agent_assertions: z
|
|
295
|
+
.array(AgentAssertionSchema)
|
|
296
|
+
.optional()
|
|
297
|
+
.describe('Semantic checks judged by an evaluation agent.'),
|
|
298
|
+
agent_context: AgentContextSchema.optional()
|
|
299
|
+
})
|
|
300
|
+
.strict()
|
|
301
|
+
.describe('Full MCPLab evaluation rules. Use this object for all supported assertion types.');
|
|
151
302
|
const ScenarioEntrySchema = z.object({
|
|
152
303
|
id: z.string(),
|
|
304
|
+
name: z.string().optional(),
|
|
153
305
|
agent: z.string().optional(),
|
|
154
|
-
|
|
306
|
+
mcp_servers: z.array(z.object({ ref: z.string() })),
|
|
155
307
|
prompt: z.string(),
|
|
156
|
-
eval:
|
|
308
|
+
eval: EvalRulesSchema.optional(),
|
|
157
309
|
extract: z
|
|
158
310
|
.array(z.object({
|
|
159
311
|
name: z.string(),
|
|
@@ -180,14 +332,7 @@ const ResolvedScenarioSchema = z.object({
|
|
|
180
332
|
servers: z.array(z.string()),
|
|
181
333
|
agent: z.string().optional(),
|
|
182
334
|
prompt: z.string().optional(),
|
|
183
|
-
eval:
|
|
184
|
-
.object({
|
|
185
|
-
type: z.string(),
|
|
186
|
-
assertions: z.array(z.string()).optional(),
|
|
187
|
-
rubric: GenericObjectSchema.optional()
|
|
188
|
-
})
|
|
189
|
-
.passthrough()
|
|
190
|
-
.optional(),
|
|
335
|
+
eval: GenericObjectSchema.optional(),
|
|
191
336
|
extract: z
|
|
192
337
|
.array(z
|
|
193
338
|
.object({
|
|
@@ -532,6 +677,7 @@ export function createConfiguredServer() {
|
|
|
532
677
|
websiteUrl: 'https://mcplab.inspectr.dev',
|
|
533
678
|
icons: [{ src: SERVER_ICON_URL, mimeType: 'image/svg+xml' }]
|
|
534
679
|
});
|
|
680
|
+
registerSkills(server);
|
|
535
681
|
registerTools(server);
|
|
536
682
|
registerPrompts(server);
|
|
537
683
|
return server;
|
|
@@ -547,6 +693,79 @@ export function registerTools(server) {
|
|
|
547
693
|
annotations: inferToolAnnotations(name, resolvedTitle, config.annotations)
|
|
548
694
|
}, cb);
|
|
549
695
|
};
|
|
696
|
+
registerTool('mcplab_build_app_link', {
|
|
697
|
+
description: 'Build a safe deep link to a local MCPLab app view.',
|
|
698
|
+
inputSchema: {
|
|
699
|
+
view: z.enum([
|
|
700
|
+
'home',
|
|
701
|
+
'evaluations',
|
|
702
|
+
'run_evaluation',
|
|
703
|
+
'results',
|
|
704
|
+
'result_detail',
|
|
705
|
+
'compare',
|
|
706
|
+
'tool_analysis',
|
|
707
|
+
'tool_analysis_results',
|
|
708
|
+
'tool_analysis_result',
|
|
709
|
+
'library_servers',
|
|
710
|
+
'library_agents',
|
|
711
|
+
'library_test_cases',
|
|
712
|
+
'settings'
|
|
713
|
+
]),
|
|
714
|
+
run_id: z.string().min(1).max(200).optional(),
|
|
715
|
+
tool_analysis_id: z.string().min(1).max(200).optional(),
|
|
716
|
+
config_id: z.string().min(1).max(200).optional(),
|
|
717
|
+
agent: z.string().min(1).max(200).optional(),
|
|
718
|
+
scenario: z.string().min(1).max(200).optional(),
|
|
719
|
+
time_filter: z.enum(['last', 'custom']).optional(),
|
|
720
|
+
time_preset: z.enum(['15min', '30min', '1h', '24h', '7d', '14d', '30d']).optional(),
|
|
721
|
+
time_start: z.string().datetime().optional(),
|
|
722
|
+
time_end: z.string().datetime().optional()
|
|
723
|
+
},
|
|
724
|
+
outputSchema: { url: z.string().url(), view: z.string(), path: z.string() }
|
|
725
|
+
}, async ({ view, run_id, tool_analysis_id, config_id, agent, scenario, time_filter, time_preset, time_start, time_end }) => withToolHandling(() => {
|
|
726
|
+
const paths = {
|
|
727
|
+
home: '/',
|
|
728
|
+
evaluations: '/evaluations',
|
|
729
|
+
run_evaluation: '/run',
|
|
730
|
+
results: '/results',
|
|
731
|
+
result_detail: '/results/detail',
|
|
732
|
+
compare: '/compare',
|
|
733
|
+
tool_analysis: '/tool-analysis',
|
|
734
|
+
tool_analysis_results: '/tool-analysis/results',
|
|
735
|
+
tool_analysis_result: '/tool-analysis/result',
|
|
736
|
+
library_servers: '/servers',
|
|
737
|
+
library_agents: '/agents',
|
|
738
|
+
library_test_cases: '/test-cases',
|
|
739
|
+
settings: '/settings'
|
|
740
|
+
};
|
|
741
|
+
const path = paths[view];
|
|
742
|
+
if (!path)
|
|
743
|
+
throw new Error(`Unsupported app view: ${view}`);
|
|
744
|
+
const url = new URL(path, process.env.MCPLAB_APP_URL ?? 'http://127.0.0.1:8787');
|
|
745
|
+
if (run_id)
|
|
746
|
+
url.searchParams.set('run', run_id);
|
|
747
|
+
if (tool_analysis_id)
|
|
748
|
+
url.searchParams.set('tool_analysis_id', tool_analysis_id);
|
|
749
|
+
if (config_id)
|
|
750
|
+
url.searchParams.set('config', config_id);
|
|
751
|
+
if (agent)
|
|
752
|
+
url.searchParams.set('agent', agent);
|
|
753
|
+
if (scenario)
|
|
754
|
+
url.searchParams.set('scenario', scenario);
|
|
755
|
+
if (time_filter)
|
|
756
|
+
url.searchParams.set('time_filter', time_filter);
|
|
757
|
+
if (time_preset)
|
|
758
|
+
url.searchParams.set('time_preset', time_preset);
|
|
759
|
+
if (time_start)
|
|
760
|
+
url.searchParams.set('time_start', time_start);
|
|
761
|
+
if (time_end)
|
|
762
|
+
url.searchParams.set('time_end', time_end);
|
|
763
|
+
if (view === 'result_detail' && !run_id)
|
|
764
|
+
throw new Error('run_id is required for result_detail');
|
|
765
|
+
if (view === 'tool_analysis_result' && !tool_analysis_id)
|
|
766
|
+
throw new Error('tool_analysis_id is required for tool_analysis_result');
|
|
767
|
+
return ok(`Built app link for ${view}`, { url: url.toString(), view, path });
|
|
768
|
+
}));
|
|
550
769
|
registerTool('mcplab_write_markdown_report', {
|
|
551
770
|
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.',
|
|
552
771
|
outputSchema: WriteMarkdownReportOutputSchema,
|
|
@@ -688,7 +907,7 @@ export function registerTools(server) {
|
|
|
688
907
|
}
|
|
689
908
|
});
|
|
690
909
|
registerTool('mcplab_search_markdown_reports', {
|
|
691
|
-
description: '
|
|
910
|
+
description: 'Search saved Markdown report FILES under mcplab/reports. Use this to locate report documents by filename, path, run-id substring, or report text metadata; it does not search evaluation answers, traces, or tool-analysis records.',
|
|
692
911
|
outputSchema: {
|
|
693
912
|
reports_dir: z.string(),
|
|
694
913
|
run_id_filter: z.string().optional(),
|
|
@@ -819,7 +1038,7 @@ export function registerTools(server) {
|
|
|
819
1038
|
outputSchema: LibraryEntrySchema,
|
|
820
1039
|
inputSchema: {
|
|
821
1040
|
kind: z
|
|
822
|
-
.enum(['all', 'servers', 'agents', 'scenarios'])
|
|
1041
|
+
.enum(['all', 'servers', 'agents', 'test_cases', 'scenarios'])
|
|
823
1042
|
.optional()
|
|
824
1043
|
.describe('Which library category to list. Defaults to all.'),
|
|
825
1044
|
includeContent: z
|
|
@@ -838,6 +1057,7 @@ export function registerTools(server) {
|
|
|
838
1057
|
bundleRoot: data.bundleRoot,
|
|
839
1058
|
servers: selectedKind === 'servers' ? data.servers : [],
|
|
840
1059
|
agents: selectedKind === 'agents' ? data.agents : [],
|
|
1060
|
+
test_cases: selectedKind === 'test_cases' ? data.test_cases : [],
|
|
841
1061
|
scenarios: selectedKind === 'scenarios' ? data.scenarios : []
|
|
842
1062
|
};
|
|
843
1063
|
return ok(`Loaded MCPLab library from ${root}`, structured);
|
|
@@ -847,14 +1067,16 @@ export function registerTools(server) {
|
|
|
847
1067
|
description: 'Get a specific reusable server, agent, or scenario definition from a MCPLab library bundle and return both structured data and YAML.',
|
|
848
1068
|
outputSchema: {
|
|
849
1069
|
bundleRoot: z.string(),
|
|
850
|
-
kind: z.enum(['servers', 'agents', 'scenarios']),
|
|
1070
|
+
kind: z.enum(['servers', 'agents', 'test_cases', 'scenarios']),
|
|
851
1071
|
id: z.string(),
|
|
852
1072
|
file: z.string().optional(),
|
|
853
1073
|
yaml: z.string(),
|
|
854
1074
|
content: GenericObjectSchema
|
|
855
1075
|
},
|
|
856
1076
|
inputSchema: {
|
|
857
|
-
kind: z
|
|
1077
|
+
kind: z
|
|
1078
|
+
.enum(['servers', 'agents', 'test_cases', 'scenarios'])
|
|
1079
|
+
.describe('Library category.'),
|
|
858
1080
|
id: z.string().describe('Entry id (for scenarios this is scenario.id, not filename).')
|
|
859
1081
|
}
|
|
860
1082
|
}, async ({ kind, id }) => {
|
|
@@ -910,8 +1132,246 @@ export function registerTools(server) {
|
|
|
910
1132
|
});
|
|
911
1133
|
});
|
|
912
1134
|
});
|
|
1135
|
+
registerTool('mcplab_create_test_case', {
|
|
1136
|
+
description: 'Create one reviewed MCPLab Test Case in mcplab/test-cases/. Put evaluation checks in the eval object for full support of response, tool-input, and agent assertions. Simple shorthand fields are supported for basic cases; do not provide a shorthand field together with its equivalent eval field. This is a scoped YAML write and refuses duplicate IDs.',
|
|
1137
|
+
outputSchema: { id: z.string(), path: z.string(), test_case: ScenarioEntrySchema },
|
|
1138
|
+
inputSchema: {
|
|
1139
|
+
id: z
|
|
1140
|
+
.string()
|
|
1141
|
+
.min(1)
|
|
1142
|
+
.describe('Stable Test Case id; letters, numbers, hyphens, and underscores.'),
|
|
1143
|
+
name: z
|
|
1144
|
+
.string()
|
|
1145
|
+
.optional()
|
|
1146
|
+
.describe('Human-readable Test Case label; defaults to id when blank.'),
|
|
1147
|
+
servers: z.array(z.string()).min(1).describe('MCP server ids available to this Test Case.'),
|
|
1148
|
+
prompt: z
|
|
1149
|
+
.string()
|
|
1150
|
+
.min(1)
|
|
1151
|
+
.max(4000)
|
|
1152
|
+
.describe('Task prompt executed by the evaluation agent.'),
|
|
1153
|
+
required_tools: z
|
|
1154
|
+
.array(z.string())
|
|
1155
|
+
.optional()
|
|
1156
|
+
.describe('Simple shorthand for eval.tool_constraints.required_tools. Use for basic checks only; put new or advanced rules under eval. Do not provide both.'),
|
|
1157
|
+
forbidden_tools: z
|
|
1158
|
+
.array(z.string())
|
|
1159
|
+
.optional()
|
|
1160
|
+
.describe('Simple shorthand for eval.tool_constraints.forbidden_tools. Use for basic checks only; put new or advanced rules under eval. Do not provide both.'),
|
|
1161
|
+
tool_sequence: z
|
|
1162
|
+
.array(z.string())
|
|
1163
|
+
.min(1)
|
|
1164
|
+
.optional()
|
|
1165
|
+
.describe('Simple shorthand for eval.tool_sequence: a flat ordered list of raw MCP tool names. Use eval for new or advanced rules. Do not provide both.'),
|
|
1166
|
+
extract_rules: z
|
|
1167
|
+
.array(z.object({
|
|
1168
|
+
name: z.string().describe('Extracted field name.'),
|
|
1169
|
+
regex: z.string().describe('Regex applied to final_text.')
|
|
1170
|
+
}))
|
|
1171
|
+
.optional()
|
|
1172
|
+
.describe('Extract values from the final response for trend analysis.'),
|
|
1173
|
+
response_regex_patterns: z
|
|
1174
|
+
.array(z.string())
|
|
1175
|
+
.optional()
|
|
1176
|
+
.describe('Simple shorthand for response regex checks. Use eval.response_assertions for new or advanced checks; choose type=contains for literal text. Do not provide both.'),
|
|
1177
|
+
eval: EvalRulesSchema.optional().describe('Full evaluation-rule object. Use this for new authoring or whenever you need more than one rule type; do not combine it with equivalent shorthand fields.')
|
|
1178
|
+
}
|
|
1179
|
+
}, async ({ id, name, servers, prompt, required_tools, forbidden_tools, tool_sequence, extract_rules, response_regex_patterns, eval: evalRules }) => withToolHandling(async () => {
|
|
1180
|
+
const bundleRoot = resolveBundleRoot();
|
|
1181
|
+
const library = readLibrary(bundleRoot, false);
|
|
1182
|
+
const created = createTestCaseFile({
|
|
1183
|
+
librariesDir: bundleRoot,
|
|
1184
|
+
knownServerIds: library.servers.map((item) => item.id),
|
|
1185
|
+
testCase: {
|
|
1186
|
+
id,
|
|
1187
|
+
name,
|
|
1188
|
+
servers,
|
|
1189
|
+
prompt,
|
|
1190
|
+
requiredTools: required_tools,
|
|
1191
|
+
forbiddenTools: forbidden_tools,
|
|
1192
|
+
toolSequence: tool_sequence,
|
|
1193
|
+
extractRules: extract_rules,
|
|
1194
|
+
responseRegexPatterns: response_regex_patterns,
|
|
1195
|
+
eval: evalRules
|
|
1196
|
+
}
|
|
1197
|
+
});
|
|
1198
|
+
return ok(`Created Test Case '${created.id}'`, {
|
|
1199
|
+
id: created.id,
|
|
1200
|
+
path: created.path,
|
|
1201
|
+
test_case: created.testCase
|
|
1202
|
+
});
|
|
1203
|
+
}));
|
|
1204
|
+
registerTool('mcplab_get_test_case', {
|
|
1205
|
+
description: 'Read one existing MCPLab test case, including its full parsed YAML content.',
|
|
1206
|
+
inputSchema: { file_path: z.string().describe('Path relative to mcplab/test-cases/.') },
|
|
1207
|
+
outputSchema: { path: z.string(), test_case: ScenarioEntrySchema }
|
|
1208
|
+
}, async ({ file_path }) => withToolHandling(() => {
|
|
1209
|
+
const result = readTestCaseFile({ librariesDir: resolveBundleRoot(), filePath: file_path });
|
|
1210
|
+
return ok(`Read Test Case '${result.id}'`, {
|
|
1211
|
+
path: result.path,
|
|
1212
|
+
test_case: result.testCase
|
|
1213
|
+
});
|
|
1214
|
+
}));
|
|
1215
|
+
registerTool('mcplab_update_test_case', {
|
|
1216
|
+
description: 'Replace one existing MCPLab test case in place. Put evaluation checks in the eval object for full support of response, tool-input, and agent assertions. Simple shorthand fields are supported for basic cases; do not provide a shorthand field together with its equivalent eval field. Requires confirm=true and never creates a new file.',
|
|
1217
|
+
inputSchema: {
|
|
1218
|
+
file_path: z
|
|
1219
|
+
.string()
|
|
1220
|
+
.describe('Path to an existing test case relative to mcplab/test-cases/.'),
|
|
1221
|
+
confirm: z
|
|
1222
|
+
.literal(true)
|
|
1223
|
+
.describe('Required confirmation that the existing file should be overwritten.'),
|
|
1224
|
+
id: z
|
|
1225
|
+
.string()
|
|
1226
|
+
.min(1)
|
|
1227
|
+
.describe('Stable Test Case id; letters, numbers, hyphens, and underscores.'),
|
|
1228
|
+
name: z
|
|
1229
|
+
.string()
|
|
1230
|
+
.optional()
|
|
1231
|
+
.describe('Human-readable Test Case label; defaults to id when blank.'),
|
|
1232
|
+
servers: z.array(z.string()).min(1).describe('MCP server ids available to this Test Case.'),
|
|
1233
|
+
prompt: z
|
|
1234
|
+
.string()
|
|
1235
|
+
.min(1)
|
|
1236
|
+
.max(4000)
|
|
1237
|
+
.describe('Task prompt executed by the evaluation agent.'),
|
|
1238
|
+
required_tools: z
|
|
1239
|
+
.array(z.string())
|
|
1240
|
+
.optional()
|
|
1241
|
+
.describe('Simple shorthand for eval.tool_constraints.required_tools. Use for basic checks only; put new or advanced rules under eval. Do not provide both.'),
|
|
1242
|
+
forbidden_tools: z
|
|
1243
|
+
.array(z.string())
|
|
1244
|
+
.optional()
|
|
1245
|
+
.describe('Simple shorthand for eval.tool_constraints.forbidden_tools. Use for basic checks only; put new or advanced rules under eval. Do not provide both.'),
|
|
1246
|
+
tool_sequence: z
|
|
1247
|
+
.array(z.string())
|
|
1248
|
+
.min(1)
|
|
1249
|
+
.optional()
|
|
1250
|
+
.describe('Simple shorthand for eval.tool_sequence: a flat ordered list of raw MCP tool names. Use eval for new or advanced rules. Do not provide both.'),
|
|
1251
|
+
extract_rules: z
|
|
1252
|
+
.array(z.object({
|
|
1253
|
+
name: z.string().describe('Extracted field name.'),
|
|
1254
|
+
regex: z.string().describe('Regex applied to final_text.')
|
|
1255
|
+
}))
|
|
1256
|
+
.optional()
|
|
1257
|
+
.describe('Extract values from the final response for trend analysis.'),
|
|
1258
|
+
response_regex_patterns: z
|
|
1259
|
+
.array(z.string())
|
|
1260
|
+
.optional()
|
|
1261
|
+
.describe('Simple shorthand for response regex checks. Use eval.response_assertions for new or advanced checks; choose type=contains for literal text. Do not provide both.'),
|
|
1262
|
+
eval: EvalRulesSchema.optional().describe('Full evaluation-rule object. Use this for new authoring or whenever you need more than one rule type; do not combine it with equivalent shorthand fields.')
|
|
1263
|
+
},
|
|
1264
|
+
outputSchema: { id: z.string(), path: z.string(), test_case: ScenarioEntrySchema }
|
|
1265
|
+
}, async ({ file_path, confirm: _confirm, id, name, servers, prompt, required_tools, forbidden_tools, tool_sequence, extract_rules, response_regex_patterns, eval: evalRules }) => withToolHandling(() => {
|
|
1266
|
+
const library = readLibrary(resolveBundleRoot(), false);
|
|
1267
|
+
const updated = updateTestCaseFile({
|
|
1268
|
+
librariesDir: resolveBundleRoot(),
|
|
1269
|
+
knownServerIds: library.servers.map((item) => item.id),
|
|
1270
|
+
filePath: file_path,
|
|
1271
|
+
testCase: {
|
|
1272
|
+
id,
|
|
1273
|
+
name,
|
|
1274
|
+
servers,
|
|
1275
|
+
prompt,
|
|
1276
|
+
requiredTools: required_tools,
|
|
1277
|
+
forbiddenTools: forbidden_tools,
|
|
1278
|
+
toolSequence: tool_sequence,
|
|
1279
|
+
extractRules: extract_rules,
|
|
1280
|
+
responseRegexPatterns: response_regex_patterns,
|
|
1281
|
+
eval: evalRules
|
|
1282
|
+
}
|
|
1283
|
+
});
|
|
1284
|
+
return ok(`Updated Test Case '${updated.id}'`, {
|
|
1285
|
+
id: updated.id,
|
|
1286
|
+
path: updated.path,
|
|
1287
|
+
test_case: updated.testCase
|
|
1288
|
+
});
|
|
1289
|
+
}));
|
|
1290
|
+
registerTool('mcplab_create_evaluation_config', {
|
|
1291
|
+
description: 'Create one reviewed MCPLab evaluation configuration in mcplab/evals/. This is a scoped YAML write with automatic filename normalization and collision handling. In scenario eval blocks, use the full evaluation rules for response, tool-input, and agent checks: tool_constraints, flat tool_sequence, response_assertions (contains, equals, regex, and JSONPath variants), tool_input_assertions, agent_assertions, and agent_context. Use contains for literal text; use regex only for patterns. Do not combine eval rules with equivalent shorthand fields.',
|
|
1292
|
+
outputSchema: {
|
|
1293
|
+
file_name: z.string(),
|
|
1294
|
+
path: z.string(),
|
|
1295
|
+
relative_path: z.string(),
|
|
1296
|
+
config: GenericObjectSchema
|
|
1297
|
+
},
|
|
1298
|
+
inputSchema: {
|
|
1299
|
+
file_name: z
|
|
1300
|
+
.string()
|
|
1301
|
+
.min(1)
|
|
1302
|
+
.describe('Destination YAML filename under mcplab/evals/. Extension is added if omitted.'),
|
|
1303
|
+
config: GenericObjectSchema.describe('Complete MCPLab evaluation configuration. Scenario eval blocks support tool_constraints, a flat tool_sequence, response_assertions (contains, not_contains, starts_with, ends_with, equals, regex, and JSONPath variants), tool_input_assertions, agent_assertions, and agent_context. Use contains for literal response text; use regex only when pattern matching is required.')
|
|
1304
|
+
}
|
|
1305
|
+
}, async ({ file_name, config }) => withToolHandling(async () => {
|
|
1306
|
+
const created = createEvaluationConfigFile({
|
|
1307
|
+
evalsDir: join(resolveBundleRoot(), 'evals'),
|
|
1308
|
+
fileName: file_name,
|
|
1309
|
+
config: config
|
|
1310
|
+
});
|
|
1311
|
+
return ok(`Created evaluation configuration '${created.fileName}'`, {
|
|
1312
|
+
file_name: created.fileName,
|
|
1313
|
+
path: created.path,
|
|
1314
|
+
relative_path: created.relativePath,
|
|
1315
|
+
config: created.config
|
|
1316
|
+
});
|
|
1317
|
+
}));
|
|
1318
|
+
registerTool('mcplab_get_evaluation_config', {
|
|
1319
|
+
description: 'Read one existing MCPLab evaluation config, including its full parsed YAML content. Returned scenario eval blocks may use full tool constraints, flat tool sequences, response/tool-input assertions, agent assertions, and agent context.',
|
|
1320
|
+
inputSchema: {
|
|
1321
|
+
config_path: z
|
|
1322
|
+
.string()
|
|
1323
|
+
.describe('Path to an existing eval YAML, relative to the MCPLab evals directory. Invalid, missing, or out-of-root paths return an error; the tool never silently coerces them.')
|
|
1324
|
+
},
|
|
1325
|
+
outputSchema: z.object({
|
|
1326
|
+
path: z.string(),
|
|
1327
|
+
relative_path: z.string(),
|
|
1328
|
+
config: GenericObjectSchema
|
|
1329
|
+
})
|
|
1330
|
+
}, async ({ config_path }) => withToolHandling(() => {
|
|
1331
|
+
const result = readEvaluationConfigFile({
|
|
1332
|
+
evalsDir: join(resolveBundleRoot(), 'evals'),
|
|
1333
|
+
filePath: config_path
|
|
1334
|
+
});
|
|
1335
|
+
return ok(`Read evaluation config '${result.relativePath}'`, {
|
|
1336
|
+
path: result.path,
|
|
1337
|
+
relative_path: result.relativePath,
|
|
1338
|
+
config: result.config
|
|
1339
|
+
});
|
|
1340
|
+
}));
|
|
1341
|
+
registerTool('mcplab_update_evaluation_config', {
|
|
1342
|
+
description: 'Replace one existing MCPLab evaluation config in place after normalization. Requires confirm=true; refuses missing or out-of-root paths and never creates a new file. In scenario eval blocks, use the full evaluation rules for response, tool-input, and agent checks: tool_constraints, flat tool_sequence, response_assertions (contains, equals, regex, and JSONPath variants), tool_input_assertions, agent_assertions, and agent_context. Use contains for literal text; use regex only for patterns. Do not combine eval rules with equivalent shorthand fields.',
|
|
1343
|
+
inputSchema: {
|
|
1344
|
+
config_path: z
|
|
1345
|
+
.string()
|
|
1346
|
+
.describe('Path to an existing eval YAML, relative to the MCPLab evals directory. Invalid, missing, or out-of-root paths return an error; the tool never silently coerces them.'),
|
|
1347
|
+
config: GenericObjectSchema.describe('Complete replacement evaluation configuration. Scenario eval blocks support tool_constraints, a flat tool_sequence, response_assertions (contains, not_contains, starts_with, ends_with, equals, regex, and JSONPath variants), tool_input_assertions, agent_assertions, and agent_context. Use contains for literal response text; use regex only when pattern matching is required.'),
|
|
1348
|
+
confirm: z
|
|
1349
|
+
.literal(true)
|
|
1350
|
+
.describe('Required confirmation that the existing file should be overwritten.')
|
|
1351
|
+
},
|
|
1352
|
+
outputSchema: {
|
|
1353
|
+
file_name: z.string(),
|
|
1354
|
+
path: z.string(),
|
|
1355
|
+
relative_path: z.string(),
|
|
1356
|
+
config: GenericObjectSchema,
|
|
1357
|
+
warnings: z.array(z.string())
|
|
1358
|
+
}
|
|
1359
|
+
}, async ({ config_path, config }) => withToolHandling(() => {
|
|
1360
|
+
const updated = updateEvaluationConfigFile({
|
|
1361
|
+
evalsDir: join(resolveBundleRoot(), 'evals'),
|
|
1362
|
+
filePath: config_path,
|
|
1363
|
+
config: config
|
|
1364
|
+
});
|
|
1365
|
+
return ok(`Updated evaluation config '${updated.relativePath}'`, {
|
|
1366
|
+
file_name: updated.fileName,
|
|
1367
|
+
path: updated.path,
|
|
1368
|
+
relative_path: updated.relativePath,
|
|
1369
|
+
config: updated.config,
|
|
1370
|
+
warnings: updated.warnings
|
|
1371
|
+
});
|
|
1372
|
+
}));
|
|
913
1373
|
registerTool('mcplab_generate_scenario_entry', {
|
|
914
|
-
description: 'Generate a MCPLab scenario YAML snippet with prompt, server links, and optional evaluation/extract rules.
|
|
1374
|
+
description: 'Generate a MCPLab scenario YAML snippet with prompt, server links, and optional evaluation/extract rules. Put checks in the eval object for full assertion support; simple shorthand fields are supported for basic cases and must not be combined with equivalent eval fields.',
|
|
915
1375
|
outputSchema: {
|
|
916
1376
|
scenario: ScenarioEntrySchema,
|
|
917
1377
|
yaml: z.string(),
|
|
@@ -928,7 +1388,7 @@ export function registerTools(server) {
|
|
|
928
1388
|
name: z
|
|
929
1389
|
.string()
|
|
930
1390
|
.optional()
|
|
931
|
-
.describe('Optional human label used
|
|
1391
|
+
.describe('Optional human-readable scenario label. Also used to derive id when omitted.'),
|
|
932
1392
|
agent: z
|
|
933
1393
|
.string()
|
|
934
1394
|
.optional()
|
|
@@ -936,22 +1396,30 @@ export function registerTools(server) {
|
|
|
936
1396
|
servers: z
|
|
937
1397
|
.array(z.string())
|
|
938
1398
|
.min(1)
|
|
939
|
-
.describe('One or more server ids
|
|
1399
|
+
.describe('One or more server ids; emitted as scenario-owned mcp_servers refs.'),
|
|
940
1400
|
prompt: z
|
|
941
1401
|
.string()
|
|
942
1402
|
.min(1)
|
|
943
1403
|
.max(4000)
|
|
944
1404
|
.describe('The task prompt the evaluation agent should execute (1-4000 chars).'),
|
|
945
|
-
required_tools: z
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
.
|
|
1405
|
+
required_tools: z
|
|
1406
|
+
.array(z.string())
|
|
1407
|
+
.optional()
|
|
1408
|
+
.describe('Simple shorthand for eval.tool_constraints.required_tools. Use for basic checks only; put new or advanced rules under eval. Do not provide both.'),
|
|
1409
|
+
forbidden_tools: z
|
|
1410
|
+
.array(z.string())
|
|
949
1411
|
.optional()
|
|
950
|
-
.describe('
|
|
1412
|
+
.describe('Simple shorthand for eval.tool_constraints.forbidden_tools. Use for basic checks only; put new or advanced rules under eval. Do not provide both.'),
|
|
1413
|
+
tool_sequence: z
|
|
1414
|
+
.array(z.string())
|
|
1415
|
+
.min(1)
|
|
1416
|
+
.optional()
|
|
1417
|
+
.describe('Simple shorthand for eval.tool_sequence: a flat ordered list of raw MCP tool names. Use eval for new or advanced rules. Do not provide both.'),
|
|
951
1418
|
response_regex_patterns: z
|
|
952
1419
|
.array(z.string())
|
|
953
1420
|
.optional()
|
|
954
|
-
.describe('
|
|
1421
|
+
.describe('Simple shorthand for response regex checks. Use eval.response_assertions for new or advanced checks; choose type=contains for literal text. Do not provide both.'),
|
|
1422
|
+
eval: EvalRulesSchema.optional().describe('Full evaluation-rule object. Use this for new authoring or whenever you need more than one rule type; do not combine it with equivalent shorthand fields.'),
|
|
955
1423
|
extract_rules: z
|
|
956
1424
|
.array(z.object({
|
|
957
1425
|
name: z.string().describe('Extracted field name.'),
|
|
@@ -981,6 +1449,16 @@ export function registerTools(server) {
|
|
|
981
1449
|
});
|
|
982
1450
|
});
|
|
983
1451
|
});
|
|
1452
|
+
registerTool('mcplab_list_evaluation_configs', {
|
|
1453
|
+
description: 'List MCPLab evaluation configurations with nested-suite filtering and deterministic sorting.',
|
|
1454
|
+
inputSchema: {
|
|
1455
|
+
suite: z.string().optional(),
|
|
1456
|
+
query: z.string().optional(),
|
|
1457
|
+
sort_by: z.enum(['name', 'scenarios', 'agents', 'updated_at']).optional(),
|
|
1458
|
+
sort_direction: z.enum(['asc', 'desc']).optional()
|
|
1459
|
+
},
|
|
1460
|
+
outputSchema: EvaluationConfigListSchema
|
|
1461
|
+
}, async ({ suite, query, sort_by, sort_direction }) => withToolHandling(() => ok('Listed evaluation configurations', listEvaluationConfigs({ suite, query, sortBy: sort_by, sortDirection: sort_direction }))));
|
|
984
1462
|
registerTool('mcplab_validate_config', {
|
|
985
1463
|
description: 'Validate and expand a MCPLab config file via mcplab-core loadConfig(), including server/agent/scenario library references.',
|
|
986
1464
|
outputSchema: {
|
|
@@ -1021,7 +1499,7 @@ export function registerTools(server) {
|
|
|
1021
1499
|
});
|
|
1022
1500
|
});
|
|
1023
1501
|
registerTool('mcplab_run_eval', {
|
|
1024
|
-
description: '
|
|
1502
|
+
description: 'Execute a MCPLab evaluation immediately in this MCP server process using mcplab-core runAll(). Use this when the caller needs results now; use mcplab_queue_run to hand the run to the APP queue instead.',
|
|
1025
1503
|
outputSchema: {
|
|
1026
1504
|
run_dir: z.string(),
|
|
1027
1505
|
total_scenarios: z.number().int().nonnegative(),
|
|
@@ -1117,6 +1595,133 @@ export function registerTools(server) {
|
|
|
1117
1595
|
});
|
|
1118
1596
|
});
|
|
1119
1597
|
});
|
|
1598
|
+
registerTool('mcplab_list_runs', {
|
|
1599
|
+
description: 'List MCPLab evaluation runs with optional inclusive ISO time bounds.',
|
|
1600
|
+
inputSchema: {
|
|
1601
|
+
since: z.string().datetime().optional(),
|
|
1602
|
+
until: z.string().datetime().optional(),
|
|
1603
|
+
limit: z.number().int().positive().max(200).optional()
|
|
1604
|
+
},
|
|
1605
|
+
outputSchema: {
|
|
1606
|
+
since: z.string().optional(),
|
|
1607
|
+
until: z.string().optional(),
|
|
1608
|
+
total_matching: z.number(),
|
|
1609
|
+
runs: z.array(z.object({
|
|
1610
|
+
run_id: z.string(),
|
|
1611
|
+
timestamp: z.string().optional(),
|
|
1612
|
+
config_hash: z.string().optional(),
|
|
1613
|
+
summary: z.any().optional()
|
|
1614
|
+
}))
|
|
1615
|
+
}
|
|
1616
|
+
}, async ({ since, until, limit }) => withToolHandling(() => {
|
|
1617
|
+
const sinceMs = since ? new Date(since).getTime() : undefined;
|
|
1618
|
+
const untilMs = until ? new Date(until).getTime() : undefined;
|
|
1619
|
+
if (sinceMs !== undefined && untilMs !== undefined && sinceMs > untilMs)
|
|
1620
|
+
throw new Error('since must be earlier than or equal to until');
|
|
1621
|
+
const matching = listRunsWithFallback(resolveRunsDir(), undefined, true).filter((entry) => {
|
|
1622
|
+
const timestamp = String(entry.metadata && typeof entry.metadata === 'object'
|
|
1623
|
+
? entry.metadata.timestamp ?? ''
|
|
1624
|
+
: '');
|
|
1625
|
+
const time = new Date(timestamp).getTime();
|
|
1626
|
+
if (sinceMs === undefined && untilMs === undefined)
|
|
1627
|
+
return true;
|
|
1628
|
+
return (!Number.isNaN(time) &&
|
|
1629
|
+
(sinceMs === undefined || time >= sinceMs) &&
|
|
1630
|
+
(untilMs === undefined || time <= untilMs));
|
|
1631
|
+
});
|
|
1632
|
+
const runs = matching.slice(0, limit ?? 50).map((entry) => ({
|
|
1633
|
+
run_id: String(entry.run_id),
|
|
1634
|
+
timestamp: typeof entry.metadata?.timestamp === 'string'
|
|
1635
|
+
? entry.metadata.timestamp
|
|
1636
|
+
: undefined,
|
|
1637
|
+
config_hash: typeof entry.metadata?.config_hash === 'string'
|
|
1638
|
+
? entry.metadata.config_hash
|
|
1639
|
+
: undefined,
|
|
1640
|
+
summary: entry.summary
|
|
1641
|
+
}));
|
|
1642
|
+
return ok(`Found ${matching.length} run(s)`, {
|
|
1643
|
+
since,
|
|
1644
|
+
until,
|
|
1645
|
+
total_matching: matching.length,
|
|
1646
|
+
runs
|
|
1647
|
+
});
|
|
1648
|
+
}));
|
|
1649
|
+
registerTool('mcplab_queue_run', {
|
|
1650
|
+
description: 'Add a MCPLab evaluation to the local APP queue for the APP worker to execute later; this tool does not execute the evaluation or return results. Use mcplab_run_eval when results are needed immediately. Returns the queued job id, position, and APP /run URL.',
|
|
1651
|
+
inputSchema: {
|
|
1652
|
+
config_path: z
|
|
1653
|
+
.string()
|
|
1654
|
+
.describe('Path to the eval YAML. Relative paths resolve under the APP evals directory; absolute paths must remain inside that directory. Invalid, missing, or disallowed paths return an error—the tool never silently coerces them.'),
|
|
1655
|
+
scenario_id: z.string().optional().describe('Optional single scenario id to run.'),
|
|
1656
|
+
scenario_ids: z
|
|
1657
|
+
.array(z.string())
|
|
1658
|
+
.min(1)
|
|
1659
|
+
.optional()
|
|
1660
|
+
.describe('Optional scenario ids to run.'),
|
|
1661
|
+
agents: z.array(z.string()).min(1).optional().describe('Optional agent ids to use.'),
|
|
1662
|
+
runs_per_scenario: z
|
|
1663
|
+
.number()
|
|
1664
|
+
.int()
|
|
1665
|
+
.positive()
|
|
1666
|
+
.optional()
|
|
1667
|
+
.describe('Runs per scenario (default 1).'),
|
|
1668
|
+
run_note: z
|
|
1669
|
+
.string()
|
|
1670
|
+
.max(500)
|
|
1671
|
+
.optional()
|
|
1672
|
+
.describe('Optional note attached to the queued run.'),
|
|
1673
|
+
server_override_all: z
|
|
1674
|
+
.array(z.string())
|
|
1675
|
+
.min(1)
|
|
1676
|
+
.optional()
|
|
1677
|
+
.describe('Replace MCP servers for every selected scenario for this queued run.'),
|
|
1678
|
+
scenario_server_overrides: z
|
|
1679
|
+
.record(z.array(z.string()))
|
|
1680
|
+
.optional()
|
|
1681
|
+
.describe('Map scenario ids to replacement MCP server ids for this queued run.')
|
|
1682
|
+
},
|
|
1683
|
+
outputSchema: z.object({
|
|
1684
|
+
jobId: z.string().describe('APP queue job identifier.'),
|
|
1685
|
+
queued: z.boolean().optional().describe('Whether the APP accepted the job into its queue.'),
|
|
1686
|
+
position: z
|
|
1687
|
+
.number()
|
|
1688
|
+
.int()
|
|
1689
|
+
.nonnegative()
|
|
1690
|
+
.optional()
|
|
1691
|
+
.describe('Current position in the APP queue.'),
|
|
1692
|
+
queue_url: z.string().url().describe('URL of the APP run queue page.')
|
|
1693
|
+
})
|
|
1694
|
+
}, async (input) => withToolHandling(async () => {
|
|
1695
|
+
const appBaseUrl = process.env.MCPLAB_APP_URL ?? 'http://127.0.0.1:8787';
|
|
1696
|
+
const response = await fetch(new URL('/api/runs', appBaseUrl), {
|
|
1697
|
+
method: 'POST',
|
|
1698
|
+
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
1699
|
+
body: JSON.stringify({
|
|
1700
|
+
configPath: input.config_path,
|
|
1701
|
+
runsPerScenario: input.runs_per_scenario ?? 1,
|
|
1702
|
+
...(input.scenario_id ? { scenarioId: input.scenario_id } : {}),
|
|
1703
|
+
...(input.scenario_ids ? { scenarioIds: input.scenario_ids } : {}),
|
|
1704
|
+
...(input.agents ? { agents: input.agents } : {}),
|
|
1705
|
+
...(input.run_note ? { runNote: input.run_note } : {}),
|
|
1706
|
+
...(input.server_override_all ? { serverOverrideAll: input.server_override_all } : {}),
|
|
1707
|
+
...(input.scenario_server_overrides
|
|
1708
|
+
? { scenarioServerOverrides: input.scenario_server_overrides }
|
|
1709
|
+
: {})
|
|
1710
|
+
})
|
|
1711
|
+
});
|
|
1712
|
+
const payload = (await response.json().catch(() => ({})));
|
|
1713
|
+
if (!response.ok) {
|
|
1714
|
+
throw new Error(typeof payload.error === 'string'
|
|
1715
|
+
? payload.error
|
|
1716
|
+
: `APP queue request failed with HTTP ${response.status}`);
|
|
1717
|
+
}
|
|
1718
|
+
if (typeof payload.jobId !== 'string')
|
|
1719
|
+
throw new Error('APP queue response did not include a jobId');
|
|
1720
|
+
return ok(`Queued MCPLab run ${payload.jobId}`, {
|
|
1721
|
+
...payload,
|
|
1722
|
+
queue_url: new URL('/run', appBaseUrl).toString()
|
|
1723
|
+
});
|
|
1724
|
+
}));
|
|
1120
1725
|
registerTool('mcplab_aggregate_runs', {
|
|
1121
1726
|
description: 'Aggregate metrics across historical MCPLab runs with compact summary-first output.',
|
|
1122
1727
|
outputSchema: z.object({
|
|
@@ -1262,7 +1867,7 @@ export function registerTools(server) {
|
|
|
1262
1867
|
});
|
|
1263
1868
|
});
|
|
1264
1869
|
registerTool('mcplab_search_tool_analysis_results', {
|
|
1265
|
-
description: 'Search saved MCP
|
|
1870
|
+
description: 'Search saved MCP TOOL-ANALYSIS records under mcplab/results/tool-analysis. Use this for aggregate tool usage, latency, and tool-analysis summaries; do not use it for final answers, raw execution traces, or Markdown report files.',
|
|
1266
1871
|
outputSchema: {
|
|
1267
1872
|
tool_analysis_results_dir: z.string(),
|
|
1268
1873
|
total: z.number().int().nonnegative(),
|
|
@@ -1525,7 +2130,7 @@ export function registerTools(server) {
|
|
|
1525
2130
|
});
|
|
1526
2131
|
});
|
|
1527
2132
|
registerTool('mcplab_trace_get_conversation', {
|
|
1528
|
-
description: 'Return
|
|
2133
|
+
description: 'Return one structured conversation timeline (messages and tool blocks) for a specific scenario and agent in a run. If you do not know the scenario_id and agent, call mcplab_trace_list_conversations first.',
|
|
1529
2134
|
outputSchema: {
|
|
1530
2135
|
run_id: z.string(),
|
|
1531
2136
|
scenario_id: z.string(),
|
|
@@ -1557,6 +2162,10 @@ export function registerTools(server) {
|
|
|
1557
2162
|
const { runId, records, legacyDetected } = readScenarioRunTraceRecordsForRun(run_id);
|
|
1558
2163
|
const textMax = max_text_chars ?? 4000;
|
|
1559
2164
|
const record = records.find((r) => r.scenario_id === scenario_id && r.agent === agent);
|
|
2165
|
+
if (!record) {
|
|
2166
|
+
const available = Array.from(new Set(records.map((r) => `${r.scenario_id} / ${r.agent}`)));
|
|
2167
|
+
throw new Error(`No conversation found for ${scenario_id} / ${agent}. Available conversations: ${available.join(', ') || 'none'}`);
|
|
2168
|
+
}
|
|
1560
2169
|
const timeline = record
|
|
1561
2170
|
? buildConversationTimeline(record, textMax).slice(0, max_items ?? 300)
|
|
1562
2171
|
: [];
|
|
@@ -1569,8 +2178,35 @@ export function registerTools(server) {
|
|
|
1569
2178
|
});
|
|
1570
2179
|
});
|
|
1571
2180
|
});
|
|
2181
|
+
registerTool('mcplab_trace_list_conversations', {
|
|
2182
|
+
description: 'List the scenario and agent conversation targets available in one run. Use this before mcplab_trace_get_conversation when you need to discover the required scenario_id and agent values.',
|
|
2183
|
+
outputSchema: {
|
|
2184
|
+
run_id: z.string(),
|
|
2185
|
+
legacy_trace_detected: z.boolean().optional(),
|
|
2186
|
+
conversations: z.array(z.object({
|
|
2187
|
+
scenario_id: z.string(),
|
|
2188
|
+
agent: z.string(),
|
|
2189
|
+
timeline_item_count: z.number().int().nonnegative()
|
|
2190
|
+
}))
|
|
2191
|
+
},
|
|
2192
|
+
inputSchema: {
|
|
2193
|
+
run_id: z.string().describe("Run id directory name or 'LATEST'.")
|
|
2194
|
+
}
|
|
2195
|
+
}, async ({ run_id }) => withToolHandling(() => {
|
|
2196
|
+
const { runId, records, legacyDetected } = readScenarioRunTraceRecordsForRun(run_id);
|
|
2197
|
+
const conversations = records.map((record) => ({
|
|
2198
|
+
scenario_id: record.scenario_id,
|
|
2199
|
+
agent: record.agent,
|
|
2200
|
+
timeline_item_count: buildConversationTimeline(record, 4000).length
|
|
2201
|
+
}));
|
|
2202
|
+
return ok(`Found ${conversations.length} conversation(s) in run ${runId}`, {
|
|
2203
|
+
run_id: runId,
|
|
2204
|
+
legacy_trace_detected: legacyDetected || undefined,
|
|
2205
|
+
conversations
|
|
2206
|
+
});
|
|
2207
|
+
}));
|
|
1572
2208
|
registerTool('mcplab_trace_search', {
|
|
1573
|
-
description: 'Search
|
|
2209
|
+
description: 'Search RAW SCENARIO-RUN TRACE events and message/block content for a text query. Use this for execution-level evidence such as tool calls, arguments, ordering, or intermediate messages; use mcplab_results_search for indexed result/answer content instead.',
|
|
1574
2210
|
outputSchema: {
|
|
1575
2211
|
run_id: z.string(),
|
|
1576
2212
|
query: z.string().trim().min(1),
|
|
@@ -1821,7 +2457,7 @@ export function registerTools(server) {
|
|
|
1821
2457
|
});
|
|
1822
2458
|
});
|
|
1823
2459
|
registerTool('mcplab_results_search', {
|
|
1824
|
-
description: 'Search MCPLab
|
|
2460
|
+
description: 'Search INDEXED MCPLab evaluation RESULTS and final-answer content in compact LLM-first format. Use this for finding passed/failed scenarios, answer text, and result metadata; use mcplab_trace_search for raw execution events, mcplab_search_tool_analysis_results for aggregate tool-analysis records, and mcplab_search_markdown_reports for report files.',
|
|
1825
2461
|
outputSchema: {
|
|
1826
2462
|
query: z.string(),
|
|
1827
2463
|
runs_dir: z.string(),
|
|
@@ -1872,7 +2508,7 @@ export function registerTools(server) {
|
|
|
1872
2508
|
});
|
|
1873
2509
|
});
|
|
1874
2510
|
registerTool('mcplab_results_context', {
|
|
1875
|
-
description: 'Fetch
|
|
2511
|
+
description: 'Fetch bounded evidence for one run/scenario after a search hit. Use this to inspect a focused result, summary, or trace excerpt; it is not a broad search tool. For broad final-answer/result searches use mcplab_results_search, and for raw trace discovery use mcplab_trace_search.',
|
|
1876
2512
|
outputSchema: {
|
|
1877
2513
|
run_id: z.string(),
|
|
1878
2514
|
scenario_id: z.string(),
|
|
@@ -1922,6 +2558,30 @@ export function registerTools(server) {
|
|
|
1922
2558
|
});
|
|
1923
2559
|
});
|
|
1924
2560
|
});
|
|
2561
|
+
registerTool('mcplab_list_skills', {
|
|
2562
|
+
description: 'List MCPLab skills bundled with this MCP server.',
|
|
2563
|
+
inputSchema: {},
|
|
2564
|
+
outputSchema: {
|
|
2565
|
+
skills: z.array(z.object({ name: z.string(), description: z.string(), files: z.array(z.string()) }))
|
|
2566
|
+
}
|
|
2567
|
+
}, async () => withToolHandling(() => {
|
|
2568
|
+
const skills = loadSkills();
|
|
2569
|
+
return ok(`Found ${skills.length} MCPLab skill(s)`, {
|
|
2570
|
+
skills: skills.map(({ name, description, files }) => ({ name, description, files }))
|
|
2571
|
+
});
|
|
2572
|
+
}));
|
|
2573
|
+
registerTool('mcplab_get_skill', {
|
|
2574
|
+
description: 'Read one file from a bundled MCPLab skill. Defaults to SKILL.md.',
|
|
2575
|
+
inputSchema: { name: z.string(), file: z.string().optional() },
|
|
2576
|
+
outputSchema: { name: z.string(), file: z.string(), content: z.string() }
|
|
2577
|
+
}, async ({ name, file }) => withToolHandling(() => {
|
|
2578
|
+
const targetFile = file ?? 'SKILL.md';
|
|
2579
|
+
return ok(`Read ${name}/${targetFile}`, {
|
|
2580
|
+
name,
|
|
2581
|
+
file: targetFile,
|
|
2582
|
+
content: readSkillFile(name, targetFile)
|
|
2583
|
+
});
|
|
2584
|
+
}));
|
|
1925
2585
|
}
|
|
1926
2586
|
export function registerPrompts(server) {
|
|
1927
2587
|
server.registerPrompt('mcplab-scenario-author', {
|
|
@@ -1988,17 +2648,33 @@ export function registerPrompts(server) {
|
|
|
1988
2648
|
const DESTRUCTIVE_TOOLS = new Set([
|
|
1989
2649
|
'mcplab_delete_tool_analysis_result',
|
|
1990
2650
|
'mcplab_write_markdown_report',
|
|
1991
|
-
'mcplab_run_eval'
|
|
2651
|
+
'mcplab_run_eval',
|
|
2652
|
+
'mcplab_queue_run',
|
|
2653
|
+
'mcplab_create_test_case',
|
|
2654
|
+
'mcplab_update_test_case',
|
|
2655
|
+
'mcplab_create_evaluation_config',
|
|
2656
|
+
'mcplab_update_evaluation_config'
|
|
2657
|
+
]);
|
|
2658
|
+
const MUTATING_TOOLS = new Set([
|
|
2659
|
+
'mcplab_write_markdown_report',
|
|
2660
|
+
'mcplab_run_eval',
|
|
2661
|
+
'mcplab_queue_run',
|
|
2662
|
+
'mcplab_create_test_case',
|
|
2663
|
+
'mcplab_update_test_case',
|
|
2664
|
+
'mcplab_create_evaluation_config',
|
|
2665
|
+
'mcplab_update_evaluation_config'
|
|
1992
2666
|
]);
|
|
1993
|
-
const
|
|
1994
|
-
const OPEN_WORLD_TOOLS = new Set(['mcplab_run_eval']);
|
|
2667
|
+
const OPEN_WORLD_TOOLS = new Set(['mcplab_run_eval', 'mcplab_queue_run']);
|
|
1995
2668
|
const PREFERRED_TOOL_TITLES = {
|
|
1996
2669
|
mcplab_write_markdown_report: 'Write Markdown Report to Disk',
|
|
1997
2670
|
mcplab_search_markdown_reports: 'Search Markdown Reports',
|
|
1998
2671
|
mcplab_list_library: 'Search Library Entries',
|
|
1999
2672
|
mcplab_generate_agent_entry: 'Generate MCPLab agents.yaml Entry',
|
|
2000
2673
|
mcplab_search_tool_analysis_results: 'Search Tool Analysis Results',
|
|
2001
|
-
mcplab_trace_search: 'Search Trace Events'
|
|
2674
|
+
mcplab_trace_search: 'Search Raw Trace Events',
|
|
2675
|
+
mcplab_results_search: 'Search Indexed Evaluation Results',
|
|
2676
|
+
mcplab_results_context: 'Fetch Focused Result or Trace Context',
|
|
2677
|
+
mcplab_queue_run: 'Queue MCPLab Evaluation in APP'
|
|
2002
2678
|
};
|
|
2003
2679
|
function normalizeOptionalNonEmpty(value) {
|
|
2004
2680
|
if (typeof value !== 'string')
|
|
@@ -2089,7 +2765,7 @@ function mergeLibraryServersAndAgents(config, bundleRoot) {
|
|
|
2089
2765
|
function readLibrary(bundleRoot, includeContent) {
|
|
2090
2766
|
const serversPath = join(bundleRoot, 'servers.yaml');
|
|
2091
2767
|
const agentsPath = join(bundleRoot, 'agents.yaml');
|
|
2092
|
-
const scenariosDir =
|
|
2768
|
+
const scenariosDir = resolveScenarioLibraryDir(bundleRoot).path;
|
|
2093
2769
|
const servers = existsSync(serversPath)
|
|
2094
2770
|
? parseYaml(readFileSync(serversPath, 'utf8')) ?? {}
|
|
2095
2771
|
: {};
|
|
@@ -2126,6 +2802,7 @@ function readLibrary(bundleRoot, includeContent) {
|
|
|
2126
2802
|
id,
|
|
2127
2803
|
...(includeContent ? { entry: agents[id] } : {})
|
|
2128
2804
|
})),
|
|
2805
|
+
test_cases: scenarioEntries,
|
|
2129
2806
|
scenarios: scenarioEntries
|
|
2130
2807
|
};
|
|
2131
2808
|
return out;
|
|
@@ -2150,7 +2827,7 @@ function getLibraryItem(bundleRoot, kind, id) {
|
|
|
2150
2827
|
content: entry
|
|
2151
2828
|
};
|
|
2152
2829
|
}
|
|
2153
|
-
const dir =
|
|
2830
|
+
const dir = resolveScenarioLibraryDir(bundleRoot).path;
|
|
2154
2831
|
if (!existsSync(dir)) {
|
|
2155
2832
|
throw new Error(`Scenario library directory not found: ${dir}`);
|
|
2156
2833
|
}
|
|
@@ -2172,6 +2849,93 @@ function getLibraryItem(bundleRoot, kind, id) {
|
|
|
2172
2849
|
}
|
|
2173
2850
|
throw new Error(`Scenario '${id}' not found in ${dir}`);
|
|
2174
2851
|
}
|
|
2852
|
+
function listEvaluationConfigs(params) {
|
|
2853
|
+
const evalsDir = join(resolveBundleRoot(), 'evals');
|
|
2854
|
+
const files = [];
|
|
2855
|
+
const walk = (dir) => {
|
|
2856
|
+
if (!existsSync(dir))
|
|
2857
|
+
return;
|
|
2858
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
2859
|
+
const path = join(dir, entry.name);
|
|
2860
|
+
if (entry.isDirectory())
|
|
2861
|
+
walk(path);
|
|
2862
|
+
else if (entry.isFile() && /\.ya?ml$/i.test(entry.name))
|
|
2863
|
+
files.push(path);
|
|
2864
|
+
}
|
|
2865
|
+
};
|
|
2866
|
+
walk(evalsDir);
|
|
2867
|
+
const all = files.map((path) => {
|
|
2868
|
+
const relativePath = relative(evalsDir, path).split(sep).join('/');
|
|
2869
|
+
const suitePath = dirname(relativePath) === '.' ? '' : dirname(relativePath).split(sep).join('/');
|
|
2870
|
+
const fallbackName = basename(path, extname(path));
|
|
2871
|
+
const stat = statSync(path);
|
|
2872
|
+
try {
|
|
2873
|
+
const parsed = parseYaml(readFileSync(path, 'utf8'));
|
|
2874
|
+
return {
|
|
2875
|
+
id: Buffer.from(relativePath, 'utf8').toString('base64url'),
|
|
2876
|
+
name: typeof parsed?.name === 'string' && parsed.name.trim()
|
|
2877
|
+
? parsed.name.trim()
|
|
2878
|
+
: fallbackName,
|
|
2879
|
+
relative_path: relativePath,
|
|
2880
|
+
config_path: path,
|
|
2881
|
+
suite_path: suitePath,
|
|
2882
|
+
updated_at: stat.mtime.toISOString(),
|
|
2883
|
+
scenario_count: Array.isArray(parsed?.scenarios) ? parsed.scenarios.length : 0,
|
|
2884
|
+
agent_count: Array.isArray(parsed?.agents) ? parsed.agents.length : 0
|
|
2885
|
+
};
|
|
2886
|
+
}
|
|
2887
|
+
catch (error) {
|
|
2888
|
+
return {
|
|
2889
|
+
id: Buffer.from(relativePath, 'utf8').toString('base64url'),
|
|
2890
|
+
name: fallbackName,
|
|
2891
|
+
relative_path: relativePath,
|
|
2892
|
+
config_path: path,
|
|
2893
|
+
suite_path: suitePath,
|
|
2894
|
+
updated_at: stat.mtime.toISOString(),
|
|
2895
|
+
scenario_count: 0,
|
|
2896
|
+
agent_count: 0,
|
|
2897
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2898
|
+
};
|
|
2899
|
+
}
|
|
2900
|
+
});
|
|
2901
|
+
const suite = params.suite?.trim().replace(/^suite:/, '');
|
|
2902
|
+
const query = params.query?.trim().toLowerCase();
|
|
2903
|
+
const matching = all.filter((config) => (!suite || config.suite_path === suite || config.suite_path.startsWith(`${suite}/`)) &&
|
|
2904
|
+
(!query ||
|
|
2905
|
+
`${config.name} ${config.relative_path} ${config.error ?? ''}`
|
|
2906
|
+
.toLowerCase()
|
|
2907
|
+
.includes(query)));
|
|
2908
|
+
const sortBy = params.sortBy ?? 'name';
|
|
2909
|
+
const direction = params.sortDirection ?? 'asc';
|
|
2910
|
+
matching.sort((a, b) => {
|
|
2911
|
+
const left = sortBy === 'scenarios'
|
|
2912
|
+
? a.scenario_count
|
|
2913
|
+
: sortBy === 'agents'
|
|
2914
|
+
? a.agent_count
|
|
2915
|
+
: sortBy === 'updated_at'
|
|
2916
|
+
? Date.parse(a.updated_at)
|
|
2917
|
+
: a.name;
|
|
2918
|
+
const right = sortBy === 'scenarios'
|
|
2919
|
+
? b.scenario_count
|
|
2920
|
+
: sortBy === 'agents'
|
|
2921
|
+
? b.agent_count
|
|
2922
|
+
: sortBy === 'updated_at'
|
|
2923
|
+
? Date.parse(b.updated_at)
|
|
2924
|
+
: b.name;
|
|
2925
|
+
const comparison = typeof left === 'number' && typeof right === 'number'
|
|
2926
|
+
? left - right
|
|
2927
|
+
: String(left).localeCompare(String(right));
|
|
2928
|
+
return direction === 'asc' ? comparison : -comparison;
|
|
2929
|
+
});
|
|
2930
|
+
return {
|
|
2931
|
+
evals_dir: evalsDir,
|
|
2932
|
+
total: all.length,
|
|
2933
|
+
matching: matching.length,
|
|
2934
|
+
filters: removeUndefined({ suite, query }),
|
|
2935
|
+
sort: { by: sortBy, direction },
|
|
2936
|
+
configs: matching
|
|
2937
|
+
};
|
|
2938
|
+
}
|
|
2175
2939
|
function buildServerEntry(input) {
|
|
2176
2940
|
const transport = input.transport ?? 'http';
|
|
2177
2941
|
const authType = input.auth_type ?? 'none';
|
|
@@ -2220,46 +2984,7 @@ function buildServerEntry(input) {
|
|
|
2220
2984
|
};
|
|
2221
2985
|
}
|
|
2222
2986
|
function buildScenario(input) {
|
|
2223
|
-
|
|
2224
|
-
if (!id) {
|
|
2225
|
-
throw new Error('Unable to derive scenario id. Provide id or name.');
|
|
2226
|
-
}
|
|
2227
|
-
const scenario = removeUndefined({
|
|
2228
|
-
id,
|
|
2229
|
-
agent: input.agent?.trim() || undefined,
|
|
2230
|
-
servers: input.servers,
|
|
2231
|
-
prompt: input.prompt,
|
|
2232
|
-
eval: buildEvalRules(input),
|
|
2233
|
-
extract: input.extract_rules?.map((rule) => ({
|
|
2234
|
-
name: rule.name,
|
|
2235
|
-
from: 'final_text',
|
|
2236
|
-
regex: rule.regex
|
|
2237
|
-
}))
|
|
2238
|
-
});
|
|
2239
|
-
return scenario;
|
|
2240
|
-
}
|
|
2241
|
-
function buildEvalRules(input) {
|
|
2242
|
-
const toolConstraints = input.required_tools?.length || input.forbidden_tools?.length
|
|
2243
|
-
? removeUndefined({
|
|
2244
|
-
required_tools: normalizeStringArray(input.required_tools),
|
|
2245
|
-
forbidden_tools: normalizeStringArray(input.forbidden_tools)
|
|
2246
|
-
})
|
|
2247
|
-
: undefined;
|
|
2248
|
-
const toolSequence = input.allowed_tool_sequences && input.allowed_tool_sequences.length > 0
|
|
2249
|
-
? { allow: input.allowed_tool_sequences }
|
|
2250
|
-
: undefined;
|
|
2251
|
-
const responseAssertions = input.response_regex_patterns && input.response_regex_patterns.length > 0
|
|
2252
|
-
? input.response_regex_patterns.map((pattern) => ({ type: 'regex', pattern }))
|
|
2253
|
-
: undefined;
|
|
2254
|
-
const evalRules = removeUndefined({
|
|
2255
|
-
tool_constraints: toolConstraints,
|
|
2256
|
-
tool_sequence: toolSequence,
|
|
2257
|
-
response_assertions: responseAssertions
|
|
2258
|
-
});
|
|
2259
|
-
if (Object.keys(evalRules ?? {}).length === 0) {
|
|
2260
|
-
return undefined;
|
|
2261
|
-
}
|
|
2262
|
-
return evalRules;
|
|
2987
|
+
return buildScenarioEntry(input);
|
|
2263
2988
|
}
|
|
2264
2989
|
function validateScenarioHeuristics(scenario) {
|
|
2265
2990
|
const warnings = [];
|
|
@@ -2981,19 +3706,6 @@ function searchableText(value) {
|
|
|
2981
3706
|
visit(value);
|
|
2982
3707
|
return tokens.join(' ');
|
|
2983
3708
|
}
|
|
2984
|
-
function normalizeStringArray(values) {
|
|
2985
|
-
if (!values)
|
|
2986
|
-
return undefined;
|
|
2987
|
-
const out = values.map((value) => value.trim()).filter(Boolean);
|
|
2988
|
-
return out.length > 0 ? out : undefined;
|
|
2989
|
-
}
|
|
2990
|
-
function slugify(input) {
|
|
2991
|
-
return input
|
|
2992
|
-
.toLowerCase()
|
|
2993
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
2994
|
-
.replace(/^-+|-+$/g, '')
|
|
2995
|
-
.slice(0, 80);
|
|
2996
|
-
}
|
|
2997
3709
|
function indentBlock(text, spaces) {
|
|
2998
3710
|
const prefix = ' '.repeat(spaces);
|
|
2999
3711
|
return text
|
|
@@ -3033,7 +3745,14 @@ async function handleHttpRequest(req, res, sessions, mcpPath) {
|
|
|
3033
3745
|
await runtime.transport.handleRequest(req, res, body);
|
|
3034
3746
|
return;
|
|
3035
3747
|
}
|
|
3036
|
-
|
|
3748
|
+
// Per the MCP Streamable HTTP transport, an unknown session indicates that
|
|
3749
|
+
// the server was restarted or the session expired. The client must receive
|
|
3750
|
+
// 404 so it can discard the stale session and initialize a new connection.
|
|
3751
|
+
if (sessionId) {
|
|
3752
|
+
sendPlain(res, 404, 'Not Found');
|
|
3753
|
+
return;
|
|
3754
|
+
}
|
|
3755
|
+
if (isInitializeRequest(body)) {
|
|
3037
3756
|
let runtime;
|
|
3038
3757
|
const transport = new StreamableHTTPServerTransport({
|
|
3039
3758
|
sessionIdGenerator: () => randomUUID(),
|
|
@@ -3067,7 +3786,11 @@ async function handleHttpRequest(req, res, sessions, mcpPath) {
|
|
|
3067
3786
|
}
|
|
3068
3787
|
if (method === 'GET' || method === 'DELETE') {
|
|
3069
3788
|
const sessionId = getSessionId(req);
|
|
3070
|
-
if (
|
|
3789
|
+
if (sessionId && !sessions.has(sessionId)) {
|
|
3790
|
+
sendPlain(res, 404, 'Not Found');
|
|
3791
|
+
return;
|
|
3792
|
+
}
|
|
3793
|
+
if (!sessionId) {
|
|
3071
3794
|
sendPlain(res, 400, 'Invalid or missing session ID');
|
|
3072
3795
|
return;
|
|
3073
3796
|
}
|