sunpeak 0.20.5 → 0.20.6

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.
@@ -149,11 +149,15 @@ export async function dev(projectRoot = process.cwd(), args = []) {
149
149
  const tailwindPlugin = await importFromProject(require, '@tailwindcss/vite');
150
150
  const tailwindcss = tailwindPlugin.default;
151
151
 
152
- // Parse port from args or use default
153
- let port = parseInt(process.env.PORT || '3000');
152
+ // Parse port from args or env. When neither is set, leave undefined so
153
+ // inspectServer auto-discovers a free port (and doesn't use strictPort,
154
+ // which would crash instead of falling back when port 3000 is busy).
155
+ let port = undefined;
154
156
  const portArgIndex = args.findIndex(arg => arg === '--port' || arg === '-p');
155
157
  if (portArgIndex !== -1 && args[portArgIndex + 1]) {
156
158
  port = parseInt(args[portArgIndex + 1]);
159
+ } else if (process.env.PORT) {
160
+ port = parseInt(process.env.PORT);
157
161
  }
158
162
 
159
163
  // Parse --no-begging flag
@@ -166,7 +170,7 @@ export async function dev(projectRoot = process.cwd(), args = []) {
166
170
  if (isProdTools) console.log('Prod Tools: MCP tool calls will use real handlers instead of simulation mocks');
167
171
  if (isProdResources) console.log('Prod Resources: resources will use production-built HTML from dist/');
168
172
 
169
- console.log(`Starting dev server on port ${port}...`);
173
+ console.log(`Starting dev server${port ? ` on port ${port}` : ''}...`);
170
174
 
171
175
  // Check if we're in the sunpeak workspace (directory is named "template")
172
176
  const isTemplate = basename(projectRoot) === 'template';
@@ -261,6 +265,8 @@ export async function dev(projectRoot = process.cwd(), args = []) {
261
265
 
262
266
  // Build path map for prod-tools handler reloading (re-imports on each call for HMR).
263
267
  // Also do an initial load to validate handlers and populate toolHandlerMap for the MCP server.
268
+ // Extract the raw Zod shape (schema export) so the MCP server can register tools
269
+ // with their actual inputSchema instead of z.object({}).passthrough().
264
270
  const toolHandlerMap = new Map();
265
271
  for (const [toolName, { tool, path: toolPath }] of toolMap) {
266
272
  void tool; // Used for metadata; handler loaded unconditionally
@@ -268,7 +274,15 @@ export async function dev(projectRoot = process.cwd(), args = []) {
268
274
  try {
269
275
  const mod = await toolLoaderServer.ssrLoadModule(`./${relativePath}`);
270
276
  if (typeof mod.default === 'function') {
271
- toolHandlerMap.set(toolName, { handler: mod.default, outputSchema: mod.outputSchema });
277
+ toolHandlerMap.set(toolName, {
278
+ handler: mod.default,
279
+ outputSchema: mod.outputSchema,
280
+ // The raw Zod shape from the tool file (e.g., { query: z.string(), limit: z.number() }).
281
+ // Passed to the MCP server so tools/list reports actual parameter schemas instead of
282
+ // empty objects. The MCP SDK duck-types Zod values (checks for parse/safeParse) so
283
+ // this works across module instances.
284
+ schema: mod.schema,
285
+ });
272
286
  }
273
287
  } catch (err) {
274
288
  console.warn(`Warning: Could not load handler for tool "${toolName}" (${relativePath}):\n ${err.message}`);
@@ -327,6 +341,10 @@ export async function dev(projectRoot = process.cwd(), args = []) {
327
341
  ...(toolHandlerMap.has(toolName) ? {
328
342
  handler: toolHandlerMap.get(toolName).handler,
329
343
  } : {}),
344
+ // Attach the raw Zod shape so the MCP server registers tools with real schemas.
345
+ ...(toolHandlerMap.has(toolName) && toolHandlerMap.get(toolName).schema ? {
346
+ inputSchema: toolHandlerMap.get(toolName).schema,
347
+ } : {}),
330
348
  });
331
349
  }
332
350
 
@@ -346,6 +364,7 @@ export async function dev(projectRoot = process.cwd(), args = []) {
346
364
  tool: { name: toolName, ...tool },
347
365
  ...(handlerInfo?.outputSchema ? { outputSchema: handlerInfo.outputSchema } : {}),
348
366
  ...(handlerInfo ? { handler: handlerInfo.handler } : {}),
367
+ ...(handlerInfo?.schema ? { inputSchema: handlerInfo.schema } : {}),
349
368
  });
350
369
  }
351
370
 
@@ -1429,8 +1429,14 @@ export async function inspectServer(opts) {
1429
1429
  ownsSandbox = true;
1430
1430
  }
1431
1431
 
1432
- // Determine server port
1433
- const port = preferredPort || Number(process.env.PORT) || (await getPort(3000));
1432
+ // Determine server port.
1433
+ // Track whether the port was explicitly requested (via option or env var) vs
1434
+ // auto-discovered. When explicit, use strictPort so Vite fails fast instead of
1435
+ // silently picking another port — Playwright tests set baseURL from the same port
1436
+ // and a silent fallback causes ERR_CONNECTION_REFUSED. When auto-discovered,
1437
+ // the port is guaranteed free so strictPort is irrelevant.
1438
+ const explicitPort = preferredPort || (process.env.PORT ? Number(process.env.PORT) : null);
1439
+ const port = explicitPort || (await getPort(3000));
1434
1440
 
1435
1441
  // Import Vite
1436
1442
  const { createServer } = await import('vite');
@@ -1562,6 +1568,12 @@ export async function inspectServer(opts) {
1562
1568
  ],
1563
1569
  server: {
1564
1570
  port,
1571
+ // When the port was explicitly requested (Playwright tests, --port flag, PORT env),
1572
+ // fail fast if busy instead of silently picking another port. Playwright tests
1573
+ // configure baseURL from the same port, so a silent fallback causes
1574
+ // ERR_CONNECTION_REFUSED. When auto-discovered via getPort(), the port is
1575
+ // already free so this doesn't apply.
1576
+ ...(explicitPort ? { strictPort: true } : {}),
1565
1577
  // Listen on all interfaces so both 127.0.0.1 (used by Playwright tests)
1566
1578
  // and localhost (used by interactive browsing) connect successfully.
1567
1579
  // Without this, Vite defaults to localhost which may resolve to IPv6-only
@@ -418,11 +418,12 @@ async function runEvals(args) {
418
418
 
419
419
  const warnings = validateApiKeys(configModels);
420
420
  if (warnings.length > 0) {
421
- console.log('');
421
+ console.error('');
422
422
  for (const w of warnings) {
423
- console.warn(`⚠ ${w}`);
423
+ console.error(`✗ ${w}`);
424
424
  }
425
- console.log('');
425
+ console.error('');
426
+ return 1;
426
427
  }
427
428
  }
428
429
 
@@ -112,9 +112,35 @@ export async function discoverAndConvertTools(client) {
112
112
  const tools = {};
113
113
 
114
114
  for (const t of mcpTools) {
115
+ // Clean up the MCP inputSchema for AI provider compatibility.
116
+ // OpenAI rejects $schema, additionalProperties: {} (empty schema has no type),
117
+ // and other JSON Schema features that MCP servers may include.
118
+ const rawSchema = t.inputSchema || { type: 'object', properties: {} };
119
+ const cleanSchema = { ...rawSchema };
120
+ delete cleanSchema.$schema;
121
+ if (
122
+ cleanSchema.additionalProperties != null &&
123
+ typeof cleanSchema.additionalProperties === 'object' &&
124
+ Object.keys(cleanSchema.additionalProperties).length === 0
125
+ ) {
126
+ // Empty additionalProperties ({}) causes OpenAI to report type: "None".
127
+ // Remove it so the schema is a plain { type: "object", properties: {...} }.
128
+ delete cleanSchema.additionalProperties;
129
+ }
130
+ if (!cleanSchema.type) cleanSchema.type = 'object';
131
+ if (!cleanSchema.properties) cleanSchema.properties = {};
132
+ // Remove `required` so the model isn't forced to ask the user for every
133
+ // parameter before calling a tool. Eval prompts are intentionally vague
134
+ // ("show me photo albums") and the model should call the tool with
135
+ // reasonable defaults, not refuse because required fields are missing.
136
+ delete cleanSchema.required;
137
+
115
138
  tools[t.name] = aiTool({
116
139
  description: t.description || '',
117
- parameters: jsonSchema(t.inputSchema || { type: 'object', properties: {} }),
140
+ // Set both so the tool works with ai v4/v5 (reads `parameters`)
141
+ // and ai v6 (reads `inputSchema`). tool() passes through both.
142
+ inputSchema: jsonSchema(cleanSchema),
143
+ parameters: jsonSchema(cleanSchema),
118
144
  execute: async (args) => {
119
145
  const result = await client.callTool({ name: t.name, arguments: args });
120
146
  // Return a simplified version for the model to consume
@@ -44,7 +44,12 @@ export async function resolveModel(modelId) {
44
44
  // that creates model instances: openai('gpt-4o'), anthropic('claude-...'), google('gemini-...')
45
45
  if (pkg === '@ai-sdk/openai') {
46
46
  const { openai } = provider;
47
- return openai(modelId);
47
+ // @ai-sdk/openai v3 defaults to the Responses API, which requires strict
48
+ // JSON Schema (additionalProperties: false at every level, all properties
49
+ // required) — incompatible with arbitrary MCP server schemas. Use .chat()
50
+ // (Chat Completions API) when available. v1/v2 default to Chat Completions
51
+ // already and may not have .chat(), so fall back to the default.
52
+ return typeof openai.chat === 'function' ? openai.chat(modelId) : openai(modelId);
48
53
  }
49
54
  if (pkg === '@ai-sdk/anthropic') {
50
55
  const { anthropic } = provider;
@@ -5,8 +5,6 @@
5
5
  * Produces a config with per-host Playwright projects, sensible defaults for
6
6
  * MCP App testing, and a webServer entry to launch the inspector backend.
7
7
  */
8
- import { getPortSync } from '../get-port.mjs';
9
-
10
8
  /**
11
9
  * @param {Object} options
12
10
  * @param {string[]} options.hosts - Host shells to create projects for
@@ -63,10 +61,19 @@ export function createBaseConfig({ hosts, testDir, webServer, port, use, globalS
63
61
  /**
64
62
  * Resolve ports for the inspector and sandbox proxy.
65
63
  * Respects env vars for CI where validate.mjs assigns unique ports.
64
+ *
65
+ * Uses FIXED default ports (no dynamic probing) so all Playwright workers
66
+ * resolve the same baseURL. Dynamic port probing (getPortSync) caused flaky
67
+ * tests: the main process would pick port X, start the webServer on it, then
68
+ * worker processes re-evaluating the config would find X occupied and resolve
69
+ * to random ports Y/Z — causing ERR_CONNECTION_REFUSED.
70
+ *
71
+ * If the default port is busy, Playwright's reuseExistingServer (local) reuses
72
+ * it, or strictPort (CI) fails fast with a clear error.
66
73
  */
67
74
  export function resolvePorts() {
68
- const port = parsePort(process.env.SUNPEAK_TEST_PORT) ?? getPortSync(6776);
69
- const sandboxPort = parsePort(process.env.SUNPEAK_SANDBOX_PORT) ?? getPortSync(24680);
75
+ const port = parsePort(process.env.SUNPEAK_TEST_PORT) ?? 6776;
76
+ const sandboxPort = parsePort(process.env.SUNPEAK_SANDBOX_PORT) ?? 24680;
70
77
  return { port, sandboxPort };
71
78
  }
72
79
 
@@ -9277,6 +9277,26 @@ function injectViteCSP(existingMeta) {
9277
9277
  };
9278
9278
  }
9279
9279
  var startupTimestamp = Date.now().toString(36);
9280
+ /**
9281
+ * Make all properties in a Zod raw shape optional.
9282
+ *
9283
+ * Tool schemas from `src/tools/*.ts` have required fields by default (e.g.
9284
+ * `z.string()`). The dev server needs to accept partial args because:
9285
+ * - Mock mode returns fixture data regardless of args
9286
+ * - Models may not send every required field
9287
+ * - The inspector's "Re-run" button sends args from the last run
9288
+ *
9289
+ * Making fields optional preserves property types/descriptions in `tools/list`
9290
+ * (so models know what args to send) while letting the SDK accept any subset.
9291
+ */
9292
+ function makeSchemaOptional(shape) {
9293
+ const optional = {};
9294
+ for (const [key, value] of Object.entries(shape)) {
9295
+ const v = value;
9296
+ optional[key] = typeof v.optional === "function" ? v.optional() : value;
9297
+ }
9298
+ return optional;
9299
+ }
9280
9300
  function createAppServer(config, simulations, viteMode) {
9281
9301
  const { name = "sunpeak-app", version = "0.1.0", serverInfo } = config;
9282
9302
  const mcpServer = new McpServer({
@@ -9350,6 +9370,7 @@ function createAppServer(config, simulations, viteMode) {
9350
9370
  });
9351
9371
  resourceHandles.set(resourceName, handle);
9352
9372
  }
9373
+ const toolInputSchema = simulation.inputSchema ? makeSchemaOptional(simulation.inputSchema) : zod.z.object({}).passthrough();
9353
9374
  const fullToolMeta = {
9354
9375
  ...toolMeta,
9355
9376
  ui: {
@@ -9359,7 +9380,7 @@ function createAppServer(config, simulations, viteMode) {
9359
9380
  };
9360
9381
  const toolHandle = hZ(mcpServer, tool.name, {
9361
9382
  description: tool.description,
9362
- inputSchema: zod.z.object({}).passthrough(),
9383
+ inputSchema: toolInputSchema,
9363
9384
  ...simulation.outputSchema ? { outputSchema: simulation.outputSchema } : {},
9364
9385
  annotations: tool.annotations,
9365
9386
  _meta: fullToolMeta
@@ -9424,7 +9445,7 @@ function createAppServer(config, simulations, viteMode) {
9424
9445
  const realHandler = simulation.handler;
9425
9446
  const plainToolConfig = {
9426
9447
  description: tool.description,
9427
- inputSchema: zod.z.object({}).passthrough(),
9448
+ inputSchema: simulation.inputSchema ? makeSchemaOptional(simulation.inputSchema) : zod.z.object({}).passthrough(),
9428
9449
  ...simulation.outputSchema ? { outputSchema: simulation.outputSchema } : {},
9429
9450
  annotations: tool.annotations,
9430
9451
  _meta: toolMeta