sunpeak 0.20.42 → 0.20.49

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.
Files changed (74) hide show
  1. package/bin/commands/inspect.mjs +142 -40
  2. package/bin/commands/test-init.mjs +2 -0
  3. package/bin/lib/eval/eval-runner.mjs +4 -0
  4. package/bin/lib/eval/model-registry.mjs +3 -6
  5. package/bin/lib/inspect/inspect-config.d.mts +8 -0
  6. package/bin/lib/inspect/inspect-config.mjs +9 -0
  7. package/bin/lib/inspect/inspect-server.d.mts +2 -0
  8. package/bin/lib/test/test-config.d.mts +6 -0
  9. package/bin/lib/test/test-config.mjs +11 -0
  10. package/bin/sunpeak.js +1 -0
  11. package/dist/chatgpt/index.cjs +1 -1
  12. package/dist/chatgpt/index.js +1 -1
  13. package/dist/claude/index.cjs +1 -1
  14. package/dist/claude/index.js +1 -1
  15. package/dist/hooks/tool-data-store.d.ts +26 -0
  16. package/dist/hooks/use-tool-data.d.ts +3 -9
  17. package/dist/host/chatgpt/index.cjs +1 -1
  18. package/dist/host/chatgpt/index.js +1 -1
  19. package/dist/index.cjs +36 -22
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.js +36 -22
  22. package/dist/index.js.map +1 -1
  23. package/dist/inspector/index.cjs +1 -1
  24. package/dist/inspector/index.js +1 -1
  25. package/dist/{inspector-DOmiG64-.cjs → inspector-BGnxpdOn.cjs} +46 -20
  26. package/dist/inspector-BGnxpdOn.cjs.map +1 -0
  27. package/dist/{inspector-C6n8zap3.js → inspector-DvduUVNG.js} +46 -20
  28. package/dist/inspector-DvduUVNG.js.map +1 -0
  29. package/dist/lib/utils.d.ts +8 -7
  30. package/dist/mcp/index.cjs +6 -4
  31. package/dist/mcp/index.cjs.map +1 -1
  32. package/dist/mcp/index.js +6 -4
  33. package/dist/mcp/index.js.map +1 -1
  34. package/dist/mcp/server.d.ts +12 -1
  35. package/dist/{use-app-Duar2Ipu.js → use-app-CmrLc3wz.js} +63 -2
  36. package/dist/use-app-CmrLc3wz.js.map +1 -0
  37. package/dist/{use-app-DUdnDLP5.cjs → use-app-fizR-zbu.cjs} +63 -2
  38. package/dist/use-app-fizR-zbu.cjs.map +1 -0
  39. package/package.json +9 -9
  40. package/template/dist/albums/albums.html +2 -2
  41. package/template/dist/albums/albums.json +1 -1
  42. package/template/dist/carousel/carousel.html +2 -2
  43. package/template/dist/carousel/carousel.json +1 -1
  44. package/template/dist/map/map.html +3 -3
  45. package/template/dist/map/map.json +1 -1
  46. package/template/dist/review/review.html +2 -2
  47. package/template/dist/review/review.json +1 -1
  48. package/template/node_modules/.bin/tsc +2 -2
  49. package/template/node_modules/.bin/tsserver +2 -2
  50. package/template/node_modules/.bin/vitest +2 -2
  51. package/template/node_modules/.vite/deps/_metadata.json +3 -3
  52. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps.js +1 -1
  53. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps.js.map +1 -1
  54. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_app-bridge.js +1 -1
  55. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_app-bridge.js.map +1 -1
  56. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_react.js +1 -1
  57. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_react.js.map +1 -1
  58. package/template/node_modules/.vite-mcp/deps/_metadata.json +23 -23
  59. package/template/node_modules/.vite-mcp/deps/vitest.js +7 -7
  60. package/template/node_modules/.vite-mcp/deps/vitest.js.map +1 -1
  61. package/template/package.json +1 -1
  62. package/template/tests/e2e/visual.spec.ts-snapshots/albums-dark-chatgpt-linux.png +0 -0
  63. package/template/tests/e2e/visual.spec.ts-snapshots/albums-dark-claude-linux.png +0 -0
  64. package/template/tests/e2e/visual.spec.ts-snapshots/albums-fullscreen-chatgpt-darwin.png +0 -0
  65. package/template/tests/e2e/visual.spec.ts-snapshots/albums-fullscreen-chatgpt-linux.png +0 -0
  66. package/template/tests/e2e/visual.spec.ts-snapshots/albums-fullscreen-claude-darwin.png +0 -0
  67. package/template/tests/e2e/visual.spec.ts-snapshots/albums-fullscreen-claude-linux.png +0 -0
  68. package/template/tests/e2e/visual.spec.ts-snapshots/albums-light-chatgpt-linux.png +0 -0
  69. package/template/tests/e2e/visual.spec.ts-snapshots/albums-light-claude-linux.png +0 -0
  70. package/template/tsconfig.json +2 -0
  71. package/dist/inspector-C6n8zap3.js.map +0 -1
  72. package/dist/inspector-DOmiG64-.cjs.map +0 -1
  73. package/dist/use-app-DUdnDLP5.cjs.map +0 -1
  74. package/dist/use-app-Duar2Ipu.js.map +0 -1
