@inspectr/mcplab-mcp-server 1.8.3 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/runtime.d.ts CHANGED
@@ -2,34 +2,28 @@ import { type IncomingMessage, type ServerResponse } from 'node:http';
2
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
4
4
  export type SessionRuntime = {
5
- transport: StreamableHTTPServerTransport;
6
- server: McpServer;
5
+ transport: StreamableHTTPServerTransport;
6
+ server: McpServer;
7
7
  };
8
8
  export interface McplabMcpServerOptions {
9
- host: string;
10
- port: number;
11
- path: string;
12
- logger?: Pick<Console, 'log' | 'error'>;
9
+ host: string;
10
+ port: number;
11
+ path: string;
12
+ logger?: Pick<Console, 'log' | 'error'>;
13
13
  }
14
14
  export interface McplabMcpServerRuntime {
15
- host: string;
16
- port: number;
17
- path: string;
18
- close(): Promise<void>;
15
+ host: string;
16
+ port: number;
17
+ path: string;
18
+ close(): Promise<void>;
19
19
  }
20
- export declare function startMcplabMcpServer(
21
- options: McplabMcpServerOptions
22
- ): Promise<McplabMcpServerRuntime>;
20
+ export declare function startMcplabMcpServer(options: McplabMcpServerOptions): Promise<McplabMcpServerRuntime>;
23
21
  export declare function defaultMcplabMcpServerOptionsFromEnv(): McplabMcpServerOptions;
24
22
  export declare function createConfiguredServer(): McpServer;
25
23
  export declare function registerTools(server: McpServer): void;
26
24
  export declare function registerPrompts(server: McpServer): void;
