@inspectr/mcplab-mcp-server 1.7.1 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/runtime.contracts.test.js +149 -3
- package/dist/runtime.contracts.test.js.map +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +612 -92
- package/dist/runtime.js.map +1 -1
- package/dist/skills/mcplab-assistant/SKILL.md +291 -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'),
|
|
@@ -150,8 +166,9 @@ const ServerEntrySchema = z.object({
|
|
|
150
166
|
});
|
|
151
167
|
const ScenarioEntrySchema = z.object({
|
|
152
168
|
id: z.string(),
|
|
169
|
+
name: z.string().optional(),
|
|
153
170
|
agent: z.string().optional(),
|
|
154
|
-
|
|
171
|
+
mcp_servers: z.array(z.object({ ref: z.string() })),
|
|
155
172
|
prompt: z.string(),
|
|
156
173
|
eval: GenericObjectSchema.optional(),
|
|
157
174
|
extract: z
|
|
@@ -180,14 +197,7 @@ const ResolvedScenarioSchema = z.object({
|
|
|
180
197
|
servers: z.array(z.string()),
|
|
181
198
|
agent: z.string().optional(),
|
|
182
199
|
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(),
|
|
200
|
+
eval: GenericObjectSchema.optional(),
|
|
191
201
|
extract: z
|
|
192
202
|
.array(z
|
|
193
203
|
.object({
|
|
@@ -532,6 +542,7 @@ export function createConfiguredServer() {
|
|
|
532
542
|
websiteUrl: 'https://mcplab.inspectr.dev',
|
|
533
543
|
icons: [{ src: SERVER_ICON_URL, mimeType: 'image/svg+xml' }]
|
|
534
544
|
});
|
|
545
|
+
registerSkills(server);
|
|
535
546
|
registerTools(server);
|
|
536
547
|
registerPrompts(server);
|
|
537
548
|
return server;
|
|
@@ -547,6 +558,79 @@ export function registerTools(server) {
|
|
|
547
558
|
annotations: inferToolAnnotations(name, resolvedTitle, config.annotations)
|
|
548
559
|
}, cb);
|
|
549
560
|
};
|
|
561
|
+
registerTool('mcplab_build_app_link', {
|
|
562
|
+
description: 'Build a safe deep link to a local MCPLab app view.',
|
|
563
|
+
inputSchema: {
|
|
564
|
+
view: z.enum([
|
|
565
|
+
'home',
|
|
566
|
+
'evaluations',
|
|
567
|
+
'run_evaluation',
|
|
568
|
+
'results',
|
|
569
|
+
'result_detail',
|
|
570
|
+
'compare',
|
|
571
|
+
'tool_analysis',
|
|
572
|
+
'tool_analysis_results',
|
|
573
|
+
'tool_analysis_result',
|
|
574
|
+
'library_servers',
|
|
575
|
+
'library_agents',
|
|
576
|
+
'library_test_cases',
|
|
577
|
+
'settings'
|
|
578
|
+
]),
|
|
579
|
+
run_id: z.string().min(1).max(200).optional(),
|
|
580
|
+
tool_analysis_id: z.string().min(1).max(200).optional(),
|
|
581
|
+
config_id: z.string().min(1).max(200).optional(),
|
|
582
|
+
agent: z.string().min(1).max(200).optional(),
|
|
583
|
+
scenario: z.string().min(1).max(200).optional(),
|
|
584
|
+
time_filter: z.enum(['last', 'custom']).optional(),
|
|
585
|
+
time_preset: z.enum(['15min', '30min', '1h', '24h', '7d', '14d', '30d']).optional(),
|
|
586
|
+
time_start: z.string().datetime().optional(),
|
|
587
|
+
time_end: z.string().datetime().optional()
|
|
588
|
+
},
|
|
589
|
+
outputSchema: { url: z.string().url(), view: z.string(), path: z.string() }
|
|
590
|
+
}, async ({ view, run_id, tool_analysis_id, config_id, agent, scenario, time_filter, time_preset, time_start, time_end }) => withToolHandling(() => {
|
|
591
|
+
const paths = {
|
|
592
|
+
home: '/',
|
|
593
|
+
evaluations: '/evaluations',
|
|
594
|
+
run_evaluation: '/run',
|
|
595
|
+
results: '/results',
|
|
596
|
+
result_detail: '/results/detail',
|
|
597
|
+
compare: '/compare',
|
|
598
|
+
tool_analysis: '/tool-analysis',
|
|
599
|
+
tool_analysis_results: '/tool-analysis/results',
|
|
600
|
+
tool_analysis_result: '/tool-analysis/result',
|
|
601
|
+
library_servers: '/servers',
|
|
602
|
+
library_agents: '/agents',
|
|
603
|
+
library_test_cases: '/test-cases',
|
|
604
|
+
settings: '/settings'
|
|
605
|
+
};
|
|
606
|
+
const path = paths[view];
|
|
607
|
+
if (!path)
|
|
608
|
+
throw new Error(`Unsupported app view: ${view}`);
|
|
609
|
+
const url = new URL(path, process.env.MCPLAB_APP_URL ?? 'http://127.0.0.1:8787');
|
|
610
|
+
if (run_id)
|
|
611
|
+
url.searchParams.set('run', run_id);
|
|
612
|
+
if (tool_analysis_id)
|
|
613
|
+
url.searchParams.set('tool_analysis_id', tool_analysis_id);
|
|
614
|
+
if (config_id)
|
|
615
|
+
url.searchParams.set('config', config_id);
|
|
616
|
+
if (agent)
|
|
617
|
+
url.searchParams.set('agent', agent);
|
|
618
|
+
if (scenario)
|
|
619
|
+
url.searchParams.set('scenario', scenario);
|
|
620
|
+
if (time_filter)
|
|
621
|
+
url.searchParams.set('time_filter', time_filter);
|
|
622
|
+
if (time_preset)
|
|
623
|
+
url.searchParams.set('time_preset', time_preset);
|
|
624
|
+
if (time_start)
|
|
625
|
+
url.searchParams.set('time_start', time_start);
|
|
626
|
+
if (time_end)
|
|
627
|
+
url.searchParams.set('time_end', time_end);
|
|
628
|
+
if (view === 'result_detail' && !run_id)
|
|
629
|
+
throw new Error('run_id is required for result_detail');
|
|
630
|
+
if (view === 'tool_analysis_result' && !tool_analysis_id)
|
|
631
|
+
throw new Error('tool_analysis_id is required for tool_analysis_result');
|
|
632
|
+
return ok(`Built app link for ${view}`, { url: url.toString(), view, path });
|
|
633
|
+
}));
|
|
550
634
|
registerTool('mcplab_write_markdown_report', {
|
|
551
635
|
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
636
|
outputSchema: WriteMarkdownReportOutputSchema,
|
|
@@ -688,7 +772,7 @@ export function registerTools(server) {
|
|
|
688
772
|
}
|
|
689
773
|
});
|
|
690
774
|
registerTool('mcplab_search_markdown_reports', {
|
|
691
|
-
description: '
|
|
775
|
+
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
776
|
outputSchema: {
|
|
693
777
|
reports_dir: z.string(),
|
|
694
778
|
run_id_filter: z.string().optional(),
|
|
@@ -819,7 +903,7 @@ export function registerTools(server) {
|
|
|
819
903
|
outputSchema: LibraryEntrySchema,
|
|
820
904
|
inputSchema: {
|
|
821
905
|
kind: z
|
|
822
|
-
.enum(['all', 'servers', 'agents', 'scenarios'])
|
|
906
|
+
.enum(['all', 'servers', 'agents', 'test_cases', 'scenarios'])
|
|
823
907
|
.optional()
|
|
824
908
|
.describe('Which library category to list. Defaults to all.'),
|
|
825
909
|
includeContent: z
|
|
@@ -838,6 +922,7 @@ export function registerTools(server) {
|
|
|
838
922
|
bundleRoot: data.bundleRoot,
|
|
839
923
|
servers: selectedKind === 'servers' ? data.servers : [],
|
|
840
924
|
agents: selectedKind === 'agents' ? data.agents : [],
|
|
925
|
+
test_cases: selectedKind === 'test_cases' ? data.test_cases : [],
|
|
841
926
|
scenarios: selectedKind === 'scenarios' ? data.scenarios : []
|
|
842
927
|
};
|
|
843
928
|
return ok(`Loaded MCPLab library from ${root}`, structured);
|
|
@@ -847,14 +932,16 @@ export function registerTools(server) {
|
|
|
847
932
|
description: 'Get a specific reusable server, agent, or scenario definition from a MCPLab library bundle and return both structured data and YAML.',
|
|
848
933
|
outputSchema: {
|
|
849
934
|
bundleRoot: z.string(),
|
|
850
|
-
kind: z.enum(['servers', 'agents', 'scenarios']),
|
|
935
|
+
kind: z.enum(['servers', 'agents', 'test_cases', 'scenarios']),
|
|
851
936
|
id: z.string(),
|
|
852
937
|
file: z.string().optional(),
|
|
853
938
|
yaml: z.string(),
|
|
854
939
|
content: GenericObjectSchema
|
|
855
940
|
},
|
|
856
941
|
inputSchema: {
|
|
857
|
-
kind: z
|
|
942
|
+
kind: z
|
|
943
|
+
.enum(['servers', 'agents', 'test_cases', 'scenarios'])
|
|
944
|
+
.describe('Library category.'),
|
|
858
945
|
id: z.string().describe('Entry id (for scenarios this is scenario.id, not filename).')
|
|
859
946
|
}
|
|
860
947
|
}, async ({ kind, id }) => {
|
|
@@ -910,6 +997,183 @@ export function registerTools(server) {
|
|
|
910
997
|
});
|
|
911
998
|
});
|
|
912
999
|
});
|
|
1000
|
+
registerTool('mcplab_create_test_case', {
|
|
1001
|
+
description: 'Create one reviewed MCPLab Test Case in mcplab/test-cases/. This is a scoped YAML write and refuses duplicate IDs.',
|
|
1002
|
+
outputSchema: { id: z.string(), path: z.string(), test_case: ScenarioEntrySchema },
|
|
1003
|
+
inputSchema: {
|
|
1004
|
+
id: z.string().min(1),
|
|
1005
|
+
name: z.string().optional(),
|
|
1006
|
+
servers: z.array(z.string()).min(1),
|
|
1007
|
+
prompt: z.string().min(1).max(4000),
|
|
1008
|
+
required_tools: z.array(z.string()).optional(),
|
|
1009
|
+
forbidden_tools: z.array(z.string()).optional(),
|
|
1010
|
+
tool_sequence: z
|
|
1011
|
+
.array(z.string())
|
|
1012
|
+
.min(1)
|
|
1013
|
+
.optional()
|
|
1014
|
+
.describe('Required tool calls in order.'),
|
|
1015
|
+
extract_rules: z.array(z.object({ name: z.string(), regex: z.string() })).optional(),
|
|
1016
|
+
response_regex_patterns: z.array(z.string()).optional()
|
|
1017
|
+
}
|
|
1018
|
+
}, async ({ id, name, servers, prompt, required_tools, forbidden_tools, tool_sequence, extract_rules, response_regex_patterns }) => withToolHandling(async () => {
|
|
1019
|
+
const bundleRoot = resolveBundleRoot();
|
|
1020
|
+
const library = readLibrary(bundleRoot, false);
|
|
1021
|
+
const created = createTestCaseFile({
|
|
1022
|
+
librariesDir: bundleRoot,
|
|
1023
|
+
knownServerIds: library.servers.map((item) => item.id),
|
|
1024
|
+
testCase: {
|
|
1025
|
+
id,
|
|
1026
|
+
name,
|
|
1027
|
+
servers,
|
|
1028
|
+
prompt,
|
|
1029
|
+
requiredTools: required_tools,
|
|
1030
|
+
forbiddenTools: forbidden_tools,
|
|
1031
|
+
toolSequence: tool_sequence,
|
|
1032
|
+
extractRules: extract_rules,
|
|
1033
|
+
responseRegexPatterns: response_regex_patterns
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
1036
|
+
return ok(`Created Test Case '${created.id}'`, {
|
|
1037
|
+
id: created.id,
|
|
1038
|
+
path: created.path,
|
|
1039
|
+
test_case: created.testCase
|
|
1040
|
+
});
|
|
1041
|
+
}));
|
|
1042
|
+
registerTool('mcplab_get_test_case', {
|
|
1043
|
+
description: 'Read one existing MCPLab test case, including its full parsed YAML content.',
|
|
1044
|
+
inputSchema: { file_path: z.string().describe('Path relative to mcplab/test-cases/.') },
|
|
1045
|
+
outputSchema: { path: z.string(), test_case: ScenarioEntrySchema }
|
|
1046
|
+
}, async ({ file_path }) => withToolHandling(() => {
|
|
1047
|
+
const result = readTestCaseFile({ librariesDir: resolveBundleRoot(), filePath: file_path });
|
|
1048
|
+
return ok(`Read Test Case '${result.id}'`, {
|
|
1049
|
+
path: result.path,
|
|
1050
|
+
test_case: result.testCase
|
|
1051
|
+
});
|
|
1052
|
+
}));
|
|
1053
|
+
registerTool('mcplab_update_test_case', {
|
|
1054
|
+
description: 'Replace one existing MCPLab test case in place. Requires confirm=true and never creates a new file.',
|
|
1055
|
+
inputSchema: {
|
|
1056
|
+
file_path: z
|
|
1057
|
+
.string()
|
|
1058
|
+
.describe('Path to an existing test case relative to mcplab/test-cases/.'),
|
|
1059
|
+
confirm: z
|
|
1060
|
+
.literal(true)
|
|
1061
|
+
.describe('Required confirmation that the existing file should be overwritten.'),
|
|
1062
|
+
id: z.string().min(1),
|
|
1063
|
+
name: z.string().optional(),
|
|
1064
|
+
servers: z.array(z.string()).min(1),
|
|
1065
|
+
prompt: z.string().min(1).max(4000),
|
|
1066
|
+
required_tools: z.array(z.string()).optional(),
|
|
1067
|
+
forbidden_tools: z.array(z.string()).optional(),
|
|
1068
|
+
tool_sequence: z.array(z.string()).min(1).optional(),
|
|
1069
|
+
extract_rules: z.array(z.object({ name: z.string(), regex: z.string() })).optional(),
|
|
1070
|
+
response_regex_patterns: z.array(z.string()).optional()
|
|
1071
|
+
},
|
|
1072
|
+
outputSchema: { id: z.string(), path: z.string(), test_case: ScenarioEntrySchema }
|
|
1073
|
+
}, async ({ file_path, confirm: _confirm, id, name, servers, prompt, required_tools, forbidden_tools, tool_sequence, extract_rules, response_regex_patterns }) => withToolHandling(() => {
|
|
1074
|
+
const library = readLibrary(resolveBundleRoot(), false);
|
|
1075
|
+
const updated = updateTestCaseFile({
|
|
1076
|
+
librariesDir: resolveBundleRoot(),
|
|
1077
|
+
knownServerIds: library.servers.map((item) => item.id),
|
|
1078
|
+
filePath: file_path,
|
|
1079
|
+
testCase: {
|
|
1080
|
+
id,
|
|
1081
|
+
name,
|
|
1082
|
+
servers,
|
|
1083
|
+
prompt,
|
|
1084
|
+
requiredTools: required_tools,
|
|
1085
|
+
forbiddenTools: forbidden_tools,
|
|
1086
|
+
toolSequence: tool_sequence,
|
|
1087
|
+
extractRules: extract_rules,
|
|
1088
|
+
responseRegexPatterns: response_regex_patterns
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
1091
|
+
return ok(`Updated Test Case '${updated.id}'`, {
|
|
1092
|
+
id: updated.id,
|
|
1093
|
+
path: updated.path,
|
|
1094
|
+
test_case: updated.testCase
|
|
1095
|
+
});
|
|
1096
|
+
}));
|
|
1097
|
+
registerTool('mcplab_create_evaluation_config', {
|
|
1098
|
+
description: 'Create one reviewed MCPLab evaluation configuration in mcplab/evals/. This is a scoped YAML write with automatic filename normalization and collision handling.',
|
|
1099
|
+
outputSchema: {
|
|
1100
|
+
file_name: z.string(),
|
|
1101
|
+
path: z.string(),
|
|
1102
|
+
relative_path: z.string(),
|
|
1103
|
+
config: GenericObjectSchema
|
|
1104
|
+
},
|
|
1105
|
+
inputSchema: {
|
|
1106
|
+
file_name: z.string().min(1),
|
|
1107
|
+
config: GenericObjectSchema.describe('Complete MCPLab evaluation configuration.')
|
|
1108
|
+
}
|
|
1109
|
+
}, async ({ file_name, config }) => withToolHandling(async () => {
|
|
1110
|
+
const created = createEvaluationConfigFile({
|
|
1111
|
+
evalsDir: join(resolveBundleRoot(), 'evals'),
|
|
1112
|
+
fileName: file_name,
|
|
1113
|
+
config: config
|
|
1114
|
+
});
|
|
1115
|
+
return ok(`Created evaluation configuration '${created.fileName}'`, {
|
|
1116
|
+
file_name: created.fileName,
|
|
1117
|
+
path: created.path,
|
|
1118
|
+
relative_path: created.relativePath,
|
|
1119
|
+
config: created.config
|
|
1120
|
+
});
|
|
1121
|
+
}));
|
|
1122
|
+
registerTool('mcplab_get_evaluation_config', {
|
|
1123
|
+
description: 'Read one existing MCPLab evaluation config, including its full parsed YAML content.',
|
|
1124
|
+
inputSchema: {
|
|
1125
|
+
config_path: z
|
|
1126
|
+
.string()
|
|
1127
|
+
.describe('Path to an existing eval YAML, relative to the MCPLab evals directory.')
|
|
1128
|
+
},
|
|
1129
|
+
outputSchema: z.object({
|
|
1130
|
+
path: z.string(),
|
|
1131
|
+
relative_path: z.string(),
|
|
1132
|
+
config: GenericObjectSchema
|
|
1133
|
+
})
|
|
1134
|
+
}, async ({ config_path }) => withToolHandling(() => {
|
|
1135
|
+
const result = readEvaluationConfigFile({
|
|
1136
|
+
evalsDir: join(resolveBundleRoot(), 'evals'),
|
|
1137
|
+
filePath: config_path
|
|
1138
|
+
});
|
|
1139
|
+
return ok(`Read evaluation config '${result.relativePath}'`, {
|
|
1140
|
+
path: result.path,
|
|
1141
|
+
relative_path: result.relativePath,
|
|
1142
|
+
config: result.config
|
|
1143
|
+
});
|
|
1144
|
+
}));
|
|
1145
|
+
registerTool('mcplab_update_evaluation_config', {
|
|
1146
|
+
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.',
|
|
1147
|
+
inputSchema: {
|
|
1148
|
+
config_path: z
|
|
1149
|
+
.string()
|
|
1150
|
+
.describe('Path to an existing eval YAML, relative to the MCPLab evals directory.'),
|
|
1151
|
+
config: GenericObjectSchema.describe('Complete replacement evaluation configuration.'),
|
|
1152
|
+
confirm: z
|
|
1153
|
+
.literal(true)
|
|
1154
|
+
.describe('Required confirmation that the existing file should be overwritten.')
|
|
1155
|
+
},
|
|
1156
|
+
outputSchema: {
|
|
1157
|
+
file_name: z.string(),
|
|
1158
|
+
path: z.string(),
|
|
1159
|
+
relative_path: z.string(),
|
|
1160
|
+
config: GenericObjectSchema,
|
|
1161
|
+
warnings: z.array(z.string())
|
|
1162
|
+
}
|
|
1163
|
+
}, async ({ config_path, config }) => withToolHandling(() => {
|
|
1164
|
+
const updated = updateEvaluationConfigFile({
|
|
1165
|
+
evalsDir: join(resolveBundleRoot(), 'evals'),
|
|
1166
|
+
filePath: config_path,
|
|
1167
|
+
config: config
|
|
1168
|
+
});
|
|
1169
|
+
return ok(`Updated evaluation config '${updated.relativePath}'`, {
|
|
1170
|
+
file_name: updated.fileName,
|
|
1171
|
+
path: updated.path,
|
|
1172
|
+
relative_path: updated.relativePath,
|
|
1173
|
+
config: updated.config,
|
|
1174
|
+
warnings: updated.warnings
|
|
1175
|
+
});
|
|
1176
|
+
}));
|
|
913
1177
|
registerTool('mcplab_generate_scenario_entry', {
|
|
914
1178
|
description: 'Generate a MCPLab scenario YAML snippet with prompt, server links, and optional evaluation/extract rules. Optimized for scenario authoring workflows.',
|
|
915
1179
|
outputSchema: {
|
|
@@ -928,7 +1192,7 @@ export function registerTools(server) {
|
|
|
928
1192
|
name: z
|
|
929
1193
|
.string()
|
|
930
1194
|
.optional()
|
|
931
|
-
.describe('Optional human label used
|
|
1195
|
+
.describe('Optional human-readable scenario label. Also used to derive id when omitted.'),
|
|
932
1196
|
agent: z
|
|
933
1197
|
.string()
|
|
934
1198
|
.optional()
|
|
@@ -936,7 +1200,7 @@ export function registerTools(server) {
|
|
|
936
1200
|
servers: z
|
|
937
1201
|
.array(z.string())
|
|
938
1202
|
.min(1)
|
|
939
|
-
.describe('One or more server ids
|
|
1203
|
+
.describe('One or more server ids; emitted as scenario-owned mcp_servers refs.'),
|
|
940
1204
|
prompt: z
|
|
941
1205
|
.string()
|
|
942
1206
|
.min(1)
|
|
@@ -944,10 +1208,11 @@ export function registerTools(server) {
|
|
|
944
1208
|
.describe('The task prompt the evaluation agent should execute (1-4000 chars).'),
|
|
945
1209
|
required_tools: z.array(z.string()).optional().describe('Tools that must be called.'),
|
|
946
1210
|
forbidden_tools: z.array(z.string()).optional().describe('Tools that must not be called.'),
|
|
947
|
-
|
|
948
|
-
.array(z.
|
|
1211
|
+
tool_sequence: z
|
|
1212
|
+
.array(z.string())
|
|
1213
|
+
.min(1)
|
|
949
1214
|
.optional()
|
|
950
|
-
.describe('
|
|
1215
|
+
.describe('Required tool calls in this exact order.'),
|
|
951
1216
|
response_regex_patterns: z
|
|
952
1217
|
.array(z.string())
|
|
953
1218
|
.optional()
|
|
@@ -981,6 +1246,16 @@ export function registerTools(server) {
|
|
|
981
1246
|
});
|
|
982
1247
|
});
|
|
983
1248
|
});
|
|
1249
|
+
registerTool('mcplab_list_evaluation_configs', {
|
|
1250
|
+
description: 'List MCPLab evaluation configurations with nested-suite filtering and deterministic sorting.',
|
|
1251
|
+
inputSchema: {
|
|
1252
|
+
suite: z.string().optional(),
|
|
1253
|
+
query: z.string().optional(),
|
|
1254
|
+
sort_by: z.enum(['name', 'scenarios', 'agents', 'updated_at']).optional(),
|
|
1255
|
+
sort_direction: z.enum(['asc', 'desc']).optional()
|
|
1256
|
+
},
|
|
1257
|
+
outputSchema: EvaluationConfigListSchema
|
|
1258
|
+
}, async ({ suite, query, sort_by, sort_direction }) => withToolHandling(() => ok('Listed evaluation configurations', listEvaluationConfigs({ suite, query, sortBy: sort_by, sortDirection: sort_direction }))));
|
|
984
1259
|
registerTool('mcplab_validate_config', {
|
|
985
1260
|
description: 'Validate and expand a MCPLab config file via mcplab-core loadConfig(), including server/agent/scenario library references.',
|
|
986
1261
|
outputSchema: {
|
|
@@ -1021,7 +1296,7 @@ export function registerTools(server) {
|
|
|
1021
1296
|
});
|
|
1022
1297
|
});
|
|
1023
1298
|
registerTool('mcplab_run_eval', {
|
|
1024
|
-
description: '
|
|
1299
|
+
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
1300
|
outputSchema: {
|
|
1026
1301
|
run_dir: z.string(),
|
|
1027
1302
|
total_scenarios: z.number().int().nonnegative(),
|
|
@@ -1117,6 +1392,133 @@ export function registerTools(server) {
|
|
|
1117
1392
|
});
|
|
1118
1393
|
});
|
|
1119
1394
|
});
|
|
1395
|
+
registerTool('mcplab_list_runs', {
|
|
1396
|
+
description: 'List MCPLab evaluation runs with optional inclusive ISO time bounds.',
|
|
1397
|
+
inputSchema: {
|
|
1398
|
+
since: z.string().datetime().optional(),
|
|
1399
|
+
until: z.string().datetime().optional(),
|
|
1400
|
+
limit: z.number().int().positive().max(200).optional()
|
|
1401
|
+
},
|
|
1402
|
+
outputSchema: {
|
|
1403
|
+
since: z.string().optional(),
|
|
1404
|
+
until: z.string().optional(),
|
|
1405
|
+
total_matching: z.number(),
|
|
1406
|
+
runs: z.array(z.object({
|
|
1407
|
+
run_id: z.string(),
|
|
1408
|
+
timestamp: z.string().optional(),
|
|
1409
|
+
config_hash: z.string().optional(),
|
|
1410
|
+
summary: z.any().optional()
|
|
1411
|
+
}))
|
|
1412
|
+
}
|
|
1413
|
+
}, async ({ since, until, limit }) => withToolHandling(() => {
|
|
1414
|
+
const sinceMs = since ? new Date(since).getTime() : undefined;
|
|
1415
|
+
const untilMs = until ? new Date(until).getTime() : undefined;
|
|
1416
|
+
if (sinceMs !== undefined && untilMs !== undefined && sinceMs > untilMs)
|
|
1417
|
+
throw new Error('since must be earlier than or equal to until');
|
|
1418
|
+
const matching = listRunsWithFallback(resolveRunsDir(), undefined, true).filter((entry) => {
|
|
1419
|
+
const timestamp = String(entry.metadata && typeof entry.metadata === 'object'
|
|
1420
|
+
? entry.metadata.timestamp ?? ''
|
|
1421
|
+
: '');
|
|
1422
|
+
const time = new Date(timestamp).getTime();
|
|
1423
|
+
if (sinceMs === undefined && untilMs === undefined)
|
|
1424
|
+
return true;
|
|
1425
|
+
return (!Number.isNaN(time) &&
|
|
1426
|
+
(sinceMs === undefined || time >= sinceMs) &&
|
|
1427
|
+
(untilMs === undefined || time <= untilMs));
|
|
1428
|
+
});
|
|
1429
|
+
const runs = matching.slice(0, limit ?? 50).map((entry) => ({
|
|
1430
|
+
run_id: String(entry.run_id),
|
|
1431
|
+
timestamp: typeof entry.metadata?.timestamp === 'string'
|
|
1432
|
+
? entry.metadata.timestamp
|
|
1433
|
+
: undefined,
|
|
1434
|
+
config_hash: typeof entry.metadata?.config_hash === 'string'
|
|
1435
|
+
? entry.metadata.config_hash
|
|
1436
|
+
: undefined,
|
|
1437
|
+
summary: entry.summary
|
|
1438
|
+
}));
|
|
1439
|
+
return ok(`Found ${matching.length} run(s)`, {
|
|
1440
|
+
since,
|
|
1441
|
+
until,
|
|
1442
|
+
total_matching: matching.length,
|
|
1443
|
+
runs
|
|
1444
|
+
});
|
|
1445
|
+
}));
|
|
1446
|
+
registerTool('mcplab_queue_run', {
|
|
1447
|
+
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.',
|
|
1448
|
+
inputSchema: {
|
|
1449
|
+
config_path: z
|
|
1450
|
+
.string()
|
|
1451
|
+
.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.'),
|
|
1452
|
+
scenario_id: z.string().optional().describe('Optional single scenario id to run.'),
|
|
1453
|
+
scenario_ids: z
|
|
1454
|
+
.array(z.string())
|
|
1455
|
+
.min(1)
|
|
1456
|
+
.optional()
|
|
1457
|
+
.describe('Optional scenario ids to run.'),
|
|
1458
|
+
agents: z.array(z.string()).min(1).optional().describe('Optional agent ids to use.'),
|
|
1459
|
+
runs_per_scenario: z
|
|
1460
|
+
.number()
|
|
1461
|
+
.int()
|
|
1462
|
+
.positive()
|
|
1463
|
+
.optional()
|
|
1464
|
+
.describe('Runs per scenario (default 1).'),
|
|
1465
|
+
run_note: z
|
|
1466
|
+
.string()
|
|
1467
|
+
.max(500)
|
|
1468
|
+
.optional()
|
|
1469
|
+
.describe('Optional note attached to the queued run.'),
|
|
1470
|
+
server_override_all: z
|
|
1471
|
+
.array(z.string())
|
|
1472
|
+
.min(1)
|
|
1473
|
+
.optional()
|
|
1474
|
+
.describe('Replace MCP servers for every selected scenario for this queued run.'),
|
|
1475
|
+
scenario_server_overrides: z
|
|
1476
|
+
.record(z.array(z.string()))
|
|
1477
|
+
.optional()
|
|
1478
|
+
.describe('Map scenario ids to replacement MCP server ids for this queued run.')
|
|
1479
|
+
},
|
|
1480
|
+
outputSchema: z.object({
|
|
1481
|
+
jobId: z.string().describe('APP queue job identifier.'),
|
|
1482
|
+
queued: z.boolean().optional().describe('Whether the APP accepted the job into its queue.'),
|
|
1483
|
+
position: z
|
|
1484
|
+
.number()
|
|
1485
|
+
.int()
|
|
1486
|
+
.nonnegative()
|
|
1487
|
+
.optional()
|
|
1488
|
+
.describe('Current position in the APP queue.'),
|
|
1489
|
+
queue_url: z.string().url().describe('URL of the APP run queue page.')
|
|
1490
|
+
})
|
|
1491
|
+
}, async (input) => withToolHandling(async () => {
|
|
1492
|
+
const appBaseUrl = process.env.MCPLAB_APP_URL ?? 'http://127.0.0.1:8787';
|
|
1493
|
+
const response = await fetch(new URL('/api/runs', appBaseUrl), {
|
|
1494
|
+
method: 'POST',
|
|
1495
|
+
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
1496
|
+
body: JSON.stringify({
|
|
1497
|
+
configPath: input.config_path,
|
|
1498
|
+
runsPerScenario: input.runs_per_scenario ?? 1,
|
|
1499
|
+
...(input.scenario_id ? { scenarioId: input.scenario_id } : {}),
|
|
1500
|
+
...(input.scenario_ids ? { scenarioIds: input.scenario_ids } : {}),
|
|
1501
|
+
...(input.agents ? { agents: input.agents } : {}),
|
|
1502
|
+
...(input.run_note ? { runNote: input.run_note } : {}),
|
|
1503
|
+
...(input.server_override_all ? { serverOverrideAll: input.server_override_all } : {}),
|
|
1504
|
+
...(input.scenario_server_overrides
|
|
1505
|
+
? { scenarioServerOverrides: input.scenario_server_overrides }
|
|
1506
|
+
: {})
|
|
1507
|
+
})
|
|
1508
|
+
});
|
|
1509
|
+
const payload = (await response.json().catch(() => ({})));
|
|
1510
|
+
if (!response.ok) {
|
|
1511
|
+
throw new Error(typeof payload.error === 'string'
|
|
1512
|
+
? payload.error
|
|
1513
|
+
: `APP queue request failed with HTTP ${response.status}`);
|
|
1514
|
+
}
|
|
1515
|
+
if (typeof payload.jobId !== 'string')
|
|
1516
|
+
throw new Error('APP queue response did not include a jobId');
|
|
1517
|
+
return ok(`Queued MCPLab run ${payload.jobId}`, {
|
|
1518
|
+
...payload,
|
|
1519
|
+
queue_url: new URL('/run', appBaseUrl).toString()
|
|
1520
|
+
});
|
|
1521
|
+
}));
|
|
1120
1522
|
registerTool('mcplab_aggregate_runs', {
|
|
1121
1523
|
description: 'Aggregate metrics across historical MCPLab runs with compact summary-first output.',
|
|
1122
1524
|
outputSchema: z.object({
|
|
@@ -1262,7 +1664,7 @@ export function registerTools(server) {
|
|
|
1262
1664
|
});
|
|
1263
1665
|
});
|
|
1264
1666
|
registerTool('mcplab_search_tool_analysis_results', {
|
|
1265
|
-
description: 'Search saved MCP
|
|
1667
|
+
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
1668
|
outputSchema: {
|
|
1267
1669
|
tool_analysis_results_dir: z.string(),
|
|
1268
1670
|
total: z.number().int().nonnegative(),
|
|
@@ -1525,7 +1927,7 @@ export function registerTools(server) {
|
|
|
1525
1927
|
});
|
|
1526
1928
|
});
|
|
1527
1929
|
registerTool('mcplab_trace_get_conversation', {
|
|
1528
|
-
description: 'Return
|
|
1930
|
+
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
1931
|
outputSchema: {
|
|
1530
1932
|
run_id: z.string(),
|
|
1531
1933
|
scenario_id: z.string(),
|
|
@@ -1557,6 +1959,10 @@ export function registerTools(server) {
|
|
|
1557
1959
|
const { runId, records, legacyDetected } = readScenarioRunTraceRecordsForRun(run_id);
|
|
1558
1960
|
const textMax = max_text_chars ?? 4000;
|
|
1559
1961
|
const record = records.find((r) => r.scenario_id === scenario_id && r.agent === agent);
|
|
1962
|
+
if (!record) {
|
|
1963
|
+
const available = Array.from(new Set(records.map((r) => `${r.scenario_id} / ${r.agent}`)));
|
|
1964
|
+
throw new Error(`No conversation found for ${scenario_id} / ${agent}. Available conversations: ${available.join(', ') || 'none'}`);
|
|
1965
|
+
}
|
|
1560
1966
|
const timeline = record
|
|
1561
1967
|
? buildConversationTimeline(record, textMax).slice(0, max_items ?? 300)
|
|
1562
1968
|
: [];
|
|
@@ -1569,8 +1975,35 @@ export function registerTools(server) {
|
|
|
1569
1975
|
});
|
|
1570
1976
|
});
|
|
1571
1977
|
});
|
|
1978
|
+
registerTool('mcplab_trace_list_conversations', {
|
|
1979
|
+
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.',
|
|
1980
|
+
outputSchema: {
|
|
1981
|
+
run_id: z.string(),
|
|
1982
|
+
legacy_trace_detected: z.boolean().optional(),
|
|
1983
|
+
conversations: z.array(z.object({
|
|
1984
|
+
scenario_id: z.string(),
|
|
1985
|
+
agent: z.string(),
|
|
1986
|
+
timeline_item_count: z.number().int().nonnegative()
|
|
1987
|
+
}))
|
|
1988
|
+
},
|
|
1989
|
+
inputSchema: {
|
|
1990
|
+
run_id: z.string().describe("Run id directory name or 'LATEST'.")
|
|
1991
|
+
}
|
|
1992
|
+
}, async ({ run_id }) => withToolHandling(() => {
|
|
1993
|
+
const { runId, records, legacyDetected } = readScenarioRunTraceRecordsForRun(run_id);
|
|
1994
|
+
const conversations = records.map((record) => ({
|
|
1995
|
+
scenario_id: record.scenario_id,
|
|
1996
|
+
agent: record.agent,
|
|
1997
|
+
timeline_item_count: buildConversationTimeline(record, 4000).length
|
|
1998
|
+
}));
|
|
1999
|
+
return ok(`Found ${conversations.length} conversation(s) in run ${runId}`, {
|
|
2000
|
+
run_id: runId,
|
|
2001
|
+
legacy_trace_detected: legacyDetected || undefined,
|
|
2002
|
+
conversations
|
|
2003
|
+
});
|
|
2004
|
+
}));
|
|
1572
2005
|
registerTool('mcplab_trace_search', {
|
|
1573
|
-
description: 'Search
|
|
2006
|
+
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
2007
|
outputSchema: {
|
|
1575
2008
|
run_id: z.string(),
|
|
1576
2009
|
query: z.string().trim().min(1),
|
|
@@ -1821,7 +2254,7 @@ export function registerTools(server) {
|
|
|
1821
2254
|
});
|
|
1822
2255
|
});
|
|
1823
2256
|
registerTool('mcplab_results_search', {
|
|
1824
|
-
description: 'Search MCPLab
|
|
2257
|
+
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
2258
|
outputSchema: {
|
|
1826
2259
|
query: z.string(),
|
|
1827
2260
|
runs_dir: z.string(),
|
|
@@ -1872,7 +2305,7 @@ export function registerTools(server) {
|
|
|
1872
2305
|
});
|
|
1873
2306
|
});
|
|
1874
2307
|
registerTool('mcplab_results_context', {
|
|
1875
|
-
description: 'Fetch
|
|
2308
|
+
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
2309
|
outputSchema: {
|
|
1877
2310
|
run_id: z.string(),
|
|
1878
2311
|
scenario_id: z.string(),
|
|
@@ -1922,6 +2355,30 @@ export function registerTools(server) {
|
|
|
1922
2355
|
});
|
|
1923
2356
|
});
|
|
1924
2357
|
});
|
|
2358
|
+
registerTool('mcplab_list_skills', {
|
|
2359
|
+
description: 'List MCPLab skills bundled with this MCP server.',
|
|
2360
|
+
inputSchema: {},
|
|
2361
|
+
outputSchema: {
|
|
2362
|
+
skills: z.array(z.object({ name: z.string(), description: z.string(), files: z.array(z.string()) }))
|
|
2363
|
+
}
|
|
2364
|
+
}, async () => withToolHandling(() => {
|
|
2365
|
+
const skills = loadSkills();
|
|
2366
|
+
return ok(`Found ${skills.length} MCPLab skill(s)`, {
|
|
2367
|
+
skills: skills.map(({ name, description, files }) => ({ name, description, files }))
|
|
2368
|
+
});
|
|
2369
|
+
}));
|
|
2370
|
+
registerTool('mcplab_get_skill', {
|
|
2371
|
+
description: 'Read one file from a bundled MCPLab skill. Defaults to SKILL.md.',
|
|
2372
|
+
inputSchema: { name: z.string(), file: z.string().optional() },
|
|
2373
|
+
outputSchema: { name: z.string(), file: z.string(), content: z.string() }
|
|
2374
|
+
}, async ({ name, file }) => withToolHandling(() => {
|
|
2375
|
+
const targetFile = file ?? 'SKILL.md';
|
|
2376
|
+
return ok(`Read ${name}/${targetFile}`, {
|
|
2377
|
+
name,
|
|
2378
|
+
file: targetFile,
|
|
2379
|
+
content: readSkillFile(name, targetFile)
|
|
2380
|
+
});
|
|
2381
|
+
}));
|
|
1925
2382
|
}
|
|
1926
2383
|
export function registerPrompts(server) {
|
|
1927
2384
|
server.registerPrompt('mcplab-scenario-author', {
|
|
@@ -1988,17 +2445,33 @@ export function registerPrompts(server) {
|
|
|
1988
2445
|
const DESTRUCTIVE_TOOLS = new Set([
|
|
1989
2446
|
'mcplab_delete_tool_analysis_result',
|
|
1990
2447
|
'mcplab_write_markdown_report',
|
|
1991
|
-
'mcplab_run_eval'
|
|
2448
|
+
'mcplab_run_eval',
|
|
2449
|
+
'mcplab_queue_run',
|
|
2450
|
+
'mcplab_create_test_case',
|
|
2451
|
+
'mcplab_update_test_case',
|
|
2452
|
+
'mcplab_create_evaluation_config',
|
|
2453
|
+
'mcplab_update_evaluation_config'
|
|
1992
2454
|
]);
|
|
1993
|
-
const MUTATING_TOOLS = new Set([
|
|
1994
|
-
|
|
2455
|
+
const MUTATING_TOOLS = new Set([
|
|
2456
|
+
'mcplab_write_markdown_report',
|
|
2457
|
+
'mcplab_run_eval',
|
|
2458
|
+
'mcplab_queue_run',
|
|
2459
|
+
'mcplab_create_test_case',
|
|
2460
|
+
'mcplab_update_test_case',
|
|
2461
|
+
'mcplab_create_evaluation_config',
|
|
2462
|
+
'mcplab_update_evaluation_config'
|
|
2463
|
+
]);
|
|
2464
|
+
const OPEN_WORLD_TOOLS = new Set(['mcplab_run_eval', 'mcplab_queue_run']);
|
|
1995
2465
|
const PREFERRED_TOOL_TITLES = {
|
|
1996
2466
|
mcplab_write_markdown_report: 'Write Markdown Report to Disk',
|
|
1997
2467
|
mcplab_search_markdown_reports: 'Search Markdown Reports',
|
|
1998
2468
|
mcplab_list_library: 'Search Library Entries',
|
|
1999
2469
|
mcplab_generate_agent_entry: 'Generate MCPLab agents.yaml Entry',
|
|
2000
2470
|
mcplab_search_tool_analysis_results: 'Search Tool Analysis Results',
|
|
2001
|
-
mcplab_trace_search: 'Search Trace Events'
|
|
2471
|
+
mcplab_trace_search: 'Search Raw Trace Events',
|
|
2472
|
+
mcplab_results_search: 'Search Indexed Evaluation Results',
|
|
2473
|
+
mcplab_results_context: 'Fetch Focused Result or Trace Context',
|
|
2474
|
+
mcplab_queue_run: 'Queue MCPLab Evaluation in APP'
|
|
2002
2475
|
};
|
|
2003
2476
|
function normalizeOptionalNonEmpty(value) {
|
|
2004
2477
|
if (typeof value !== 'string')
|
|
@@ -2089,7 +2562,7 @@ function mergeLibraryServersAndAgents(config, bundleRoot) {
|
|
|
2089
2562
|
function readLibrary(bundleRoot, includeContent) {
|
|
2090
2563
|
const serversPath = join(bundleRoot, 'servers.yaml');
|
|
2091
2564
|
const agentsPath = join(bundleRoot, 'agents.yaml');
|
|
2092
|
-
const scenariosDir =
|
|
2565
|
+
const scenariosDir = resolveScenarioLibraryDir(bundleRoot).path;
|
|
2093
2566
|
const servers = existsSync(serversPath)
|
|
2094
2567
|
? parseYaml(readFileSync(serversPath, 'utf8')) ?? {}
|
|
2095
2568
|
: {};
|
|
@@ -2126,6 +2599,7 @@ function readLibrary(bundleRoot, includeContent) {
|
|
|
2126
2599
|
id,
|
|
2127
2600
|
...(includeContent ? { entry: agents[id] } : {})
|
|
2128
2601
|
})),
|
|
2602
|
+
test_cases: scenarioEntries,
|
|
2129
2603
|
scenarios: scenarioEntries
|
|
2130
2604
|
};
|
|
2131
2605
|
return out;
|
|
@@ -2150,7 +2624,7 @@ function getLibraryItem(bundleRoot, kind, id) {
|
|
|
2150
2624
|
content: entry
|
|
2151
2625
|
};
|
|
2152
2626
|
}
|
|
2153
|
-
const dir =
|
|
2627
|
+
const dir = resolveScenarioLibraryDir(bundleRoot).path;
|
|
2154
2628
|
if (!existsSync(dir)) {
|
|
2155
2629
|
throw new Error(`Scenario library directory not found: ${dir}`);
|
|
2156
2630
|
}
|
|
@@ -2172,6 +2646,93 @@ function getLibraryItem(bundleRoot, kind, id) {
|
|
|
2172
2646
|
}
|
|
2173
2647
|
throw new Error(`Scenario '${id}' not found in ${dir}`);
|
|
2174
2648
|
}
|
|
2649
|
+
function listEvaluationConfigs(params) {
|
|
2650
|
+
const evalsDir = join(resolveBundleRoot(), 'evals');
|
|
2651
|
+
const files = [];
|
|
2652
|
+
const walk = (dir) => {
|
|
2653
|
+
if (!existsSync(dir))
|
|
2654
|
+
return;
|
|
2655
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
2656
|
+
const path = join(dir, entry.name);
|
|
2657
|
+
if (entry.isDirectory())
|
|
2658
|
+
walk(path);
|
|
2659
|
+
else if (entry.isFile() && /\.ya?ml$/i.test(entry.name))
|
|
2660
|
+
files.push(path);
|
|
2661
|
+
}
|
|
2662
|
+
};
|
|
2663
|
+
walk(evalsDir);
|
|
2664
|
+
const all = files.map((path) => {
|
|
2665
|
+
const relativePath = relative(evalsDir, path).split(sep).join('/');
|
|
2666
|
+
const suitePath = dirname(relativePath) === '.' ? '' : dirname(relativePath).split(sep).join('/');
|
|
2667
|
+
const fallbackName = basename(path, extname(path));
|
|
2668
|
+
const stat = statSync(path);
|
|
2669
|
+
try {
|
|
2670
|
+
const parsed = parseYaml(readFileSync(path, 'utf8'));
|
|
2671
|
+
return {
|
|
2672
|
+
id: Buffer.from(relativePath, 'utf8').toString('base64url'),
|
|
2673
|
+
name: typeof parsed?.name === 'string' && parsed.name.trim()
|
|
2674
|
+
? parsed.name.trim()
|
|
2675
|
+
: fallbackName,
|
|
2676
|
+
relative_path: relativePath,
|
|
2677
|
+
config_path: path,
|
|
2678
|
+
suite_path: suitePath,
|
|
2679
|
+
updated_at: stat.mtime.toISOString(),
|
|
2680
|
+
scenario_count: Array.isArray(parsed?.scenarios) ? parsed.scenarios.length : 0,
|
|
2681
|
+
agent_count: Array.isArray(parsed?.agents) ? parsed.agents.length : 0
|
|
2682
|
+
};
|
|
2683
|
+
}
|
|
2684
|
+
catch (error) {
|
|
2685
|
+
return {
|
|
2686
|
+
id: Buffer.from(relativePath, 'utf8').toString('base64url'),
|
|
2687
|
+
name: fallbackName,
|
|
2688
|
+
relative_path: relativePath,
|
|
2689
|
+
config_path: path,
|
|
2690
|
+
suite_path: suitePath,
|
|
2691
|
+
updated_at: stat.mtime.toISOString(),
|
|
2692
|
+
scenario_count: 0,
|
|
2693
|
+
agent_count: 0,
|
|
2694
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2695
|
+
};
|
|
2696
|
+
}
|
|
2697
|
+
});
|
|
2698
|
+
const suite = params.suite?.trim().replace(/^suite:/, '');
|
|
2699
|
+
const query = params.query?.trim().toLowerCase();
|
|
2700
|
+
const matching = all.filter((config) => (!suite || config.suite_path === suite || config.suite_path.startsWith(`${suite}/`)) &&
|
|
2701
|
+
(!query ||
|
|
2702
|
+
`${config.name} ${config.relative_path} ${config.error ?? ''}`
|
|
2703
|
+
.toLowerCase()
|
|
2704
|
+
.includes(query)));
|
|
2705
|
+
const sortBy = params.sortBy ?? 'name';
|
|
2706
|
+
const direction = params.sortDirection ?? 'asc';
|
|
2707
|
+
matching.sort((a, b) => {
|
|
2708
|
+
const left = sortBy === 'scenarios'
|
|
2709
|
+
? a.scenario_count
|
|
2710
|
+
: sortBy === 'agents'
|
|
2711
|
+
? a.agent_count
|
|
2712
|
+
: sortBy === 'updated_at'
|
|
2713
|
+
? Date.parse(a.updated_at)
|
|
2714
|
+
: a.name;
|
|
2715
|
+
const right = sortBy === 'scenarios'
|
|
2716
|
+
? b.scenario_count
|
|
2717
|
+
: sortBy === 'agents'
|
|
2718
|
+
? b.agent_count
|
|
2719
|
+
: sortBy === 'updated_at'
|
|
2720
|
+
? Date.parse(b.updated_at)
|
|
2721
|
+
: b.name;
|
|
2722
|
+
const comparison = typeof left === 'number' && typeof right === 'number'
|
|
2723
|
+
? left - right
|
|
2724
|
+
: String(left).localeCompare(String(right));
|
|
2725
|
+
return direction === 'asc' ? comparison : -comparison;
|
|
2726
|
+
});
|
|
2727
|
+
return {
|
|
2728
|
+
evals_dir: evalsDir,
|
|
2729
|
+
total: all.length,
|
|
2730
|
+
matching: matching.length,
|
|
2731
|
+
filters: removeUndefined({ suite, query }),
|
|
2732
|
+
sort: { by: sortBy, direction },
|
|
2733
|
+
configs: matching
|
|
2734
|
+
};
|
|
2735
|
+
}
|
|
2175
2736
|
function buildServerEntry(input) {
|
|
2176
2737
|
const transport = input.transport ?? 'http';
|
|
2177
2738
|
const authType = input.auth_type ?? 'none';
|
|
@@ -2220,46 +2781,7 @@ function buildServerEntry(input) {
|
|
|
2220
2781
|
};
|
|
2221
2782
|
}
|
|
2222
2783
|
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;
|
|
2784
|
+
return buildScenarioEntry(input);
|
|
2263
2785
|
}
|
|
2264
2786
|
function validateScenarioHeuristics(scenario) {
|
|
2265
2787
|
const warnings = [];
|
|
@@ -2981,19 +3503,6 @@ function searchableText(value) {
|
|
|
2981
3503
|
visit(value);
|
|
2982
3504
|
return tokens.join(' ');
|
|
2983
3505
|
}
|
|
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
3506
|
function indentBlock(text, spaces) {
|
|
2998
3507
|
const prefix = ' '.repeat(spaces);
|
|
2999
3508
|
return text
|
|
@@ -3033,7 +3542,14 @@ async function handleHttpRequest(req, res, sessions, mcpPath) {
|
|
|
3033
3542
|
await runtime.transport.handleRequest(req, res, body);
|
|
3034
3543
|
return;
|
|
3035
3544
|
}
|
|
3036
|
-
|
|
3545
|
+
// Per the MCP Streamable HTTP transport, an unknown session indicates that
|
|
3546
|
+
// the server was restarted or the session expired. The client must receive
|
|
3547
|
+
// 404 so it can discard the stale session and initialize a new connection.
|
|
3548
|
+
if (sessionId) {
|
|
3549
|
+
sendPlain(res, 404, 'Not Found');
|
|
3550
|
+
return;
|
|
3551
|
+
}
|
|
3552
|
+
if (isInitializeRequest(body)) {
|
|
3037
3553
|
let runtime;
|
|
3038
3554
|
const transport = new StreamableHTTPServerTransport({
|
|
3039
3555
|
sessionIdGenerator: () => randomUUID(),
|
|
@@ -3067,7 +3583,11 @@ async function handleHttpRequest(req, res, sessions, mcpPath) {
|
|
|
3067
3583
|
}
|
|
3068
3584
|
if (method === 'GET' || method === 'DELETE') {
|
|
3069
3585
|
const sessionId = getSessionId(req);
|
|
3070
|
-
if (
|
|
3586
|
+
if (sessionId && !sessions.has(sessionId)) {
|
|
3587
|
+
sendPlain(res, 404, 'Not Found');
|
|
3588
|
+
return;
|
|
3589
|
+
}
|
|
3590
|
+
if (!sessionId) {
|
|
3071
3591
|
sendPlain(res, 400, 'Invalid or missing session ID');
|
|
3072
3592
|
return;
|
|
3073
3593
|
}
|