@@ -44,6 +44,7 @@ function parseArgs(args) {
44
44
  name: undefined,
45
45
  env: undefined,
46
46
  cwd: undefined,
47
+ headers: undefined,
47
48
  };
48
49
 
49
50
  for (let i = 0; i < args.length; i++) {
@@ -66,6 +67,10 @@ function parseArgs(args) {
66
67
  }
67
68
  } else if (arg === '--cwd' && i + 1 < args.length) {
68
69
  opts.cwd = args[++i];
70
+ } else if ((arg === '--header' || arg === '-H') && i + 1 < args.length) {
71
+ opts.headers = opts.headers || {};
72
+ const [name, value] = parseHttpHeader(args[++i]);
73
+ setHttpHeader(opts.headers, name, value);
69
74
  } else if (arg === '--help' || arg === '-h') {
70
75
  printHelp();
71
76
  process.exit(0);
@@ -89,16 +94,52 @@ Options:
89
94
  --name <string> App name in inspector chrome
90
95
  --env <KEY=VALUE> Environment variable for stdio servers (repeatable)
91
96
  --cwd <path> Working directory for stdio servers
97
+ --header, -H <Name: value> HTTP header for HTTP MCP servers (repeatable)
92
98
  --help, -h Show this help
93
99
 
94
100
  Examples:
95
101
  sunpeak inspect --server http://localhost:8000/mcp
102
+ sunpeak inspect --server http://localhost:8000/mcp -H "Authorization: Bearer $TOKEN"
96
103
  sunpeak inspect --server "python my_server.py"
97
104
  sunpeak inspect --server "python server.py" --env API_KEY=sk-123 --cwd ./backend
98
105
  sunpeak inspect --server http://localhost:8000/mcp --simulations tests/simulations
99
106
  `);
100
107
  }
101
108
 
109
+ function setHttpHeader(headers, name, value) {
110
+ const lowerName = name.toLowerCase();
111
+ for (const existingName of Object.keys(headers)) {
112
+ if (existingName.toLowerCase() === lowerName) {
113
+ delete headers[existingName];
114
+ }
115
+ }
116
+ headers[name] = value;
117
+ }
118
+
119
+ function parseHttpHeader(raw) {
120
+ if (typeof raw !== 'string') {
121
+ throw new Error('Invalid --header value. Expected "Name: value".');
122
+ }
123
+ const separator = raw.indexOf(':');
124
+ if (separator <= 0) {
125
+ throw new Error('Invalid --header value. Expected "Name: value".');
126
+ }
127
+
128
+ const name = raw.slice(0, separator).trim();
129
+ const value = raw.slice(separator + 1).trim();
130
+ if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name)) {
131
+ throw new Error(`Invalid HTTP header name: ${name || '(empty)'}`);
132
+ }
133
+ if (/[\u0000-\u001f\u007f]/.test(value)) {
134
+ throw new Error(`Invalid HTTP header value for ${name}: control characters are not allowed`);
135
+ }
136
+ return [name, value];
137
+ }
138
+
139
+ function hasAuthorizationHeader(headers) {
140
+ return Object.keys(headers || {}).some((name) => name.toLowerCase() === 'authorization');
141
+ }
142
+
102
143
  /**
103
144
  * Create an in-memory OAuth client provider for the inspector.
104
145
  * The provider stores tokens, client info, and code verifier in memory.
@@ -308,7 +349,7 @@ async function negotiateOAuth(serverUrl) {
308
349
 
309
350
  // Try the anonymous/auto-approved path first: follow the authorization URL
310
351
  // without a browser and see if it immediately redirects with a code.
311
- const code = await tryAnonymousOAuth(authUrl.toString(), callbackUrl);
352
+ const code = await tryAnonymousOAuth(authUrl.toString(), callbackUrl, oauthState.stateParam);
312
353
  if (code) {
313
354
  // Complete the flow with the authorization code.
314
355
  const tokenResult = await auth(provider, {
@@ -347,15 +388,17 @@ async function negotiateOAuth(serverUrl) {
347
388
  *
348
389
  * @param {string} authUrl - The authorization URL
349
390
  * @param {string} callbackUrl - The expected callback URL prefix
391
+ * @param {string} [expectedState] - OAuth state value that must be echoed by the callback
392
+ * @param {typeof fetch} [fetchFn]
350
393
  * @returns {Promise<string | null>}
351
394
  */
352
- async function tryAnonymousOAuth(authUrl, callbackUrl) {
395
+ async function tryAnonymousOAuth(authUrl, callbackUrl, expectedState, fetchFn = fetch) {
353
396
  // Follow redirects manually to detect when the server redirects back
354
397
  // to our callback URL with a code parameter.
355
398
  let url = authUrl;
356
399
  const maxRedirects = 10;
357
400
  for (let i = 0; i < maxRedirects; i++) {
358
- const response = await fetch(url, { redirect: 'manual' });
401
+ const response = await fetchFn(url, { redirect: 'manual' });
359
402
  const location = response.headers.get('location');
360
403
 
361
404
  if (!location) {
@@ -366,11 +409,21 @@ async function tryAnonymousOAuth(authUrl, callbackUrl) {
366
409
  }
367
410
 
368
411
  // Resolve relative redirects.
369
- const resolved = new URL(location, url).toString();
412
+ const resolvedUrl = new URL(location, url);
413
+ if (resolvedUrl.protocol !== 'http:' && resolvedUrl.protocol !== 'https:') {
414
+ throw new Error(
415
+ `OAuth authorization redirect has unsupported scheme: ${resolvedUrl.protocol}`
416
+ );
417
+ }
418
+ const resolved = resolvedUrl.toString();
370
419
 
371
420
  // Check if the redirect goes to our callback URL.
372
421
  if (resolved.startsWith(callbackUrl)) {
373
422
  const params = new URL(resolved).searchParams;
423
+ const state = params.get('state');
424
+ if (expectedState && state !== expectedState) {
425
+ throw new Error('OAuth state mismatch — callback rejected');
426
+ }
374
427
  const code = params.get('code');
375
428
  if (code) return code;
376
429
  const error = params.get('error');
@@ -491,6 +544,9 @@ function isAuthError(err) {
491
544
  // StreamableHTTPError includes a status code in its message.
492
545
  // Check for the specific "401" HTTP status pattern, not substring matches.
493
546
  const msg = err.message || '';
547
+ if (/"statusCode"\s*:\s*401\b/.test(msg)) return true;
548
+ if (/\bstatus(?:Code)?\s*[:=]\s*401\b/i.test(msg)) return true;
549
+ if (/\bHTTP\s+401\b/i.test(msg)) return true;
494
550
  if (msg.includes('invalid_token')) return true;
495
551
 
496
552
  // Connection errors (ECONNREFUSED, ETIMEDOUT, etc.) are never auth errors.
@@ -681,11 +737,15 @@ async function assertHttpServerUrlAllowed(
681
737
 
682
738
  async function resolveHttpRedirectsForMcp(
683
739
  serverArg,
684
- { enforcePublicHttpUrl = false, fetchFn = fetch, lookupFn = dnsLookup } = {}
740
+ { enforcePublicHttpUrl = false, fetchFn = fetch, lookupFn = dnsLookup, requestInit } = {}
685
741
  ) {
686
742
  if (!enforcePublicHttpUrl) {
687
743
  try {
688
- const probeResponse = await fetchFn(serverArg, { method: 'HEAD', redirect: 'follow' });
744
+ const probeResponse = await fetchFn(serverArg, {
745
+ ...(requestInit ?? {}),
746
+ method: 'HEAD',
747
+ redirect: 'follow',
748
+ });
689
749
  await probeResponse.body?.cancel?.();
690
750
  return probeResponse.url && probeResponse.url !== serverArg ? probeResponse.url : serverArg;
691
751
  } catch {
@@ -698,7 +758,11 @@ async function resolveHttpRedirectsForMcp(
698
758
  for (let i = 0; i < maxRedirects; i++) {
699
759
  let probeResponse;
700
760
  try {
701
- probeResponse = await fetchFn(currentUrl, { method: 'HEAD', redirect: 'manual' });
761
+ probeResponse = await fetchFn(currentUrl, {
762
+ ...(requestInit ?? {}),
763
+ method: 'HEAD',
764
+ redirect: 'manual',
765
+ });
702
766
  } catch {
703
767
  return currentUrl;
704
768
  }
@@ -721,7 +785,7 @@ async function resolveHttpRedirectsForMcp(
721
785
  /**
722
786
  * Create an MCP client connection.
723
787
  * @param {string} serverArg - URL or command string
724
- * @param {{ type?: 'none' | 'bearer' | 'oauth', bearerToken?: string, authProvider?: import('@modelcontextprotocol/sdk/client/auth.js').OAuthClientProvider, env?: Record<string, string>, cwd?: string, enforcePublicHttpUrl?: boolean }} [authConfig]
788
+ * @param {{ type?: 'none' | 'bearer' | 'oauth', bearerToken?: string, authProvider?: import('@modelcontextprotocol/sdk/client/auth.js').OAuthClientProvider, headers?: Record<string, string>, env?: Record<string, string>, cwd?: string, enforcePublicHttpUrl?: boolean }} [authConfig]
725
789
  * @returns {Promise<{ client: import('@modelcontextprotocol/sdk/client/index.js').Client, transport: import('@modelcontextprotocol/sdk/types.js').Transport, serverUrl?: string, stderrOutput?: string[] }>}
726
790
  */
727
791
  async function createMcpConnection(serverArg, authConfig) {
@@ -737,10 +801,18 @@ async function createMcpConnection(serverArg, authConfig) {
737
801
  const { StreamableHTTPClientTransport } =
738
802
  await import('@modelcontextprotocol/sdk/client/streamableHttp.js');
739
803
 
804
+ const requestHeaders = { ...(authConfig?.headers ?? {}) };
805
+ if (authConfig?.type === 'bearer' && authConfig.bearerToken) {
806
+ requestHeaders.Authorization = `Bearer ${authConfig.bearerToken}`;
807
+ }
808
+
740
809
  // Follow redirects (e.g. /mcp → /mcp/) before creating the transport.
741
810
  // The MCP SDK transport doesn't follow redirects on its own.
742
811
  const finalUrl = await resolveHttpRedirectsForMcp(serverArg, {
743
812
  enforcePublicHttpUrl: !!authConfig?.enforcePublicHttpUrl,
813
+ ...(Object.keys(requestHeaders).length > 0
814
+ ? { requestInit: { headers: requestHeaders } }
815
+ : {}),
744
816
  });
745
817
 
746
818
  if (authConfig?.enforcePublicHttpUrl) {
@@ -751,11 +823,6 @@ async function createMcpConnection(serverArg, authConfig) {
751
823
  if (authConfig?.enforcePublicHttpUrl) {
752
824
  transportOpts.requestInit = { redirect: 'manual' };
753
825
  }
754
-
755
- const requestHeaders = {};
756
- if (authConfig?.type === 'bearer' && authConfig.bearerToken) {
757
- requestHeaders.Authorization = `Bearer ${authConfig.bearerToken}`;
758
- }
759
826
  if (Object.keys(requestHeaders).length > 0) {
760
827
  transportOpts.requestInit = {
761
828
  ...(transportOpts.requestInit ?? {}),
@@ -906,45 +973,68 @@ async function discoverSimulations(client) {
906
973
 
907
974
  /**
908
975
  * Load simulation JSON fixtures from a directory and merge into discovered simulations.
976
+ *
977
+ * Each fixture becomes a simulation keyed by its filename, so a tool can have
978
+ * multiple fixtures (e.g. `show-albums.json` and `show-albums-empty.json`
979
+ * both targeting tool `show-albums`). Auto-discovered slots are kept only for
980
+ * tools that have no fixture file.
981
+ *
909
982
  * @param {string} dir - Simulation directory path
910
983
  * @param {Record<string, object>} simulations - Discovered simulations to merge into
911
984
  */
912
- function mergeSimulationFixtures(dir, simulations) {
985
+ export function mergeSimulationFixtures(dir, simulations) {
913
986
  if (!existsSync(dir)) return;
914
987
 
915
988
  const files = readdirSync(dir).filter((f) => f.endsWith('.json'));
989
+
990
+ // Load every fixture first so we can group by tool name. We need the grouping
991
+ // to decide whether to keep the auto-discovered slot (no fixtures) or replace
992
+ // it with one entry per fixture file (one or more fixtures).
993
+ const fixtures = [];
916
994
  for (const file of files) {
917
995
  try {
918
996
  const fixture = JSON.parse(readFileSync(join(dir, file), 'utf-8'));
919
- const toolName = fixture.tool;
920
- if (!toolName) continue;
921
-
922
- // Find matching simulation by tool name
923
- const sim = simulations[toolName];
924
- if (sim) {
925
- // Merge fixture data into discovered simulation
926
- if (fixture.toolInput !== undefined) sim.toolInput = fixture.toolInput;
927
- if (fixture.toolResult !== undefined) sim.toolResult = fixture.toolResult;
928
- if (fixture.serverTools !== undefined) sim.serverTools = fixture.serverTools;
929
- if (fixture.userMessage !== undefined) sim.userMessage = fixture.userMessage;
930
- if (fixture.hostContext !== undefined) sim.hostContext = fixture.hostContext;
931
- } else {
932
- // Create a new simulation from the fixture (tool not on server, but user wants to mock it)
933
- const simName = file.replace(/\.json$/, '');
934
- simulations[simName] = {
935
- name: simName,
936
- tool: { name: toolName, inputSchema: { type: 'object' } },
937
- toolInput: fixture.toolInput,
938
- toolResult: fixture.toolResult,
939
- serverTools: fixture.serverTools,
940
- userMessage: fixture.userMessage,
941
- hostContext: fixture.hostContext,
942
- };
943
- }
997
+ if (!fixture.tool) continue;
998
+ fixtures.push({ file, fixture });
944
999
  } catch (err) {
945
1000
  console.warn(`Warning: Failed to parse simulation fixture ${file}:`, err.message);
946
1001
  }
947
1002
  }
1003
+
1004
+ const byTool = new Map();
1005
+ for (const item of fixtures) {
1006
+ const tool = item.fixture.tool;
1007
+ if (!byTool.has(tool)) byTool.set(tool, []);
1008
+ byTool.get(tool).push(item);
1009
+ }
1010
+
1011
+ for (const [toolName, items] of byTool) {
1012
+ const discovered = simulations[toolName];
1013
+
1014
+ // Drop the auto-discovered slot if none of the fixtures will reuse its
1015
+ // key (filename === tool name). Otherwise the named fixture overwrites
1016
+ // it in place below.
1017
+ const reusesSlot = items.some(({ file }) => file.replace(/\.json$/, '') === toolName);
1018
+ if (discovered && !reusesSlot) {
1019
+ delete simulations[toolName];
1020
+ }
1021
+
1022
+ for (const { file, fixture } of items) {
1023
+ const simName = file.replace(/\.json$/, '');
1024
+ const sim = discovered
1025
+ ? { ...discovered, name: simName }
1026
+ : {
1027
+ name: simName,
1028
+ tool: { name: toolName, inputSchema: { type: 'object' } },
1029
+ };
1030
+ if (fixture.toolInput !== undefined) sim.toolInput = fixture.toolInput;
1031
+ if (fixture.toolResult !== undefined) sim.toolResult = fixture.toolResult;
1032
+ if (fixture.serverTools !== undefined) sim.serverTools = fixture.serverTools;
1033
+ if (fixture.userMessage !== undefined) sim.userMessage = fixture.userMessage;
1034
+ if (fixture.hostContext !== undefined) sim.hostContext = fixture.hostContext;
1035
+ simulations[simName] = sim;
1036
+ }
1037
+ }
948
1038
  }
949
1039
 
950
1040
  const MODEL_PROVIDERS = new Set(['openai', 'anthropic']);
@@ -2872,9 +2962,13 @@ export const _securityTestExports = {
2872
2962
  normalizeModelId,
2873
2963
  normalizeModelProviderModelId,
2874
2964
  quoteSecurityInteractiveArg,
2965
+ parseHttpHeader,
2966
+ hasAuthorizationHeader,
2967
+ isAuthError,
2875
2968
  readRequestBody,
2876
2969
  resolveHttpRedirectsForMcp,
2877
2970
  shouldAllowPrivateServerUrls,
2971
+ tryAnonymousOAuth,
2878
2972
  };
2879
2973
 
2880
2974
  /**
@@ -2934,6 +3028,7 @@ function readRequestBody(req, { maxBytes = Infinity } = {}) {
2934
3028
  * @param {object} [opts.viteCssConfig] - Vite css config override (e.g., lightningcss customAtRules)
2935
3029
  * @param {Record<string, string>} [opts.env] - Extra environment variables for stdio server processes
2936
3030
  * @param {string} [opts.cwd] - Working directory for stdio server processes
3031
+ * @param {Record<string, string>} [opts.headers] - Extra HTTP headers for HTTP MCP server requests
2937
3032
  */
2938
3033
  export async function inspectServer(opts) {
2939
3034
  const {
@@ -2954,6 +3049,7 @@ export async function inspectServer(opts) {
2954
3049
  viteCssConfig,
2955
3050
  env: serverEnv,
2956
3051
  cwd: serverCwd,
3052
+ headers: serverHeaders,
2957
3053
  } = opts;
2958
3054
 
2959
3055
  // Load favicon from sunpeak package for the inspector UI.
@@ -2981,6 +3077,7 @@ export async function inspectServer(opts) {
2981
3077
  const connectionOpts = {};
2982
3078
  if (serverEnv) connectionOpts.env = serverEnv;
2983
3079
  if (serverCwd) connectionOpts.cwd = serverCwd;
3080
+ if (serverHeaders) connectionOpts.headers = serverHeaders;
2984
3081
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
2985
3082
  try {
2986
3083
  mcpConnection = await createMcpConnection(resolvedServerUrl, connectionOpts);
@@ -2993,7 +3090,11 @@ export async function inspectServer(opts) {
2993
3090
  }
2994
3091
 
2995
3092
  // If the server requires OAuth, negotiate it and retry once.
2996
- if (isAuthError(err) && resolvedServerUrl.startsWith('http')) {
3093
+ if (
3094
+ isAuthError(err) &&
3095
+ resolvedServerUrl.startsWith('http') &&
3096
+ !hasAuthorizationHeader(connectionOpts.headers)
3097
+ ) {
2997
3098
  console.log('Server requires authentication. Negotiating OAuth...');
2998
3099
  try {
2999
3100
  const authProvider = await negotiateOAuth(resolvedServerUrl);
@@ -3380,5 +3481,6 @@ export async function inspect(args) {
3380
3481
  name: opts.name,
3381
3482
  env: opts.env,
3382
3483
  cwd: opts.cwd,
3484
+ headers: opts.headers,
3383
3485
  });
3384
3486
  }
@@ -596,8 +596,10 @@ ${serverBlock}
596
596
  {
597
597
  compilerOptions: {
598
598
  target: 'ES2022',
599
+ lib: ['ESNext', 'DOM'],
599
600
  module: 'ESNext',
600
601
  moduleResolution: 'bundler',
602
+ types: ['node'],
601
603
  strict: true,
602
604
  esModuleInterop: true,
603
605
  },
@@ -220,12 +220,16 @@ export async function runSingleEval({
220
220
  }) {
221
221
  const { generateText } = await import('ai');
222
222
  const system = formatEvalAppContextForModel(appContext);
223
+ const providerOptions = model?.provider?.startsWith('openai.')
224
+ ? { openai: { strictJsonSchema: false } }
225
+ : undefined;
223
226
 
224
227
  const result = await generateText({
225
228
  model,
226
229
  tools,
227
230
  prompt,
228
231
  ...(system ? { system } : {}),
232
+ ...(providerOptions ? { providerOptions } : {}),
229
233
  maxSteps,
230
234
  temperature,
231
235
  maxRetries: 0, // We manage runs ourselves; AI SDK retries compound rate limits
@@ -47,12 +47,9 @@ export async function resolveModel(modelId) {
47
47
  // @ai-sdk/openai v3 defaults to the Responses API, which requires strict
48
48
  // JSON Schema (additionalProperties: false at every level, all properties
49
49
  // required) — incompatible with arbitrary MCP server schemas. Use .chat()
50
- // (Chat Completions API) when available and disable structured outputs,
51
- // because reasoning models also enable strict function schemas by default.
52
- const settings = { structuredOutputs: false };
53
- return typeof openai.chat === 'function'
54
- ? openai.chat(modelId, settings)
55
- : openai(modelId, settings);
50
+ // (Chat Completions API) when available. The eval runner disables strict
51
+ // JSON Schema through per-call provider options.
52
+ return typeof openai.chat === 'function' ? openai.chat(modelId) : openai(modelId);
56
53
  }
57
54
  if (pkg === '@ai-sdk/anthropic') {
58
55
  const { anthropic } = provider;
@@ -15,6 +15,14 @@ export interface InspectConfigOptions {
15
15
  use?: Record<string, unknown>;
16
16
  /** Visual regression testing configuration */
17
17
  visual?: VisualConfig;
18
+ /** HTTP headers for HTTP MCP server requests */
19
+ headers?: Record<string, string>;
20
+ /** Server startup timeout in ms */
21
+ timeout?: number;
22
+ /** Environment variables for stdio servers */
23
+ env?: Record<string, string>;
24
+ /** Working directory for stdio servers */
25
+ cwd?: string;
18
26
  }
19
27
 
20
28
  /**
@@ -30,6 +30,7 @@ import { resolveSunpeakBin } from '../resolve-bin.mjs';
30
30
  * @param {number} [options.timeout] - Server startup timeout in ms (default: 60000)
31
31
  * @param {Record<string, string>} [options.env] - Environment variables for stdio servers
32
32
  * @param {string} [options.cwd] - Working directory for stdio servers
33
+ * @param {Record<string, string>} [options.headers] - HTTP headers for HTTP MCP server requests
33
34
  * @returns {import('@playwright/test').PlaywrightTestConfig}
34
35
  */
35
36
  export function defineInspectConfig(options) {
@@ -44,6 +45,7 @@ export function defineInspectConfig(options) {
44
45
  timeout,
45
46
  env,
46
47
  cwd,
48
+ headers,
47
49
  } = options;
48
50
 
49
51
  if (!server) {
@@ -65,6 +67,9 @@ export function defineInspectConfig(options) {
65
67
  })
66
68
  : []),
67
69
  ...(cwd ? [cwd.includes(' ') ? `--cwd "${cwd}"` : `--cwd ${cwd}`] : []),
70
+ ...(headers
71
+ ? Object.entries(headers).map(([k, v]) => `--header ${shellQuote(`${k}: ${v}`)}`)
72
+ : []),
68
73
  ...(simulationsDir ? [`--simulations ${simulationsDir}`] : []),
69
74
  `--port ${port}`,
70
75
  ...(name ? [`--name "${name}"`] : []),
@@ -83,3 +88,7 @@ export function defineInspectConfig(options) {
83
88
  },
84
89
  });
85
90
  }
91
+
92
+ function shellQuote(value) {
93
+ return `'${String(value).replace(/'/g, "'\\''")}'`;
94
+ }
@@ -29,4 +29,6 @@ export function inspectServer(opts: {
29
29
  env?: Record<string, string>;
30
30
  /** Working directory for stdio server processes. */
31
31
  cwd?: string;
32
+ /** HTTP headers for HTTP MCP server requests. */
33
+ headers?: Record<string, string>;
32
34
  }): Promise<void>;