27
- export declare function handleMcplabMcpHttpRequest(
28
- req: IncomingMessage,
29
- res: ServerResponse,
30
- sessions: Map<string, SessionRuntime>,
31
- options?: {
25
+ export declare function handleMcplabMcpHttpRequest(req: IncomingMessage, res: ServerResponse, sessions: Map<string, SessionRuntime>, options?: {
32
26
  path?: string;
33
- }
34
- ): Promise<void>;
35
- //# sourceMappingURL=runtime.d.ts.map
27
+ logger?: Pick<Console, 'log' | 'error'>;
28
+ }): Promise<void>;
29
+ //# sourceMappingURL=runtime.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAUA,OAAO,EAAgB,KAAK,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAC;AAEpF,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,6BAA6B,EAAE,MAAM,oDAAoD,CAAC;AA4tBnG,MAAM,MAAM,cAAc,GAAG;IAC3B,SAAS,EAAE,6BAA6B,CAAC;IACzC,MAAM,EAAE,SAAS,CAAC;CACnB,CAAC;AAEF,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,KAAK,GAAG,OAAO,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,sBAAsB,CAAC,CAoDjC;AAED,wBAAgB,oCAAoC,IAAI,sBAAsB,CAM7E;AAED,wBAAgB,sBAAsB,IAAI,SAAS,CAclD;AA8CD,wBAAgB,aAAa,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA4vErD;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA0EvD;AAwsCD,wBAAsB,0BAA0B,CAC9C,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,EACrC,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1B,OAAO,CAAC,IAAI,CAAC,CAEf"}
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAUA,OAAO,EAAgB,KAAK,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAC;AAEpF,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,6BAA6B,EAAE,MAAM,oDAAoD,CAAC;AAkvBnG,MAAM,MAAM,cAAc,GAAG;IAC3B,SAAS,EAAE,6BAA6B,CAAC;IACzC,MAAM,EAAE,SAAS,CAAC;CACnB,CAAC;AAEF,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,KAAK,GAAG,OAAO,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,sBAAsB,CAAC,CAoDjC;AAED,wBAAgB,oCAAoC,IAAI,sBAAsB,CAM7E;AAED,wBAAgB,sBAAsB,IAAI,SAAS,CAclD;AA8CD,wBAAgB,aAAa,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA0zErD;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA0EvD;AAqvCD,wBAAsB,0BAA0B,CAC9C,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,EACrC,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,KAAK,GAAG,OAAO,CAAC,CAAA;CAAE,GACnE,OAAO,CAAC,IAAI,CAAC,CAQf"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=runtime.decision.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.decision.test.d.ts","sourceRoot":"","sources":["../src/runtime.decision.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,141 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { aggregateResults } from '@inspectr/mcplab-core';
3
+ const mocks = vi.hoisted(() => ({
4
+ runAll: vi.fn(),
5
+ loadConfig: vi.fn(),
6
+ loadWorkspaceSettingsOverrides: vi.fn(),
7
+ readLibraryAgentsAndServers: vi.fn()
8
+ }));
9
+ vi.mock('@inspectr/mcplab-core', async (importOriginal) => ({
10
+ ...(await importOriginal()),
11
+ ...mocks
12
+ }));
13
+ import { registerTools } from './runtime.js';
14
+ describe('immediate MCP decision evaluation', () => {
15
+ const agent = { provider: 'openai', model: 'gpt-5-mini' };
16
+ const decision = {
17
+ provider: 'ollama',
18
+ model: 'clef-flash',
19
+ passThreshold: 0.9,
20
+ failThreshold: 0.1,
21
+ baseUrl: 'http://localhost:11434'
22
+ };
23
+ let runTool;
24
+ beforeEach(() => {
25
+ vi.clearAllMocks();
26
+ mocks.loadConfig.mockReturnValue({
27
+ config: {
28
+ name: 'test',
29
+ servers: {},
30
+ agents: {},
31
+ scenarios: [
32
+ {
33
+ id: 'semantic',
34
+ servers: [],
35
+ prompt: 'Answer',
36
+ eval: { agent_assertions: [{ label: 'Check', prompt: 'Valid?' }] }
37
+ }
38
+ ]
39
+ },
40
+ hash: 'test'
41
+ });
42
+ mocks.readLibraryAgentsAndServers.mockReturnValue({ agents: { judge: agent }, servers: {} });
43
+ mocks.runAll.mockResolvedValue({
44
+ runDir: '/tmp/mcplab-mcp-test',
45
+ results: aggregateResults({
46
+ runId: 'test',
47
+ timestamp: '2026-10-13T00:00:00Z',
48
+ configHash: 'test',
49
+ cliVersion: 'test',
50
+ scenarioRuns: []
51
+ })
52
+ });
53
+ registerTools({
54
+ registerTool: (name, _config, cb) => {
55
+ if (name === 'mcplab_run_eval')
56
+ runTool = cb;
57
+ return { name };
58
+ }
59
+ });
60
+ });
61
+ it.each(['deleted', 'browser'])('runs deterministic checks despite a %s workspace judge', async (name) => {
62
+ mocks.loadConfig.mockReturnValue({
63
+ config: {
64
+ servers: {},
65
+ agents: {},
66
+ scenarios: [
67
+ {
68
+ id: 'deterministic',
69
+ servers: [],
70
+ prompt: 'Answer',
71
+ eval: { response_assertions: [{ type: 'contains', value: 'done' }] }
72
+ }
73
+ ]
74
+ }
75
+ });
76
+ mocks.loadWorkspaceSettingsOverrides.mockReturnValue({
77
+ evaluation_judge_agent_name: name,
78
+ evaluation_decision: decision
79
+ });
80
+ mocks.readLibraryAgentsAndServers.mockReturnValue({
81
+ agents: {
82
+ main: agent,
83
+ browser: { type: 'browser', provider: 'polo', url: 'https://polo.example' }
84
+ },
85
+ servers: {}
86
+ });
87
+ const result = await runTool({ config_path: '/tmp/eval.yaml' });
88
+ expect(result.isError).not.toBe(true);
89
+ const options = mocks.runAll.mock.calls[0][1];
90
+ expect(options.evaluationJudge).toBeUndefined();
91
+ expect(options.evaluationDecision).toBeUndefined();
92
+ });
93
+ it('uses shared workspace decision configuration and LLM judge backup', async () => {
94
+ mocks.loadWorkspaceSettingsOverrides.mockReturnValue({
95
+ evaluation_judge_agent_name: 'judge',
96
+ evaluation_decision: decision
97
+ });
98
+ const result = await runTool({ config_path: '/tmp/eval.yaml' });
99
+ expect(result.isError).not.toBe(true);
100
+ expect(mocks.runAll).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({
101
+ evaluationDecision: decision,
102
+ evaluationJudge: { name: 'judge', agent }
103
+ }));
104
+ expect(mocks.loadWorkspaceSettingsOverrides).toHaveBeenCalledWith(mocks.readLibraryAgentsAndServers.mock.calls[0][0]);
105
+ });
106
+ it('preserves legacy judge-only workspaces', async () => {
107
+ mocks.loadWorkspaceSettingsOverrides.mockReturnValue({ evaluation_judge_agent_name: 'judge' });
108
+ await runTool({ config_path: '/tmp/eval.yaml' });
109
+ expect(mocks.runAll).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({
110
+ evaluationDecision: undefined,
111
+ evaluationJudge: { name: 'judge', agent }
112
+ }));
113
+ });
114
+ it('passes malformed persisted decision settings through for shared fallback handling', async () => {
115
+ const malformed = { provider: 'missing', passThreshold: 2 };
116
+ mocks.loadWorkspaceSettingsOverrides.mockReturnValue({
117
+ evaluation_judge_agent_name: 'judge',
118
+ evaluation_decision: malformed
119
+ });
120
+ await runTool({ config_path: '/tmp/eval.yaml' });
121
+ expect(mocks.runAll).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({
122
+ evaluationDecision: malformed,
123
+ evaluationJudge: { name: 'judge', agent }
124
+ }));
125
+ });
126
+ it('rejects a browser agent selected as judge before starting execution', async () => {
127
+ mocks.loadWorkspaceSettingsOverrides.mockReturnValue({
128
+ evaluation_judge_agent_name: 'judge',
129
+ evaluation_decision: decision
130
+ });
131
+ mocks.readLibraryAgentsAndServers.mockReturnValue({
132
+ agents: { judge: { type: 'browser', provider: 'polo', url: 'https://polo.example' } },
133
+ servers: {}
134
+ });
135
+ const result = await runTool({ config_path: '/tmp/eval.yaml' });
136
+ expect(result.isError).toBe(true);
137
+ expect(result.content[0].text).toContain('LLM');
138
+ expect(mocks.runAll).not.toHaveBeenCalled();
139
+ });
140
+ });
141
+ //# sourceMappingURL=runtime.decision.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.decision.test.js","sourceRoot":"","sources":["../src/runtime.decision.test.ts"],"names":[],"mappings":"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"}
package/dist/runtime.js CHANGED
@@ -5,7 +5,7 @@ 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, resolveScenarioLibraryDir, createEvaluationConfigFile, updateEvaluationConfigFile, readEvaluationConfigFile, createTestCaseFile, readTestCaseFile, updateTestCaseFile, buildScenarioEntry } from '@inspectr/mcplab-core';
8
+ import { loadConfig, hashConfig, runAll, loadWorkspaceSettingsOverrides, toWorkspaceEvaluationSettings, resolveWorkspaceEvaluation, selectScenarios, loadOrBuildSearchIndex, indexNeedsRefresh, getResultsIndexPaths, resolveRunArtifactPath, searchDocs, getContext, applyRuntimeServerOverrides, readLibraryAgentsAndServers, readBrowserProviderProfiles, serializeBrowserProviderProfile, resolveScenarioLibraryDir, createEvaluationConfigFile, updateEvaluationConfigFile, readEvaluationConfigFile, createTestCaseFile, readTestCaseFile, updateTestCaseFile, buildScenarioEntry, DEFAULT_BROWSER_PROVIDER_PROFILES, mergeBrowserProviderProfiles } 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';
@@ -25,7 +25,7 @@ const GenericObjectSchema = z.object({}).passthrough();
25
25
  const ResultsSummarySchema = z.object({
26
26
  total_scenarios: z.number().int().nonnegative(),
27
27
  total_runs: z.number().int().nonnegative(),
28
- pass_rate: z.number(),
28
+ pass_rate: z.number().nullable(),
29
29
  avg_tool_calls_per_run: z.number(),
30
30
  avg_tool_latency_ms: z.number().nullable()
31
31
  });
