@inspectr/mcplab-mcp-server 1.8.2 → 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.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,CAalD;AAED,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;AA+uBnG,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,CA8yErD;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA0EvD;AAmvCD,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"}
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);
@@ -680,8 +693,51 @@ export function createConfiguredServer() {
680
693
  registerSkills(server);
681
694
  registerTools(server);
682
695
  registerPrompts(server);
696
+ patchToolSchemaDialect(server);
683
697
  return server;
684
698
  }
699
+ const DRAFT_07_DIALECT = 'http://json-schema.org/draft-07/schema#';
700
+ const DRAFT_2020_12_DIALECT = 'https://json-schema.org/draft/2020-12/schema';
701
+ function rewriteJsonSchemaDialect(value) {
702
+ if (Array.isArray(value)) {
703
+ for (const item of value)
704
+ rewriteJsonSchemaDialect(item);
705
+ return;
706
+ }
707
+ if (!value || typeof value !== 'object')
708
+ return;
709
+ const obj = value;
710
+ if (obj.$schema === DRAFT_07_DIALECT) {
711
+ obj.$schema = DRAFT_2020_12_DIALECT;
712
+ }
713
+ for (const key of Object.keys(obj)) {
714
+ rewriteJsonSchemaDialect(obj[key]);
715
+ }
716
+ }
717
+ /**
718
+ * @modelcontextprotocol/sdk@1.30.0 always stamps tool input/output JSON Schemas
719
+ * converted from Zod v3 as draft-07 ($schema is hardcoded, with no way for a
720
+ * server author to opt into 2020-12 — see
721
+ * https://github.com/modelcontextprotocol/typescript-sdk/issues/2721). Clients
722
+ * that validate structured output strictly against 2020-12 reject every tool
723
+ * call outright, which is what broke every mcplab_* tool after the 1.29.0 ->
724
+ * 1.30.0 bump in this repo. Patch the already-installed tools/list handler to
725
+ * relabel the dialect after the SDK builds its response.
726
+ *
727
+ * TODO: revisit once typescript-sdk#2721 is fixed upstream and we bump past
728
+ * the version that carries the fix — this whole function can then be deleted.
729
+ */
730
+ function patchToolSchemaDialect(server) {
731
+ const protocol = server.server;
732
+ const original = protocol._requestHandlers.get('tools/list');
733
+ if (!original)
734
+ return;
735
+ protocol._requestHandlers.set('tools/list', async (request, extra) => {
736
+ const result = await original(request, extra);
737
+ rewriteJsonSchemaDialect(result);
738
+ return result;
739
+ });
740
+ }
685
741
  export function registerTools(server) {
686
742
  const registerTool = (name, config, cb) => {
687
743
  const resolvedTitle = resolveToolTitle(name, config.title, config.annotations?.title);
@@ -1038,7 +1094,7 @@ export function registerTools(server) {
1038
1094
  outputSchema: LibraryEntrySchema,
1039
1095
  inputSchema: {
1040
1096
  kind: z
1041
- .enum(['all', 'servers', 'agents', 'test_cases', 'scenarios'])
1097
+ .enum(['all', 'servers', 'agents', 'browser_providers', 'test_cases', 'scenarios'])
1042
1098
  .optional()
1043
1099
  .describe('Which library category to list. Defaults to all.'),
1044
1100
  includeContent: z
@@ -1048,15 +1104,16 @@ export function registerTools(server) {
1048
1104
  }
1049
1105
  }, async ({ kind, includeContent }) => {
1050
1106
  return withToolHandling(async () => {
1051
- const root = resolveBundleRoot();
1052
- const data = readLibrary(root, Boolean(includeContent));
1053
1107
  const selectedKind = kind ?? 'all';
1108
+ const root = resolveBundleRoot();
1109
+ const data = readLibrary(root, Boolean(includeContent), selectedKind);
1054
1110
  const structured = selectedKind === 'all'
1055
1111
  ? data
1056
1112
  : {
1057
1113
  bundleRoot: data.bundleRoot,
1058
1114
  servers: selectedKind === 'servers' ? data.servers : [],
1059
1115
  agents: selectedKind === 'agents' ? data.agents : [],
1116
+ browser_providers: selectedKind === 'browser_providers' ? data.browser_providers : [],
1060
1117
  test_cases: selectedKind === 'test_cases' ? data.test_cases : [],
1061
1118
  scenarios: selectedKind === 'scenarios' ? data.scenarios : []
1062
1119
  };
@@ -1064,10 +1121,10 @@ export function registerTools(server) {
1064
1121
  });
1065
1122
  });
1066
1123
  registerTool('mcplab_get_library_item', {
1067
- 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.',
1068
1125
  outputSchema: {
1069
1126
  bundleRoot: z.string(),
1070
- kind: z.enum(['servers', 'agents', 'test_cases', 'scenarios']),
1127
+ kind: z.enum(['servers', 'agents', 'browser_providers', 'test_cases', 'scenarios']),
1071
1128
  id: z.string(),
1072
1129
  file: z.string().optional(),
1073
1130
  yaml: z.string(),
@@ -1075,7 +1132,7 @@ export function registerTools(server) {
1075
1132
  },
1076
1133
  inputSchema: {
1077
1134
  kind: z
1078
- .enum(['servers', 'agents', 'test_cases', 'scenarios'])
1135
+ .enum(['servers', 'agents', 'browser_providers', 'test_cases', 'scenarios'])
1079
1136
  .describe('Library category.'),
1080
1137
  id: z.string().describe('Entry id (for scenarios this is scenario.id, not filename).')
1081
1138
  }
@@ -1132,6 +1189,46 @@ export function registerTools(server) {
1132
1189
  });
1133
1190
  });
1134
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
+ }));
1135
1232
  registerTool('mcplab_create_test_case', {
1136
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.',
1137
1234
  outputSchema: { id: z.string(), path: z.string(), test_case: ScenarioEntrySchema },
@@ -1513,7 +1610,7 @@ export function registerTools(server) {
1513
1610
  scenarios: z.array(z.object({
1514
1611
  scenario_id: z.string(),
1515
1612
  agent: z.string(),
1516
- pass_rate: z.number(),
1613
+ pass_rate: z.number().nullable(),
1517
1614
  tool_usage_frequency: z.record(z.number())
1518
1615
  })),
1519
1616
  effective_server_overrides: z.record(z.array(z.string())),
@@ -1620,7 +1717,7 @@ export function registerTools(server) {
1620
1717
  throw new Error('since must be earlier than or equal to until');
1621
1718
  const matching = listRunsWithFallback(resolveRunsDir(), undefined, true).filter((entry) => {
1622
1719
  const timestamp = String(entry.metadata && typeof entry.metadata === 'object'
1623
- ? entry.metadata.timestamp ?? ''
1720
+ ? (entry.metadata.timestamp ?? '')
1624
1721
  : '');
1625
1722
  const time = new Date(timestamp).getTime();
1626
1723
  if (sinceMs === undefined && untilMs === undefined)
@@ -1814,7 +1911,7 @@ export function registerTools(server) {
1814
1911
  left: MetricSummarySchema,
1815
1912
  right: MetricSummarySchema,
1816
1913
  deltas: z.object({
1817
- pass_rate: z.number(),
1914
+ pass_rate: z.number().nullable(),
1818
1915
  failed_runs: z.number(),
1819
1916
  avg_tool_calls_per_run: z.number(),
1820
1917
  avg_tool_latency_ms: z.number().nullable()
@@ -1824,7 +1921,8 @@ export function registerTools(server) {
1824
1921
  improved: z.number().int().nonnegative(),
1825
1922
  unchanged: z.number().int().nonnegative(),
1826
1923
  new: z.number().int().nonnegative(),
1827
- missing: z.number().int().nonnegative()
1924
+ missing: z.number().int().nonnegative(),
1925
+ not_evaluated: z.number().int().nonnegative()
1828
1926
  })
1829
1927
  }),
1830
1928
  regressions: z.array(CompareRowSchema),
@@ -2346,7 +2444,7 @@ export function registerTools(server) {
2346
2444
  .array(z.object({
2347
2445
  scenario_id: z.string(),
2348
2446
  agent: z.string(),
2349
- pass_rate: z.number()
2447
+ pass_rate: z.number().nullable()
2350
2448
  }))
2351
2449
  .optional()
2352
2450
  },
@@ -2762,15 +2860,18 @@ function mergeLibraryServersAndAgents(config, bundleRoot) {
2762
2860
  servers: { ...libraries.servers, ...config.servers }
2763
2861
  };
2764
2862
  }
2765
- function readLibrary(bundleRoot, includeContent) {
2863
+ function readLibrary(bundleRoot, includeContent, kind = 'all') {
2766
2864
  const serversPath = join(bundleRoot, 'servers.yaml');
2767
2865
  const agentsPath = join(bundleRoot, 'agents.yaml');
2768
2866
  const scenariosDir = resolveScenarioLibraryDir(bundleRoot).path;
2769
2867
  const servers = existsSync(serversPath)
2770
- ? parseYaml(readFileSync(serversPath, 'utf8')) ?? {}
2868
+ ? (parseYaml(readFileSync(serversPath, 'utf8')) ?? {})
2771
2869
  : {};
2772
2870
  const agents = existsSync(agentsPath)
2773
- ? parseYaml(readFileSync(agentsPath, 'utf8')) ?? {}
2871
+ ? (parseYaml(readFileSync(agentsPath, 'utf8')) ?? {})
2872
+ : {};
2873
+ const browserProviders = kind === 'all' || kind === 'browser_providers'
2874
+ ? mergeBrowserProviderProfiles(readBrowserProviderProfiles(bundleRoot))
2774
2875
  : {};
2775
2876
  const scenarioEntries = [];
2776
2877
  if (existsSync(scenariosDir)) {
@@ -2802,29 +2903,57 @@ function readLibrary(bundleRoot, includeContent) {
2802
2903
  id,
2803
2904
  ...(includeContent ? { entry: agents[id] } : {})
2804
2905
  })),
2906
+ browser_providers: Object.keys(browserProviders)
2907
+ .sort()
2908
+ .map((id) => ({
2909
+ id,
2910
+ ...(includeContent
2911
+ ? { entry: browserProviders[id] }
2912
+ : {})
2913
+ })),
2805
2914
  test_cases: scenarioEntries,
2806
2915
  scenarios: scenarioEntries
2807
2916
  };
2808
2917
  return out;
2809
2918
  }
2810
2919
  function getLibraryItem(bundleRoot, kind, id) {
2811
- if (kind === 'servers' || kind === 'agents') {
2812
- 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
+ }
2813
2938
  if (!existsSync(file)) {
2814
2939
  throw new Error(`Library file not found: ${file}`);
2815
2940
  }
2816
2941
  const raw = readFileSync(file, 'utf8');
2817
2942
  const parsed = parseYaml(raw) ?? {};
2818
- if (!(id in parsed)) {
2943
+ if (kind !== 'browser_providers' && !(id in parsed)) {
2819
2944
  throw new Error(`'${id}' not found in ${file}`);
2820
2945
  }
2821
- 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}`);
2822
2949
  return {
2823
2950
  bundleRoot,
2824
2951
  kind,
2825
2952
  id,
2826
- yaml: stringifyYaml({ [id]: entry }).trimEnd(),
2827
- content: entry
2953
+ yaml: kind === 'browser_providers'
2954
+ ? raw.trimEnd()
2955
+ : stringifyYaml({ [id]: normalizedEntry }).trimEnd(),
2956
+ content: normalizedEntry
2828
2957
  };
2829
2958
  }
2830
2959
  const dir = resolveScenarioLibraryDir(bundleRoot).path;
@@ -2849,6 +2978,14 @@ function getLibraryItem(bundleRoot, kind, id) {
2849
2978
  }
2850
2979
  throw new Error(`Scenario '${id}' not found in ${dir}`);
2851
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
+ }
2852
2989
  function listEvaluationConfigs(params) {
2853
2990
  const evalsDir = join(resolveBundleRoot(), 'evals');
2854
2991
  const files = [];
@@ -3719,9 +3856,9 @@ function truncate(text, maxChars) {
3719
3856
  return `${text.slice(0, maxChars)}\n...[truncated ${text.length - maxChars} chars]`;
3720
3857
  }
3721
3858
  export async function handleMcplabMcpHttpRequest(req, res, sessions, options) {
3722
- await handleHttpRequest(req, res, sessions, options?.path ?? DEFAULT_MCP_PATH);
3859
+ await handleHttpRequest(req, res, sessions, options?.path ?? DEFAULT_MCP_PATH, options?.logger ?? console);
3723
3860
  }
3724
- async function handleHttpRequest(req, res, sessions, mcpPath) {
3861
+ async function handleHttpRequest(req, res, sessions, mcpPath, logger = console) {
3725
3862
  const method = req.method ?? 'GET';
3726
3863
  const pathname = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`).pathname;
3727
3864
  if (pathname === '/' && method === 'GET') {
@@ -3753,14 +3890,25 @@ async function handleHttpRequest(req, res, sessions, mcpPath) {
3753
3890
  return;
3754
3891
  }
3755
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;
3756
3902
  let runtime;
3757
3903
  const transport = new StreamableHTTPServerTransport({
3758
3904
  sessionIdGenerator: () => randomUUID(),
3759
3905
  onsessioninitialized: (sid) => {
3760
3906
  sessions.set(sid, runtime);
3907
+ logger.log(`[mcplab-mcp] client connected: ${clientName}${clientVersion ? `/${clientVersion}` : ''} (session ${sid})`);
3761
3908
  },
3762
3909
  onsessionclosed: (sid) => {
3763
3910
  sessions.delete(sid);
3911
+ logger.log(`[mcplab-mcp] client disconnected: session ${sid}`);
3764
3912
  }
3765
3913
  });
3766
3914
  const mcpServer = createConfiguredServer();