@@ -12,6 +12,10 @@ export interface ServerConfig {
12
12
  url?: string;
13
13
  /** Environment variables for the server process. */
14
14
  env?: Record<string, string>;
15
+ /** Working directory for the server process. */
16
+ cwd?: string;
17
+ /** HTTP headers for HTTP MCP server requests. */
18
+ headers?: Record<string, string>;
15
19
  }
16
20
 
17
21
  /**
@@ -55,6 +59,8 @@ export interface TestConfigOptions {
55
59
  use?: Record<string, unknown>;
56
60
  /** Visual regression testing configuration. */
57
61
  visual?: VisualConfig;
62
+ /** Server startup timeout in ms. */
63
+ timeout?: number;
58
64
  }
59
65
 
60
66
  /**
@@ -28,6 +28,7 @@ import { resolveSunpeakBin } from '../resolve-bin.mjs';
28
28
  * @param {string} [options.server.url] - HTTP server URL (alternative to command)
29
29
  * @param {Record<string, string>} [options.server.env] - Environment variables
30
30
  * @param {string} [options.server.cwd] - Working directory for the server process
31
+ * @param {Record<string, string>} [options.server.headers] - HTTP headers for HTTP MCP server requests
31
32
  * @param {string[]} [options.hosts] - Host shells to test (default: ['chatgpt', 'claude'])
32
33
  * @param {string} [options.testDir] - Test directory
33
34
  * @param {string} [options.simulationsDir] - Simulations directory for mock data
@@ -126,6 +127,12 @@ function buildInspectCommand({ server, port, sandboxPort, simulationsDir }) {
126
127
  parts.push(server.cwd.includes(' ') ? `--cwd "${server.cwd}"` : `--cwd ${server.cwd}`);
127
128
  }
128
129
 
130
+ if (server.headers) {
131
+ for (const [key, value] of Object.entries(server.headers)) {
132
+ parts.push(`--header ${shellQuote(`${key}: ${value}`)}`);
133
+ }
134
+ }
135
+
129
136
  if (simulationsDir) {
130
137
  parts.push(`--simulations ${simulationsDir}`);
131
138
  }
@@ -134,3 +141,7 @@ function buildInspectCommand({ server, port, sandboxPort, simulationsDir }) {
134
141
 
135
142
  return parts.join(' ');
136
143
  }
144
+
145
+ function shellQuote(value) {
146
+ return `'${String(value).replace(/'/g, "'\\''")}'`;
147
+ }
package/bin/sunpeak.js CHANGED
@@ -131,6 +131,7 @@ Inspector (works with any MCP server):
131
131
  sunpeak inspect Inspect any MCP server in the inspector
132
132
  --server, -s <url|cmd> MCP server URL or stdio command (required)
133
133
  --simulations <dir> Simulation JSON directory
134
+ --header, -H <header> HTTP header for HTTP MCP servers (repeatable)
134
135
 
135
136
  sunpeak --version Show version number
136
137
  Resources: ${resources.join(', ')} (comma/space separated)
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("../chunk-Cek0wNdY.cjs");
3
- const require_inspector = require("../inspector-DOmiG64-.cjs");
3
+ const require_inspector = require("../inspector-BGnxpdOn.cjs");
4
4
  const require_inspector_url = require("../inspector-url-BxScdDag.cjs");
5
5
  const require_discovery = require("../discovery-31_n0zcu.cjs");
6
6
  //#region src/chatgpt/index.ts
@@ -1,5 +1,5 @@
1
1
  import { Ct as __exportAll } from "../protocol-bhrz2H_E.js";
2
- import { _ as extractResourceCSP, f as ThemeProvider, g as IframeResource, p as useThemeContext, r as resolveServerToolResult, t as Inspector, v as McpAppHost, y as SCREEN_WIDTHS } from "../inspector-C6n8zap3.js";
2
+ import { _ as extractResourceCSP, f as ThemeProvider, g as IframeResource, p as useThemeContext, r as resolveServerToolResult, t as Inspector, v as McpAppHost, y as SCREEN_WIDTHS } from "../inspector-DvduUVNG.js";
3
3
  import { t as createInspectorUrl } from "../inspector-url-xUMGbWis.js";
4
4
  import { c as toPascalCase, i as findResourceKey, n as extractSimulationKey, r as findResourceDirs, s as getComponentName, t as extractResourceKey } from "../discovery-DOVner--.js";
5
5
  //#region src/chatgpt/index.ts
@@ -1,4 +1,4 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  require("../chunk-Cek0wNdY.cjs");
3
- const require_inspector = require("../inspector-DOmiG64-.cjs");
3
+ const require_inspector = require("../inspector-BGnxpdOn.cjs");
4
4
  exports.Inspector = require_inspector.Inspector;
@@ -1,2 +1,2 @@
1
- import { t as Inspector } from "../inspector-C6n8zap3.js";
1
+ import { t as Inspector } from "../inspector-DvduUVNG.js";
2
2
  export { Inspector };
@@ -0,0 +1,26 @@
1
+ import { App } from '@modelcontextprotocol/ext-apps';
2
+ export interface ToolData<TInput = unknown, TOutput = unknown> {
3
+ input: TInput | null;
4
+ inputPartial: TInput | null;
5
+ output: TOutput | null;
6
+ isError: boolean;
7
+ isLoading: boolean;
8
+ isCancelled: boolean;
9
+ cancelReason: string | null;
10
+ }
11
+ export interface ToolDataStore<TInput = unknown, TOutput = unknown> {
12
+ data: ToolData<TInput, TOutput>;
13
+ listeners: Set<() => void>;
14
+ }
15
+ declare module '@modelcontextprotocol/ext-apps' {
16
+ interface App {
17
+ /** @internal Eager store attached by AppProvider before connect(). */
18
+ __toolDataStore?: ToolDataStore;
19
+ }
20
+ }
21
+ /**
22
+ * Initialize the tool-data store on an App instance and wire its event
23
+ * listeners. Idempotent - returns the existing store if one is already
24
+ * attached.
25
+ */
26
+ export declare function initToolDataStore(app: App): ToolDataStore;
@@ -1,12 +1,6 @@
1
- export interface ToolData<TInput = unknown, TOutput = unknown> {
2
- input: TInput | null;
3
- inputPartial: TInput | null;
4
- output: TOutput | null;
5
- isError: boolean;
6
- isLoading: boolean;
7
- isCancelled: boolean;
8
- cancelReason: string | null;
9
- }
1
+ import { initToolDataStore, ToolData } from './tool-data-store';
2
+ export type { ToolData };
3
+ export { initToolDataStore };
10
4
  /**
11
5
  * Reactive access to tool input and output data from the MCP Apps host.
12
6
  *
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  require("../../chunk-Cek0wNdY.cjs");
3
- const require_use_app = require("../../use-app-DUdnDLP5.cjs");
3
+ const require_use_app = require("../../use-app-fizR-zbu.cjs");
4
4
  let react = require("react");
5
5
  //#region src/host/chatgpt/openai-types.ts
6
6
  /**
@@ -1,4 +1,4 @@
1
- import { t as useApp } from "../../use-app-Duar2Ipu.js";
1
+ import { t as useApp } from "../../use-app-CmrLc3wz.js";
2
2
  import { useCallback } from "react";
3
3
  //#region src/host/chatgpt/openai-types.ts
4
4
  /**