@@ -44,7 +44,8 @@ const MetricSummarySchema = z.object({
44
44
  total_runs: z.number().int().nonnegative(),
45
45
  passed_runs: z.number().int().nonnegative(),
46
46
  failed_runs: z.number().int().nonnegative(),
47
- pass_rate: z.number(),
47
+ stopped_runs: z.number().int().nonnegative().optional(),
48
+ pass_rate: z.number().nullable(),
48
49
  avg_tool_calls_per_run: z.number(),
49
50
  avg_tool_latency_ms: z.number().nullable()
50
51
  });
@@ -66,7 +67,7 @@ const CompareRowSchema = z.object({
66
67
  key: z.string(),
67
68
  scenario_id: z.string(),
68
69
  agent: z.string(),
69
- classification: z.enum(['regressed', 'improved', 'unchanged', 'new', 'missing']),
70
+ classification: z.enum(['regressed', 'improved', 'unchanged', 'new', 'missing', 'not_evaluated']),
70
71
  left: MetricSummarySchema.nullable(),
71
72
  right: MetricSummarySchema.nullable(),
72
73
  deltas: z.object({
@@ -90,12 +91,19 @@ const LibraryScenarioEntrySchema = z.object({
90
91
  yaml: z.string().optional()
91
92
  });
92
93
  const AgentEntrySchema = z.object({
93
- provider: z.enum(['openai', 'anthropic', 'azure_openai']),
94
+ provider: z.enum(['openai', 'anthropic', 'azure_openai', 'azure_foundry']),
94
95
  model: z.string(),
95
96
  temperature: z.number().optional(),
96
97
  max_tokens: z.number().int().positive().optional(),
97
98
  system: z.string().optional()
98
99
  });
100
+ const BrowserAgentEntrySchema = z.object({
101
+ type: z.literal('browser'),
102
+ name: z.string().optional(),
103
+ provider: z.string(),
104
+ url: z.string(),
105
+ new_conversation_between_scenarios: z.boolean().optional()
106
+ });
99
107
  const LibraryServerEntryContentSchema = z
100
108
  .object({
101
109
  transport: z.string().optional(),
@@ -112,12 +120,17 @@ const LibraryServerEntrySchema = z.object({
112
120
  });
113
121
  const LibraryAgentEntrySchema = z.object({
114
122
  id: z.string(),
115
- entry: AgentEntrySchema.optional()
123
+ entry: z.union([AgentEntrySchema, BrowserAgentEntrySchema]).optional()
124
+ });
125
+ const LibraryBrowserProviderEntrySchema = z.object({
126
+ id: z.string(),
127
+ entry: GenericObjectSchema.optional()
116
128
  });
117
129
  const LibraryEntrySchema = z.object({
118
130
  bundleRoot: z.string(),
119
131
  servers: z.array(LibraryServerEntrySchema),
120
132
  agents: z.array(LibraryAgentEntrySchema),
133
+ browser_providers: z.array(LibraryBrowserProviderEntrySchema),
121
134
  test_cases: z.array(LibraryScenarioEntrySchema),
122
135
  scenarios: z.array(LibraryScenarioEntrySchema)
123
136
  });
@@ -615,7 +628,7 @@ export async function startMcplabMcpServer(options) {
615
628
  const sessions = new Map();
616
629
  const httpServer = createServer(async (req, res) => {
617
630
  try {
618
- await handleHttpRequest(req, res, sessions, options.path);
631
+ await handleHttpRequest(req, res, sessions, options.path, logger);
619
632
  }
620
633
  catch (error) {
621
634
  logger.error('[mcplab-mcp] request error:', error);
@@ -1081,7 +1094,7 @@ export function registerTools(server) {
1081
1094
  outputSchema: LibraryEntrySchema,
1082
1095
  inputSchema: {
1083
1096
  kind: z
1084
- .enum(['all', 'servers', 'agents', 'test_cases', 'scenarios'])
1097
+ .enum(['all', 'servers', 'agents', 'browser_providers', 'test_cases', 'scenarios'])
1085
1098
  .optional()
1086
1099
  .describe('Which library category to list. Defaults to all.'),
1087
1100
  includeContent: z
@@ -1091,15 +1104,16 @@ export function registerTools(server) {
1091
1104
  }
1092
1105
  }, async ({ kind, includeContent }) => {
1093
1106
  return withToolHandling(async () => {
1094
- const root = resolveBundleRoot();
1095
- const data = readLibrary(root, Boolean(includeContent));
1096
1107
  const selectedKind = kind ?? 'all';
1108
+ const root = resolveBundleRoot();
1109
+ const data = readLibrary(root, Boolean(includeContent), selectedKind);
1097
1110
  const structured = selectedKind === 'all'
1098
1111
  ? data
1099
1112
  : {
1100
1113
  bundleRoot: data.bundleRoot,
1101
1114
  servers: selectedKind === 'servers' ? data.servers : [],
1102
1115
  agents: selectedKind === 'agents' ? data.agents : [],
1116
+ browser_providers: selectedKind === 'browser_providers' ? data.browser_providers : [],
1103
1117
  test_cases: selectedKind === 'test_cases' ? data.test_cases : [],
1104
1118
  scenarios: selectedKind === 'scenarios' ? data.scenarios : []
1105
1119
  };
@@ -1107,10 +1121,10 @@ export function registerTools(server) {
1107
1121
  });
1108
1122
  });
1109
1123
  registerTool('mcplab_get_library_item', {
1110
- description: 'Get a specific reusable server, agent, or scenario definition from a MCPLab library bundle and return both structured data and YAML.',
1124
+ description: 'Get a specific reusable server, agent, browser provider, test case, or scenario definition from a MCPLab library bundle and return both structured data and YAML.',
1111
1125
  outputSchema: {
1112
1126
  bundleRoot: z.string(),
1113
- kind: z.enum(['servers', 'agents', 'test_cases', 'scenarios']),
1127
+ kind: z.enum(['servers', 'agents', 'browser_providers', 'test_cases', 'scenarios']),
1114
1128
  id: z.string(),
1115
1129
  file: z.string().optional(),
1116
1130
  yaml: z.string(),
@@ -1118,7 +1132,7 @@ export function registerTools(server) {
1118
1132
  },
1119
1133
  inputSchema: {
1120
1134
  kind: z
1121
- .enum(['servers', 'agents', 'test_cases', 'scenarios'])
1135
+ .enum(['servers', 'agents', 'browser_providers', 'test_cases', 'scenarios'])
1122
1136
  .describe('Library category.'),
1123
1137
  id: z.string().describe('Entry id (for scenarios this is scenario.id, not filename).')
1124
1138
  }
@@ -1158,9 +1172,11 @@ export function registerTools(server) {
1158
1172
  inputSchema: {
1159
1173
  id: z.string().describe('Agent id key (kebab-case recommended).'),
1160
1174
  provider: z
1161
- .enum(['openai', 'anthropic', 'azure_openai'])
1175
+ .enum(['openai', 'anthropic', 'azure_openai', 'azure_foundry'])
1162
1176
  .describe('LLM provider supported by MCPLab.'),
1163
- model: z.string().describe('Model id or deployment name (for Azure OpenAI).'),
1177
+ model: z
1178
+ .string()
1179
+ .describe('Model id or deployment name (for Azure OpenAI and Azure Foundry).'),
1164
1180
  temperature: z.number().optional().describe('Sampling temperature.'),
1165
1181
  max_tokens: z.number().int().positive().optional().describe('Maximum output tokens.'),
1166
1182
  system: z.string().optional().describe('Optional system prompt.')
@@ -1175,6 +1191,46 @@ export function registerTools(server) {
1175
1191
  });
1176
1192
  });
1177
1193
  });
1194
+ registerTool('mcplab_generate_browser_agent_entry', {
1195
+ description: 'Generate an agents.yaml entry for a Rover browser agent. Browser agents reference a validated browser provider profile and are executed by Rover, not by an LLM API.',
1196
+ outputSchema: {
1197
+ id: z.string(),
1198
+ entry: z.object({
1199
+ type: z.literal('browser'),
1200
+ name: z.string().optional(),
1201
+ provider: z.string(),
1202
+ url: z.string(),
1203
+ new_conversation_between_scenarios: z.boolean().optional()
1204
+ }),
1205
+ yaml: z.string()
1206
+ },
1207
+ inputSchema: {
1208
+ id: z.string().describe('Browser agent id key (kebab-case recommended).'),
1209
+ name: z.string().optional().describe('Human-readable browser agent name.'),
1210
+ provider: z
1211
+ .string()
1212
+ .min(1)
1213
+ .describe('Provider id from browser-providers/<provider-id>.yaml.'),
1214
+ url: z.string().url().describe('Browser origin where Rover should execute the agent.'),
1215
+ new_conversation_between_scenarios: z
1216
+ .boolean()
1217
+ .optional()
1218
+ .describe('Whether Rover should start a new conversation between scenarios.')
1219
+ }
1220
+ }, async ({ id, name, provider, url, new_conversation_between_scenarios }) => withToolHandling(async () => {
1221
+ const entry = removeUndefined({
1222
+ type: 'browser',
1223
+ name,
1224
+ provider,
1225
+ url,
1226
+ new_conversation_between_scenarios
1227
+ });
1228
+ return ok(`Generated browser agent entry '${id}'`, {
1229
+ id,
1230
+ entry,
1231
+ yaml: stringifyYaml({ [id]: entry }).trimEnd()
1232
+ });
1233
+ }));
1178
1234
  registerTool('mcplab_create_test_case', {
1179
1235
  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.',
1180
1236
  outputSchema: { id: z.string(), path: z.string(), test_case: ScenarioEntrySchema },
@@ -1529,7 +1585,7 @@ export function registerTools(server) {
1529
1585
  const loaded = loadConfig(resolve(config_path), {
1530
1586
  bundleRoot
1531
1587
  });
1532
- const mergedConfig = mergeLibraryServersAndAgents(loaded.config, bundleRoot);
1588
+ const mergedConfig = mergeLibraryServersAndAgents(loaded.config, readLibraryAgentsAndServers(bundleRoot));
1533
1589
  const selected = selectScenarios(mergedConfig, scenario_id);
1534
1590
  const summary = summarizeConfig(selected);
1535
1591
  return ok(`Validated config ${config_path}`, {
@@ -1556,7 +1612,7 @@ export function registerTools(server) {
1556
1612
  scenarios: z.array(z.object({
1557
1613
  scenario_id: z.string(),
1558
1614
  agent: z.string(),
1559
- pass_rate: z.number(),
1615
+ pass_rate: z.number().nullable(),
1560
1616
  tool_usage_frequency: z.record(z.number())
1561
1617
  })),
1562
1618
  effective_server_overrides: z.record(z.array(z.string())),
@@ -1586,7 +1642,8 @@ export function registerTools(server) {
1586
1642
  const loaded = loadConfig(resolve(config_path), {
1587
1643
  bundleRoot
1588
1644
  });
1589
- const mergedConfig = mergeLibraryServersAndAgents(loaded.config, bundleRoot);
1645
+ const libraries = readLibraryAgentsAndServers(bundleRoot);
1646
+ const mergedConfig = mergeLibraryServersAndAgents(loaded.config, libraries);
1590
1647
  const selected = selectScenarios(mergedConfig, scenario_id);
1591
1648
  const runtimeOverridden = applyRuntimeServerOverrides(selected, {
1592
1649
  serverOverrideAll: server_override_all,
@@ -1594,13 +1651,19 @@ export function registerTools(server) {
1594
1651
  });
1595
1652
  const effectiveConfigHash = hashConfig(runtimeOverridden);
1596
1653
  const executable = expandConfigForAgents(runtimeOverridden, runtimeOverridden.run_defaults?.selected_agents);
1654
+ const workspaceSettings = loadWorkspaceSettingsOverrides(bundleRoot);
1597
1655
  const { runDir, results } = await runAll(executable, {
1598
1656
  runsPerScenario: runs_per_scenario ?? 1,
1599
1657
  scenarioId: scenario_id,
1600
1658
  configHash: effectiveConfigHash,
1601
1659
  cliVersion: `mcplab-mcp-server/${SERVER_VERSION}`,
1602
1660
  runsDir: resolveRunsDir(),
1603
- cwd: process.cwd()
1661
+ cwd: process.cwd(),
1662
+ ...resolveWorkspaceEvaluation({
1663
+ agents: libraries.agents,
1664
+ scenarios: executable.scenarios,
1665
+ settings: toWorkspaceEvaluationSettings(workspaceSettings)
1666
+ })
1604
1667
  });
1605
1668
  const reportHtml = renderReport(results);
1606
1669
  const allRuns = results.scenarios.flatMap((scenario) => scenario.runs);
@@ -1663,7 +1726,7 @@ export function registerTools(server) {
1663
1726
  throw new Error('since must be earlier than or equal to until');
1664
1727
  const matching = listRunsWithFallback(resolveRunsDir(), undefined, true).filter((entry) => {
1665
1728
  const timestamp = String(entry.metadata && typeof entry.metadata === 'object'
1666
- ? entry.metadata.timestamp ?? ''
1729
+ ? (entry.metadata.timestamp ?? '')
1667
1730
  : '');
1668
1731
  const time = new Date(timestamp).getTime();
1669
1732
  if (sinceMs === undefined && untilMs === undefined)
@@ -1857,7 +1920,7 @@ export function registerTools(server) {
1857
1920
  left: MetricSummarySchema,
1858
1921
  right: MetricSummarySchema,
1859
1922
  deltas: z.object({
1860
- pass_rate: z.number(),
1923
+ pass_rate: z.number().nullable(),
1861
1924
  failed_runs: z.number(),
1862
1925
  avg_tool_calls_per_run: z.number(),
1863
1926
  avg_tool_latency_ms: z.number().nullable()
@@ -1867,7 +1930,8 @@ export function registerTools(server) {
1867
1930
  improved: z.number().int().nonnegative(),
1868
1931
  unchanged: z.number().int().nonnegative(),
1869
1932
  new: z.number().int().nonnegative(),
1870
- missing: z.number().int().nonnegative()
1933
+ missing: z.number().int().nonnegative(),
1934
+ not_evaluated: z.number().int().nonnegative()
1871
1935
  })
1872
1936
  }),
1873
1937
  regressions: z.array(CompareRowSchema),
@@ -2389,7 +2453,7 @@ export function registerTools(server) {
2389
2453
  .array(z.object({
2390
2454
  scenario_id: z.string(),
2391
2455
  agent: z.string(),
2392
- pass_rate: z.number()
2456
+ pass_rate: z.number().nullable()
2393
2457
  }))
2394
2458
  .optional()
2395
2459
  },
@@ -2797,23 +2861,25 @@ function resolveServerOwnedRoots() {
2797
2861
  function resolveBundleRoot() {
2798
2862
  return SERVER_OWNED_ROOTS.bundleRoot;
2799
2863
  }
2800
- function mergeLibraryServersAndAgents(config, bundleRoot) {
2801
- const libraries = readLibraryAgentsAndServers(bundleRoot);
2864
+ function mergeLibraryServersAndAgents(config, libraries) {
2802
2865
  return {
2803
2866
  ...config,
2804
2867
  agents: { ...libraries.agents, ...config.agents },
2805
2868
  servers: { ...libraries.servers, ...config.servers }
2806
2869
  };
2807
2870
  }
2808
- function readLibrary(bundleRoot, includeContent) {
2871
+ function readLibrary(bundleRoot, includeContent, kind = 'all') {
2809
2872
  const serversPath = join(bundleRoot, 'servers.yaml');
2810
2873
  const agentsPath = join(bundleRoot, 'agents.yaml');
2811
2874
  const scenariosDir = resolveScenarioLibraryDir(bundleRoot).path;
2812
2875
  const servers = existsSync(serversPath)
2813
- ? parseYaml(readFileSync(serversPath, 'utf8')) ?? {}
2876
+ ? (parseYaml(readFileSync(serversPath, 'utf8')) ?? {})
2814
2877
  : {};
2815
2878
  const agents = existsSync(agentsPath)
2816
- ? parseYaml(readFileSync(agentsPath, 'utf8')) ?? {}
2879
+ ? (parseYaml(readFileSync(agentsPath, 'utf8')) ?? {})
2880
+ : {};
2881
+ const browserProviders = kind === 'all' || kind === 'browser_providers'
2882
+ ? mergeBrowserProviderProfiles(readBrowserProviderProfiles(bundleRoot))
2817
2883
  : {};
2818
2884
  const scenarioEntries = [];
2819
2885
  if (existsSync(scenariosDir)) {
@@ -2845,29 +2911,57 @@ function readLibrary(bundleRoot, includeContent) {
2845
2911
  id,
2846
2912
  ...(includeContent ? { entry: agents[id] } : {})
2847
2913
  })),
2914
+ browser_providers: Object.keys(browserProviders)
2915
+ .sort()
2916
+ .map((id) => ({
2917
+ id,
2918
+ ...(includeContent
2919
+ ? { entry: browserProviders[id] }
2920
+ : {})
2921
+ })),
2848
2922
  test_cases: scenarioEntries,
2849
2923
  scenarios: scenarioEntries
2850
2924
  };
2851
2925
  return out;
2852
2926
  }
2853
2927
  function getLibraryItem(bundleRoot, kind, id) {
2854
- if (kind === 'servers' || kind === 'agents') {
2855
- const file = join(bundleRoot, `${kind}.yaml`);
2928
+ if (kind === 'servers' || kind === 'agents' || kind === 'browser_providers') {
2929
+ const directory = kind === 'browser_providers' ? join(bundleRoot, 'browser-providers') : null;
2930
+ const builtIn = kind === 'browser_providers' && Object.hasOwn(DEFAULT_BROWSER_PROVIDER_PROFILES, id)
2931
+ ? DEFAULT_BROWSER_PROVIDER_PROFILES[id]
2932
+ : undefined;
2933
+ const file = directory
2934
+ ? findBrowserProviderFile(directory, id)
2935
+ : join(bundleRoot, `${kind}.yaml`);
2936
+ if (builtIn && !existsSync(file)) {
2937
+ const content = structuredClone(builtIn);
2938
+ return {
2939
+ bundleRoot,
2940
+ kind,
2941
+ id,
2942
+ yaml: serializeBrowserProviderProfile(content).trimEnd(),
2943
+ content: content
2944
+ };
2945
+ }
2856
2946
  if (!existsSync(file)) {
2857
2947
  throw new Error(`Library file not found: ${file}`);
2858
2948
  }
2859
2949
  const raw = readFileSync(file, 'utf8');
2860
2950
  const parsed = parseYaml(raw) ?? {};
2861
- if (!(id in parsed)) {
2951
+ if (kind !== 'browser_providers' && !(id in parsed)) {
2862
2952
  throw new Error(`'${id}' not found in ${file}`);
2863
2953
  }
2864
- const entry = parsed[id];
2954
+ const normalizedEntry = kind === 'browser_providers' ? readBrowserProviderProfiles(bundleRoot)[id] : parsed[id];
2955
+ if (!normalizedEntry)
2956
+ throw new Error(`'${id}' not found in ${file}`);
2865
2957
  return {
2866
2958
  bundleRoot,
2867
2959
  kind,
2868
2960
  id,
2869
- yaml: stringifyYaml({ [id]: entry }).trimEnd(),
2870
- content: entry
2961
+ yaml: kind === 'browser_providers'
2962
+ ? raw.trimEnd()
2963
+ : stringifyYaml({ [id]: normalizedEntry }).trimEnd(),
2964
+ content: normalizedEntry
2871
2965
  };
2872
2966
  }
2873
2967
  const dir = resolveScenarioLibraryDir(bundleRoot).path;
@@ -2892,6 +2986,14 @@ function getLibraryItem(bundleRoot, kind, id) {
2892
2986
  }
2893
2987
  throw new Error(`Scenario '${id}' not found in ${dir}`);
2894
2988
  }
2989
+ function findBrowserProviderFile(directory, id) {
2990
+ if (!existsSync(directory))
2991
+ return join(directory, `${id}.yaml`);
2992
+ const file = readdirSync(directory)
2993
+ .filter((name) => name.endsWith('.yaml') || name.endsWith('.yml'))
2994
+ .find((name) => basename(name, extname(name)) === id);
2995
+ return join(directory, file ?? `${id}.yaml`);
2996
+ }
2895
2997
  function listEvaluationConfigs(params) {
2896
2998
  const evalsDir = join(resolveBundleRoot(), 'evals');
2897
2999
  const files = [];
@@ -3762,9 +3864,9 @@ function truncate(text, maxChars) {
3762
3864
  return `${text.slice(0, maxChars)}\n...[truncated ${text.length - maxChars} chars]`;
3763
3865
  }
3764
3866
  export async function handleMcplabMcpHttpRequest(req, res, sessions, options) {
3765
- await handleHttpRequest(req, res, sessions, options?.path ?? DEFAULT_MCP_PATH);
3867
+ await handleHttpRequest(req, res, sessions, options?.path ?? DEFAULT_MCP_PATH, options?.logger ?? console);
3766
3868
  }
3767
- async function handleHttpRequest(req, res, sessions, mcpPath) {
3869
+ async function handleHttpRequest(req, res, sessions, mcpPath, logger = console) {
3768
3870
  const method = req.method ?? 'GET';
3769
3871
  const pathname = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`).pathname;
3770
3872
  if (pathname === '/' && method === 'GET') {
@@ -3796,14 +3898,25 @@ async function handleHttpRequest(req, res, sessions, mcpPath) {
3796
3898
  return;
3797
3899
  }
3798
3900
  if (isInitializeRequest(body)) {
3901
+ const clientInfo = body.params && typeof body.params === 'object' && 'clientInfo' in body.params
3902
+ ? body.params.clientInfo
3903
+ : undefined;
3904
+ const clientName = clientInfo && typeof clientInfo === 'object' && 'name' in clientInfo
3905
+ ? String(clientInfo.name)
3906
+ : 'unknown';
3907
+ const clientVersion = clientInfo && typeof clientInfo === 'object' && 'version' in clientInfo
3908
+ ? String(clientInfo.version)
3909
+ : undefined;
3799
3910
  let runtime;
3800
3911
  const transport = new StreamableHTTPServerTransport({
3801
3912
  sessionIdGenerator: () => randomUUID(),
3802
3913
  onsessioninitialized: (sid) => {
3803
3914
  sessions.set(sid, runtime);
3915
+ logger.log(`[mcplab-mcp] client connected: ${clientName}${clientVersion ? `/${clientVersion}` : ''} (session ${sid})`);
3804
3916
  },
3805
3917
  onsessionclosed: (sid) => {
3806
3918
  sessions.delete(sid);
3919
+ logger.log(`[mcplab-mcp] client disconnected: session ${sid}`);
3807
3920
  }
3808
3921
  });
3809
3922
  const mcpServer = createConfiguredServer();