sunpeak 0.20.5 → 0.20.7
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/bin/commands/dev.mjs +23 -4
- package/bin/commands/inspect.mjs +14 -2
- package/bin/commands/test-init.mjs +21 -61
- package/bin/commands/test.mjs +15 -5
- package/bin/lib/eval/eval-runner.mjs +27 -1
- package/bin/lib/eval/model-registry.mjs +6 -1
- package/bin/lib/test/base-config.mjs +11 -4
- package/dist/mcp/index.cjs +23 -2
- package/dist/mcp/index.cjs.map +1 -1
- package/dist/mcp/index.js +23 -2
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/types.d.ts +1 -0
- package/package.json +1 -1
- package/template/dist/albums/albums.json +1 -1
- package/template/dist/carousel/carousel.json +1 -1
- package/template/dist/map/map.json +1 -1
- package/template/dist/review/review.json +1 -1
- package/template/tests/evals/albums.eval.ts +1 -1
- package/template/tests/evals/map.eval.ts +1 -1
- package/template/tests/evals/review.eval.ts +4 -2
package/bin/commands/dev.mjs
CHANGED
|
@@ -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
|
|
153
|
-
|
|
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, {
|
|
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
|
|
package/bin/commands/inspect.mjs
CHANGED
|
@@ -1429,8 +1429,14 @@ export async function inspectServer(opts) {
|
|
|
1429
1429
|
ownsSandbox = true;
|
|
1430
1430
|
}
|
|
1431
1431
|
|
|
1432
|
-
// Determine server port
|
|
1433
|
-
|
|
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
|
|
@@ -61,12 +61,14 @@ export const defaultDeps = {
|
|
|
61
61
|
* - JS/TS projects: root-level config + test files
|
|
62
62
|
* - sunpeak projects: migrate to defineConfig()
|
|
63
63
|
*
|
|
64
|
-
* Scaffolds
|
|
64
|
+
* Scaffolds 4 test types:
|
|
65
65
|
* 1. E2E tests — Playwright-based inspector tests (mcp fixture)
|
|
66
66
|
* 2. Visual regression — Screenshot comparison via result.screenshot()
|
|
67
67
|
* 3. Live tests — Test against real ChatGPT/Claude hosts
|
|
68
68
|
* 4. Evals — Multi-model tool calling reliability tests
|
|
69
|
-
*
|
|
69
|
+
*
|
|
70
|
+
* Unit tests are not scaffolded here — they're part of the sunpeak app
|
|
71
|
+
* framework (`sunpeak new`) where tool handlers can be imported directly.
|
|
70
72
|
*/
|
|
71
73
|
export async function testInit(args = [], deps = defaultDeps) {
|
|
72
74
|
const d = { ...defaultDeps, ...deps };
|
|
@@ -532,56 +534,6 @@ export default defineLiveConfig({${serverOption}
|
|
|
532
534
|
d.log.success(`Created ${liveDir}/ with live test config and example.`);
|
|
533
535
|
}
|
|
534
536
|
|
|
535
|
-
/**
|
|
536
|
-
* Scaffold a unit test example for JS/TS projects.
|
|
537
|
-
* @param {string} filePath - Full path to the unit test file
|
|
538
|
-
* @param {object} d - Dependencies
|
|
539
|
-
*/
|
|
540
|
-
function scaffoldUnitTest(filePath, d) {
|
|
541
|
-
if (d.existsSync(filePath)) {
|
|
542
|
-
d.log.info('Unit test already exists. Skipping.');
|
|
543
|
-
return;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
d.mkdirSync(dirname(filePath), { recursive: true });
|
|
547
|
-
|
|
548
|
-
d.writeFileSync(
|
|
549
|
-
filePath,
|
|
550
|
-
`import { describe, it, expect } from 'vitest';
|
|
551
|
-
|
|
552
|
-
/**
|
|
553
|
-
* Unit tests for your MCP tool handlers.
|
|
554
|
-
*
|
|
555
|
-
* Import your tool handler directly and test its input/output
|
|
556
|
-
* without starting the MCP server or inspector.
|
|
557
|
-
*
|
|
558
|
-
* Run with: npx sunpeak test --unit
|
|
559
|
-
*
|
|
560
|
-
* To set up vitest, add it to your devDependencies:
|
|
561
|
-
* npm install -D vitest
|
|
562
|
-
*
|
|
563
|
-
* Uncomment and customize the tests below for your tools.
|
|
564
|
-
*/
|
|
565
|
-
|
|
566
|
-
// import handler, { tool, schema } from '../../src/tools/your-tool';
|
|
567
|
-
// const extra = {} as Parameters<typeof handler>[1];
|
|
568
|
-
|
|
569
|
-
// describe('your tool', () => {
|
|
570
|
-
// it('returns expected output', async () => {
|
|
571
|
-
// const result = await handler({ key: 'value' }, extra);
|
|
572
|
-
// expect(result.structuredContent).toBeDefined();
|
|
573
|
-
// });
|
|
574
|
-
//
|
|
575
|
-
// it('exports correct tool config', () => {
|
|
576
|
-
// expect(tool.title).toBe('Your Tool');
|
|
577
|
-
// expect(tool.annotations?.readOnlyHint).toBe(true);
|
|
578
|
-
// });
|
|
579
|
-
// });
|
|
580
|
-
`
|
|
581
|
-
);
|
|
582
|
-
d.log.success(`Created ${filePath}`);
|
|
583
|
-
}
|
|
584
|
-
|
|
585
537
|
async function initExternalProject(cliServer, d) {
|
|
586
538
|
d.log.info('Detected non-JS project. Creating self-contained test directory.');
|
|
587
539
|
|
|
@@ -715,6 +667,22 @@ async function initJsProject(cliServer, d) {
|
|
|
715
667
|
const server = await getServerConfig(cliServer, d);
|
|
716
668
|
const cwd = d.cwd();
|
|
717
669
|
|
|
670
|
+
// Ensure "type": "module" — sunpeak exports are ESM-only and Playwright's
|
|
671
|
+
// CJS resolver won't find them without it.
|
|
672
|
+
const pkgPath = join(cwd, 'package.json');
|
|
673
|
+
if (d.existsSync(pkgPath)) {
|
|
674
|
+
try {
|
|
675
|
+
const pkg = JSON.parse(d.readFileSync(pkgPath, 'utf-8'));
|
|
676
|
+
if (pkg.type !== 'module') {
|
|
677
|
+
pkg.type = 'module';
|
|
678
|
+
d.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
679
|
+
d.log.success('Set "type": "module" in package.json (required for sunpeak imports)');
|
|
680
|
+
}
|
|
681
|
+
} catch {
|
|
682
|
+
d.log.warn('Could not read package.json. Make sure "type": "module" is set.');
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
718
686
|
// Create playwright.config.ts
|
|
719
687
|
const configPath = join(cwd, 'playwright.config.ts');
|
|
720
688
|
if (d.existsSync(configPath)) {
|
|
@@ -775,19 +743,15 @@ test('server exposes tools', async ({ mcp }) => {
|
|
|
775
743
|
// 4. Eval boilerplate
|
|
776
744
|
scaffoldEvals(join(cwd, 'tests', 'evals'), { server, d });
|
|
777
745
|
|
|
778
|
-
// 5. Unit test
|
|
779
|
-
scaffoldUnitTest(join(cwd, 'tests', 'unit', 'example.test.ts'), d);
|
|
780
|
-
|
|
781
746
|
if (server.type === 'later') {
|
|
782
747
|
d.log.warn('Server not configured. Edit playwright.config.ts before running tests.');
|
|
783
748
|
}
|
|
784
749
|
const pkgMgr = d.detectPackageManager();
|
|
785
750
|
d.log.step('Next steps:');
|
|
786
|
-
d.log.message(` ${pkgMgr} add -D sunpeak @playwright/test
|
|
751
|
+
d.log.message(` ${pkgMgr} add -D sunpeak @playwright/test`);
|
|
787
752
|
d.log.message(` ${pkgMgr} exec playwright install chromium`);
|
|
788
753
|
d.log.message('');
|
|
789
754
|
d.log.message(' npx sunpeak test # E2E tests');
|
|
790
|
-
d.log.message(' npx sunpeak test --unit # Unit tests (vitest)');
|
|
791
755
|
d.log.message(' npx sunpeak test --visual # Visual regression');
|
|
792
756
|
d.log.message(' npx sunpeak test --live # Live tests against real hosts');
|
|
793
757
|
d.log.message(' npx sunpeak test --eval # Multi-model evals');
|
|
@@ -833,14 +797,10 @@ export default defineConfig();
|
|
|
833
797
|
// 3. Eval boilerplate
|
|
834
798
|
scaffoldEvals(join(cwd, 'tests', 'evals'), { isSunpeak: true, d });
|
|
835
799
|
|
|
836
|
-
// 4. Unit test
|
|
837
|
-
scaffoldUnitTest(join(cwd, 'tests', 'unit', 'example.test.ts'), d);
|
|
838
|
-
|
|
839
800
|
d.log.step('Scaffolded test types:');
|
|
840
801
|
d.log.message(' tests/e2e/visual.test.ts — Visual regression (npx sunpeak test --visual)');
|
|
841
802
|
d.log.message(' tests/live/ — Live host tests (npx sunpeak test --live)');
|
|
842
803
|
d.log.message(' tests/evals/ — Multi-model evals (npx sunpeak test --eval)');
|
|
843
|
-
d.log.message(' tests/unit/example.test.ts — Unit tests (npx sunpeak test --unit)');
|
|
844
804
|
d.log.message('');
|
|
845
805
|
d.log.message(' Migrate existing e2e tests:');
|
|
846
806
|
d.log.message(' Replace: import { test, expect } from "@playwright/test"');
|
package/bin/commands/test.mjs
CHANGED
|
@@ -59,8 +59,17 @@ export async function runTest(args) {
|
|
|
59
59
|
const results = [];
|
|
60
60
|
|
|
61
61
|
if (runUnit) {
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
// Only run unit tests if vitest is available (app framework projects have it,
|
|
63
|
+
// standalone testing framework projects don't).
|
|
64
|
+
const hasVitest = existsSync(join(process.cwd(), 'node_modules', '.bin', 'vitest'));
|
|
65
|
+
if (hasVitest) {
|
|
66
|
+
const code = await runChild('pnpm', ['exec', 'vitest', 'run', ...filteredArgs]);
|
|
67
|
+
results.push({ suite: 'unit', code });
|
|
68
|
+
} else if (isUnit) {
|
|
69
|
+
// Only warn if the user explicitly asked for --unit
|
|
70
|
+
console.error('vitest is not installed. Install it with: npm add -D vitest');
|
|
71
|
+
results.push({ suite: 'unit', code: 1 });
|
|
72
|
+
}
|
|
64
73
|
}
|
|
65
74
|
|
|
66
75
|
if (runE2e) {
|
|
@@ -418,11 +427,12 @@ async function runEvals(args) {
|
|
|
418
427
|
|
|
419
428
|
const warnings = validateApiKeys(configModels);
|
|
420
429
|
if (warnings.length > 0) {
|
|
421
|
-
console.
|
|
430
|
+
console.error('');
|
|
422
431
|
for (const w of warnings) {
|
|
423
|
-
console.
|
|
432
|
+
console.error(`✗ ${w}`);
|
|
424
433
|
}
|
|
425
|
-
console.
|
|
434
|
+
console.error('');
|
|
435
|
+
return 1;
|
|
426
436
|
}
|
|
427
437
|
}
|
|
428
438
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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) ??
|
|
69
|
-
const sandboxPort = parsePort(process.env.SUNPEAK_SANDBOX_PORT) ??
|
|
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
|
|
package/dist/mcp/index.cjs
CHANGED
|
@@ -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:
|
|
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
|