@inspectr/mcplab-mcp-server 1.8.3 → 1.9.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.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, 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({
@@ -96,6 +97,13 @@ const AgentEntrySchema = z.object({
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
  }
@@ -1175,6 +1189,46 @@ export function registerTools(server) {
1175
1189
  });
1176
1190
  });
1177
1191
  });
1192
+ registerTool('mcplab_generate_browser_agent_entry', {
1193
+ 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.',
1194
+ outputSchema: {
1195
+ id: z.string(),
1196
+ entry: z.object({
1197
+ type: z.literal('browser'),
1198
+ name: z.string().optional(),
1199
+ provider: z.string(),
1200
+ url: z.string(),
1201
+ new_conversation_between_scenarios: z.boolean().optional()
1202
+ }),
1203
+ yaml: z.string()
1204
+ },
1205
+ inputSchema: {
1206
+ id: z.string().describe('Browser agent id key (kebab-case recommended).'),
1207
+ name: z.string().optional().describe('Human-readable browser agent name.'),
1208
+ provider: z
1209
+ .string()
1210
+ .min(1)
1211
+ .describe('Provider id from browser-providers/<provider-id>.yaml.'),
1212
+ url: z.string().url().describe('Browser origin where Rover should execute the agent.'),
1213
+ new_conversation_between_scenarios: z
1214
+ .boolean()
1215
+ .optional()
1216
+ .describe('Whether Rover should start a new conversation between scenarios.')
1217
+ }
1218
+ }, async ({ id, name, provider, url, new_conversation_between_scenarios }) => withToolHandling(async () => {
1219
+ const entry = removeUndefined({
1220
+ type: 'browser',
1221
+ name,
1222
+ provider,
1223
+ url,
1224
+ new_conversation_between_scenarios
1225
+ });
1226
+ return ok(`Generated browser agent entry '${id}'`, {
1227
+ id,
1228
+ entry,
1229
+ yaml: stringifyYaml({ [id]: entry }).trimEnd()
1230
+ });
1231
+ }));
1178
1232
  registerTool('mcplab_create_test_case', {
1179
1233
  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
1234
  outputSchema: { id: z.string(), path: z.string(), test_case: ScenarioEntrySchema },
@@ -1556,7 +1610,7 @@ export function registerTools(server) {
1556
1610
  scenarios: z.array(z.object({
1557
1611
  scenario_id: z.string(),
1558
1612
  agent: z.string(),
1559
- pass_rate: z.number(),
1613
+ pass_rate: z.number().nullable(),
1560
1614
  tool_usage_frequency: z.record(z.number())
1561
1615
  })),
1562
1616
  effective_server_overrides: z.record(z.array(z.string())),
@@ -1663,7 +1717,7 @@ export function registerTools(server) {
1663
1717
  throw new Error('since must be earlier than or equal to until');
1664
1718
  const matching = listRunsWithFallback(resolveRunsDir(), undefined, true).filter((entry) => {
1665
1719
  const timestamp = String(entry.metadata && typeof entry.metadata === 'object'
1666
- ? entry.metadata.timestamp ?? ''
1720
+ ? (entry.metadata.timestamp ?? '')
1667
1721
  : '');
1668
1722
  const time = new Date(timestamp).getTime();
1669
1723
  if (sinceMs === undefined && untilMs === undefined)
@@ -1857,7 +1911,7 @@ export function registerTools(server) {
1857
1911
  left: MetricSummarySchema,
1858
1912
  right: MetricSummarySchema,
1859
1913
  deltas: z.object({
1860
- pass_rate: z.number(),
1914
+ pass_rate: z.number().nullable(),
1861
1915
  failed_runs: z.number(),
1862
1916
  avg_tool_calls_per_run: z.number(),
1863
1917
  avg_tool_latency_ms: z.number().nullable()
@@ -1867,7 +1921,8 @@ export function registerTools(server) {
1867
1921
  improved: z.number().int().nonnegative(),
1868
1922
  unchanged: z.number().int().nonnegative(),
1869
1923
  new: z.number().int().nonnegative(),
1870
- missing: z.number().int().nonnegative()
1924
+ missing: z.number().int().nonnegative(),
1925
+ not_evaluated: z.number().int().nonnegative()
1871
1926
  })
1872
1927
  }),
1873
1928
  regressions: z.array(CompareRowSchema),
@@ -2389,7 +2444,7 @@ export function registerTools(server) {
2389
2444
  .array(z.object({
2390
2445
  scenario_id: z.string(),
2391
2446
  agent: z.string(),
2392
- pass_rate: z.number()
2447
+ pass_rate: z.number().nullable()
2393
2448
  }))
2394
2449
  .optional()
2395
2450
  },
@@ -2805,15 +2860,18 @@ function mergeLibraryServersAndAgents(config, bundleRoot) {
2805
2860
  servers: { ...libraries.servers, ...config.servers }
2806
2861
  };
2807
2862
  }
2808
- function readLibrary(bundleRoot, includeContent) {
2863
+ function readLibrary(bundleRoot, includeContent, kind = 'all') {
2809
2864
  const serversPath = join(bundleRoot, 'servers.yaml');
2810
2865
  const agentsPath = join(bundleRoot, 'agents.yaml');
2811
2866
  const scenariosDir = resolveScenarioLibraryDir(bundleRoot).path;
2812
2867
  const servers = existsSync(serversPath)
2813
- ? parseYaml(readFileSync(serversPath, 'utf8')) ?? {}
2868
+ ? (parseYaml(readFileSync(serversPath, 'utf8')) ?? {})
2814
2869
  : {};
2815
2870
  const agents = existsSync(agentsPath)
2816
- ? parseYaml(readFileSync(agentsPath, 'utf8')) ?? {}
2871
+ ? (parseYaml(readFileSync(agentsPath, 'utf8')) ?? {})
2872
+ : {};
2873
+ const browserProviders = kind === 'all' || kind === 'browser_providers'
2874
+ ? mergeBrowserProviderProfiles(readBrowserProviderProfiles(bundleRoot))
2817
2875
  : {};
2818
2876
  const scenarioEntries = [];
2819
2877
  if (existsSync(scenariosDir)) {
@@ -2845,29 +2903,57 @@ function readLibrary(bundleRoot, includeContent) {
2845
2903
  id,
2846
2904
  ...(includeContent ? { entry: agents[id] } : {})
2847
2905
  })),
2906
+ browser_providers: Object.keys(browserProviders)
2907
+ .sort()
2908
+ .map((id) => ({
2909
+ id,
2910
+ ...(includeContent
2911
+ ? { entry: browserProviders[id] }
2912
+ : {})
2913
+ })),
2848
2914
  test_cases: scenarioEntries,
2849
2915
  scenarios: scenarioEntries
2850
2916
  };
2851
2917
  return out;
2852
2918
  }
2853
2919
  function getLibraryItem(bundleRoot, kind, id) {
2854
- if (kind === 'servers' || kind === 'agents') {
2855
- const file = join(bundleRoot, `${kind}.yaml`);
2920
+ if (kind === 'servers' || kind === 'agents' || kind === 'browser_providers') {
2921
+ const directory = kind === 'browser_providers' ? join(bundleRoot, 'browser-providers') : null;
2922
+ const builtIn = kind === 'browser_providers' && Object.hasOwn(DEFAULT_BROWSER_PROVIDER_PROFILES, id)
2923
+ ? DEFAULT_BROWSER_PROVIDER_PROFILES[id]
2924
+ : undefined;
2925
+ const file = directory
2926
+ ? findBrowserProviderFile(directory, id)
2927
+ : join(bundleRoot, `${kind}.yaml`);
2928
+ if (builtIn && !existsSync(file)) {
2929
+ const content = structuredClone(builtIn);
2930
+ return {
2931
+ bundleRoot,
2932
+ kind,
2933
+ id,
2934
+ yaml: serializeBrowserProviderProfile(content).trimEnd(),
2935
+ content: content
2936
+ };
2937
+ }
2856
2938
  if (!existsSync(file)) {
2857
2939
  throw new Error(`Library file not found: ${file}`);
2858
2940
  }
2859
2941
  const raw = readFileSync(file, 'utf8');
2860
2942
  const parsed = parseYaml(raw) ?? {};
2861
- if (!(id in parsed)) {
2943
+ if (kind !== 'browser_providers' && !(id in parsed)) {
2862
2944
  throw new Error(`'${id}' not found in ${file}`);
2863
2945
  }
2864
- const entry = parsed[id];
2946
+ const normalizedEntry = kind === 'browser_providers' ? readBrowserProviderProfiles(bundleRoot)[id] : parsed[id];
2947
+ if (!normalizedEntry)
2948
+ throw new Error(`'${id}' not found in ${file}`);
2865
2949
  return {
2866
2950
  bundleRoot,
2867
2951
  kind,
2868
2952
  id,
2869
- yaml: stringifyYaml({ [id]: entry }).trimEnd(),
2870
- content: entry
2953
+ yaml: kind === 'browser_providers'
2954
+ ? raw.trimEnd()
2955
+ : stringifyYaml({ [id]: normalizedEntry }).trimEnd(),
2956
+ content: normalizedEntry
2871
2957
  };
2872
2958
  }
2873
2959
  const dir = resolveScenarioLibraryDir(bundleRoot).path;
@@ -2892,6 +2978,14 @@ function getLibraryItem(bundleRoot, kind, id) {
2892
2978
  }
2893
2979
  throw new Error(`Scenario '${id}' not found in ${dir}`);
2894
2980
  }
2981
+ function findBrowserProviderFile(directory, id) {
2982
+ if (!existsSync(directory))
2983
+ return join(directory, `${id}.yaml`);
2984
+ const file = readdirSync(directory)
2985
+ .filter((name) => name.endsWith('.yaml') || name.endsWith('.yml'))
2986
+ .find((name) => basename(name, extname(name)) === id);
2987
+ return join(directory, file ?? `${id}.yaml`);
2988
+ }
2895
2989
  function listEvaluationConfigs(params) {
2896
2990
  const evalsDir = join(resolveBundleRoot(), 'evals');
2897
2991
  const files = [];
@@ -3762,9 +3856,9 @@ function truncate(text, maxChars) {
3762
3856
  return `${text.slice(0, maxChars)}\n...[truncated ${text.length - maxChars} chars]`;
3763
3857
  }
3764
3858
  export async function handleMcplabMcpHttpRequest(req, res, sessions, options) {
3765
- await handleHttpRequest(req, res, sessions, options?.path ?? DEFAULT_MCP_PATH);
3859
+ await handleHttpRequest(req, res, sessions, options?.path ?? DEFAULT_MCP_PATH, options?.logger ?? console);
3766
3860
  }
3767
- async function handleHttpRequest(req, res, sessions, mcpPath) {
3861
+ async function handleHttpRequest(req, res, sessions, mcpPath, logger = console) {
3768
3862
  const method = req.method ?? 'GET';
3769
3863
  const pathname = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`).pathname;
3770
3864
  if (pathname === '/' && method === 'GET') {
@@ -3796,14 +3890,25 @@ async function handleHttpRequest(req, res, sessions, mcpPath) {
3796
3890
  return;
3797
3891
  }
3798
3892
  if (isInitializeRequest(body)) {
3893
+ const clientInfo = body.params && typeof body.params === 'object' && 'clientInfo' in body.params
3894
+ ? body.params.clientInfo
3895
+ : undefined;
3896
+ const clientName = clientInfo && typeof clientInfo === 'object' && 'name' in clientInfo
3897
+ ? String(clientInfo.name)
3898
+ : 'unknown';
3899
+ const clientVersion = clientInfo && typeof clientInfo === 'object' && 'version' in clientInfo
3900
+ ? String(clientInfo.version)
3901
+ : undefined;
3799
3902
  let runtime;
3800
3903
  const transport = new StreamableHTTPServerTransport({
3801
3904
  sessionIdGenerator: () => randomUUID(),
3802
3905
  onsessioninitialized: (sid) => {
3803
3906
  sessions.set(sid, runtime);
3907
+ logger.log(`[mcplab-mcp] client connected: ${clientName}${clientVersion ? `/${clientVersion}` : ''} (session ${sid})`);
3804
3908
  },
3805
3909
  onsessionclosed: (sid) => {
3806
3910
  sessions.delete(sid);
3911
+ logger.log(`[mcplab-mcp] client disconnected: session ${sid}`);
3807
3912
  }
3808
3913
  });
3809
3914
  const mcpServer = createConfiguredServer();