sunpeak 0.15.4 → 0.16.2
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/README.md +53 -49
- package/bin/commands/build.mjs +119 -6
- package/bin/commands/dev.mjs +168 -27
- package/bin/commands/new.mjs +13 -3
- package/bin/commands/start.mjs +215 -0
- package/bin/lib/extract-resource.mjs +1 -1
- package/bin/lib/extract-tool.mjs +78 -0
- package/bin/lib/patterns.mjs +2 -26
- package/bin/sunpeak.js +11 -1
- package/dist/chatgpt/index.cjs +3 -6
- package/dist/chatgpt/index.cjs.map +1 -1
- package/dist/chatgpt/index.d.ts +1 -1
- package/dist/chatgpt/index.js +6 -9
- package/dist/claude/index.cjs +1 -1
- package/dist/claude/index.js +1 -1
- package/dist/discovery-CH80W5l9.js +217 -0
- package/dist/discovery-CH80W5l9.js.map +1 -0
- package/dist/discovery-DmB8_4QL.cjs +216 -0
- package/dist/discovery-DmB8_4QL.cjs.map +1 -0
- package/dist/{index-Cngntkp2.cjs → index-Bll1bszc.cjs} +3 -6
- package/dist/{index-Cngntkp2.cjs.map → index-Bll1bszc.cjs.map} +1 -1
- package/dist/{index-Ce_5ZIdJ.js → index-CACtnwu2.js} +3 -6
- package/dist/{index-Ce_5ZIdJ.js.map → index-CACtnwu2.js.map} +1 -1
- package/dist/{index-CutQgPzR.js → index-CLcr8IyR.js} +3 -6
- package/dist/index-CLcr8IyR.js.map +1 -0
- package/dist/{index-B0dxRJvS.cjs → index-CaQmwZJc.cjs} +3 -6
- package/dist/index-CaQmwZJc.cjs.map +1 -0
- package/dist/index.cjs +49 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3405 -3362
- package/dist/index.js.map +1 -1
- package/dist/lib/discovery-cli.cjs +58 -5
- package/dist/lib/discovery-cli.cjs.map +1 -1
- package/dist/lib/discovery-cli.d.ts +3 -2
- package/dist/lib/discovery-cli.js +61 -8
- package/dist/lib/discovery-cli.js.map +1 -1
- package/dist/lib/discovery.d.ts +42 -43
- package/dist/lib/extract-tool.d.ts +12 -0
- package/dist/mcp/favicon.d.ts +1 -1
- package/dist/mcp/index.cjs +1582 -5
- package/dist/mcp/index.cjs.map +1 -1
- package/dist/mcp/index.d.ts +3 -1
- package/dist/mcp/index.js +1583 -6
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/production-server.d.ts +156 -0
- package/dist/mcp/types.d.ts +24 -1
- package/dist/platform/chatgpt/index.cjs +1 -1
- package/dist/platform/chatgpt/index.js +1 -1
- package/dist/{protocol-DFbsCx7E.js → protocol-BD5jDQEx.js} +8 -1
- package/dist/{protocol-DFbsCx7E.js.map → protocol-BD5jDQEx.js.map} +1 -1
- package/dist/{protocol-CL4_Npj5.cjs → protocol-BOjXuK6l.cjs} +8 -1
- package/dist/{protocol-CL4_Npj5.cjs.map → protocol-BOjXuK6l.cjs.map} +1 -1
- package/dist/simulator/index.cjs +2 -5
- package/dist/simulator/index.cjs.map +1 -1
- package/dist/simulator/index.d.ts +1 -1
- package/dist/simulator/index.js +5 -8
- package/dist/simulator/simulator-url.d.ts +9 -9
- package/dist/{simulator-CxrtnguM.js → simulator-B7rw83zP.js} +9 -3
- package/dist/{simulator-CxrtnguM.js.map → simulator-B7rw83zP.js.map} +1 -1
- package/dist/{simulator-DcfQBRXE.cjs → simulator-DjZNa1MI.cjs} +9 -3
- package/dist/{simulator-DcfQBRXE.cjs.map → simulator-DjZNa1MI.cjs.map} +1 -1
- package/dist/simulator-url-CuLqtnSS.js.map +1 -1
- package/dist/simulator-url-rgg_KYOg.cjs.map +1 -1
- package/dist/types/resource-config.d.ts +7 -5
- package/dist/{use-app-BnoSPiUT.cjs → use-app-BpAJqzdE.cjs} +50 -21
- package/dist/{use-app-BnoSPiUT.cjs.map → use-app-BpAJqzdE.cjs.map} +1 -1
- package/dist/{use-app-D_TeaMFG.js → use-app-WOUdh1PR.js} +52 -23
- package/dist/{use-app-D_TeaMFG.js.map → use-app-WOUdh1PR.js.map} +1 -1
- package/package.json +1 -1
- package/template/.sunpeak/dev.tsx +8 -4
- package/template/.sunpeak/resource-loader.tsx +2 -1
- package/template/README.md +27 -22
- package/template/package.json +3 -1
- package/template/src/resources/albums/{albums-resource.test.tsx → albums.test.tsx} +1 -1
- package/template/src/resources/albums/{albums-resource.tsx → albums.tsx} +0 -1
- package/template/src/resources/carousel/{carousel-resource.test.tsx → carousel.test.tsx} +1 -1
- package/template/src/resources/carousel/{carousel-resource.tsx → carousel.tsx} +0 -1
- package/template/src/resources/index.ts +4 -4
- package/template/src/resources/map/{map-resource.test.tsx → map.test.tsx} +1 -1
- package/template/src/resources/map/{map-resource.tsx → map.tsx} +0 -1
- package/template/src/resources/review/{review-resource.test.tsx → review.test.tsx} +1 -1
- package/template/src/resources/review/{review-resource.tsx → review.tsx} +1 -2
- package/template/src/server.ts +17 -0
- package/template/src/tools/review-diff.ts +24 -0
- package/template/src/tools/review-post.ts +26 -0
- package/template/src/tools/review-purchase.ts +31 -0
- package/template/src/tools/show-albums.ts +22 -0
- package/template/src/tools/show-carousel.ts +25 -0
- package/template/src/tools/show-map.ts +29 -0
- package/template/tests/e2e/albums.spec.ts +6 -6
- package/template/tests/e2e/carousel.spec.ts +6 -6
- package/template/tests/e2e/map.spec.ts +11 -11
- package/template/tests/simulations/{review/review-diff-simulation.json → review-diff.json} +1 -31
- package/template/tests/simulations/{review/review-post-simulation.json → review-post.json} +1 -37
- package/template/tests/simulations/{review/review-purchase-simulation.json → review-purchase.json} +1 -38
- package/template/tests/simulations/{albums/albums-show-simulation.json → show-albums.json} +1 -24
- package/template/tests/simulations/{carousel/carousel-show-simulation.json → show-carousel.json} +1 -24
- package/template/tests/simulations/{map/map-show-simulation.json → show-map.json} +1 -35
- package/dist/discovery-CRR3SlyI.cjs +0 -156
- package/dist/discovery-CRR3SlyI.cjs.map +0 -1
- package/dist/discovery-DzV3HLXs.js +0 -157
- package/dist/discovery-DzV3HLXs.js.map +0 -1
- package/dist/index-B0dxRJvS.cjs.map +0 -1
- package/dist/index-CutQgPzR.js.map +0 -1
package/README.md
CHANGED
|
@@ -61,21 +61,26 @@ Next.js for MCP Apps. Using an example App `my-app` with a `Review` UI (MCP reso
|
|
|
61
61
|
|
|
62
62
|
```bash
|
|
63
63
|
my-app/
|
|
64
|
-
├── src/
|
|
65
|
-
│
|
|
66
|
-
│
|
|
64
|
+
├── src/
|
|
65
|
+
│ ├── resources/
|
|
66
|
+
│ │ └── review/
|
|
67
|
+
│ │ └── review.tsx # Review UI component + resource metadata.
|
|
68
|
+
│ ├── tools/
|
|
69
|
+
│ │ ├── review-diff.ts # Tool with handler, schema, and resource reference.
|
|
70
|
+
│ │ └── review-post.ts # Multiple tools can share one resource.
|
|
71
|
+
│ └── server.ts # Optional: auth, server config.
|
|
67
72
|
├── tests/simulations/
|
|
68
|
-
│
|
|
69
|
-
│
|
|
70
|
-
│ └── review-{scenario2}-simulation.json # Mock state for testing.
|
|
73
|
+
│ ├── review-diff.json # Mock state for testing.
|
|
74
|
+
│ └── review-post.json # Mock state for testing.
|
|
71
75
|
└── package.json
|
|
72
76
|
```
|
|
73
77
|
|
|
74
78
|
1. Project scaffold: Complete development setup with the `sunpeak` library.
|
|
75
79
|
2. UI components: Production-ready components following MCP App design guidelines.
|
|
76
80
|
3. Convention over configuration:
|
|
77
|
-
1. Create a UI by creating a
|
|
78
|
-
2. Create
|
|
81
|
+
1. Create a UI by creating a `.tsx` file in `src/resources/{name}/` that exports a `ResourceConfig` and a React component ([example below](#resource-component)).
|
|
82
|
+
2. Create a tool by creating a `.ts` file in `src/tools/` that exports `tool` (metadata with resource reference), `schema` (Zod), and a `default` handler ([example below](#tool-file)).
|
|
83
|
+
3. Create test state (`Simulation`s) by creating a `.json` file in `tests/simulations/` ([example below](#simulation)).
|
|
79
84
|
|
|
80
85
|
### The `sunpeak` CLI
|
|
81
86
|
|
|
@@ -83,7 +88,8 @@ Commands for managing MCP Apps:
|
|
|
83
88
|
|
|
84
89
|
- `sunpeak new [name] [resources]` - Create a new project
|
|
85
90
|
- `sunpeak dev` - Start dev server with MCP endpoint and live simulator
|
|
86
|
-
- `sunpeak build` - Build resources for production
|
|
91
|
+
- `sunpeak build` - Build resources and compile tools for production
|
|
92
|
+
- `sunpeak start` - Start the production MCP server (real handlers, auth, Zod validation)
|
|
87
93
|
- `sunpeak upgrade` - Upgrade sunpeak to latest version
|
|
88
94
|
|
|
89
95
|
## Example App
|
|
@@ -92,23 +98,15 @@ Example `Resource`, `Simulation`, and testing file (using the `Simulator`) for a
|
|
|
92
98
|
|
|
93
99
|
### `Resource` Component
|
|
94
100
|
|
|
95
|
-
```bash
|
|
96
|
-
my-app/
|
|
97
|
-
├── src/resources/
|
|
98
|
-
│ └── review/
|
|
99
|
-
│ └── review-resource.tsx # This!
|
|
100
|
-
```
|
|
101
|
-
|
|
102
101
|
Each resource `.tsx` file exports both the React component and the MCP resource metadata:
|
|
103
102
|
|
|
104
103
|
```tsx
|
|
105
|
-
// src/resources/review/review
|
|
104
|
+
// src/resources/review/review.tsx
|
|
106
105
|
|
|
107
106
|
import { useToolData } from 'sunpeak';
|
|
108
107
|
import type { ResourceConfig } from 'sunpeak';
|
|
109
108
|
|
|
110
109
|
export const resource: ResourceConfig = {
|
|
111
|
-
name: 'review',
|
|
112
110
|
description: 'Visualize and review a code change',
|
|
113
111
|
_meta: { ui: { csp: { resourceDomains: ['https://cdn.example.com'] } } },
|
|
114
112
|
};
|
|
@@ -120,48 +118,54 @@ export function ReviewResource() {
|
|
|
120
118
|
}
|
|
121
119
|
```
|
|
122
120
|
|
|
123
|
-
###
|
|
121
|
+
### Tool File
|
|
124
122
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
│ └── review-{scenario2}-simulation.json # These!
|
|
130
|
-
```
|
|
123
|
+
Each tool `.ts` file exports metadata (with a direct resource reference), a Zod schema, and a handler:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
// src/tools/review-diff.ts
|
|
131
127
|
|
|
132
|
-
|
|
128
|
+
import { z } from 'zod';
|
|
129
|
+
import type { AppToolConfig, ToolHandlerExtra } from 'sunpeak/mcp';
|
|
133
130
|
|
|
134
|
-
|
|
131
|
+
export const tool: AppToolConfig = {
|
|
132
|
+
resource: 'review',
|
|
133
|
+
title: 'Diff Review',
|
|
134
|
+
description: 'Show a review dialog for a proposed code diff',
|
|
135
|
+
annotations: { readOnlyHint: false },
|
|
136
|
+
_meta: { ui: { visibility: ['model', 'app'] } },
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export const schema = {
|
|
140
|
+
changesetId: z.string().describe('Unique identifier for the changeset'),
|
|
141
|
+
title: z.string().describe('Title describing the changes'),
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export default async function (args: Record<string, unknown>, extra: ToolHandlerExtra) {
|
|
145
|
+
return { structuredContent: { title: args.title, sections: [] } };
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### `Simulation`
|
|
135
150
|
|
|
136
|
-
|
|
151
|
+
Simulation files provide fixture data for testing. Each references a tool by filename and contains the mock input/output:
|
|
137
152
|
|
|
138
153
|
```jsonc
|
|
139
|
-
// tests/simulations/review-diff
|
|
154
|
+
// tests/simulations/review-diff.json
|
|
140
155
|
|
|
141
156
|
{
|
|
142
|
-
//
|
|
143
|
-
"
|
|
144
|
-
"name": "review-diff",
|
|
145
|
-
"description": "Show a review dialog for a proposed code diff",
|
|
146
|
-
"inputSchema": { "type": "object", "properties": {}, "additionalProperties": false },
|
|
147
|
-
"title": "Diff Review",
|
|
148
|
-
"annotations": { "readOnlyHint": false },
|
|
149
|
-
"_meta": {
|
|
150
|
-
"ui": { "visibility": ["model", "app"] },
|
|
151
|
-
},
|
|
152
|
-
},
|
|
153
|
-
// Tool input arguments (sent to CallTool).
|
|
157
|
+
"tool": "review-diff", // References src/tools/review-diff.ts
|
|
158
|
+
"userMessage": "Refactor the auth module to use JWT tokens.",
|
|
154
159
|
"toolInput": {
|
|
155
160
|
"changesetId": "cs_789",
|
|
156
|
-
"title": "Refactor Authentication Module"
|
|
161
|
+
"title": "Refactor Authentication Module"
|
|
157
162
|
},
|
|
158
|
-
// Tool result data (CallToolResult response).
|
|
159
163
|
"toolResult": {
|
|
160
164
|
"structuredContent": {
|
|
161
165
|
"title": "Refactor Authentication Module",
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
}
|
|
166
|
+
"sections": [...]
|
|
167
|
+
}
|
|
168
|
+
}
|
|
165
169
|
}
|
|
166
170
|
```
|
|
167
171
|
|
|
@@ -217,6 +221,6 @@ npx skills add Sunpeak-AI/sunpeak@create-sunpeak-app
|
|
|
217
221
|
|
|
218
222
|
## Resources
|
|
219
223
|
|
|
220
|
-
- [MCP Apps](https://
|
|
221
|
-
- [MCP Apps SDK
|
|
222
|
-
- [
|
|
224
|
+
- [MCP Apps Documentation](https://sunpeak.ai/docs/mcp-apps/introduction)
|
|
225
|
+
- [MCP Apps SDK](https://github.com/modelcontextprotocol/ext-apps)
|
|
226
|
+
- [ChatGPT Apps SDK Design Guidelines](https://developers.openai.com/apps-sdk/concepts/design-guidelines)
|
package/bin/commands/build.mjs
CHANGED
|
@@ -59,6 +59,11 @@ export async function build(projectRoot = process.cwd()) {
|
|
|
59
59
|
process.exit(1);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// Read project identity from package.json for appInfo
|
|
63
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
|
|
64
|
+
const appName = pkg.name || 'sunpeak-app';
|
|
65
|
+
const appVersion = pkg.version || '0.1.0';
|
|
66
|
+
|
|
62
67
|
// Check if we're in the sunpeak workspace (directory is named "template")
|
|
63
68
|
const isTemplate = path.basename(projectRoot) === 'template';
|
|
64
69
|
const parentSrc = path.resolve(projectRoot, '../src');
|
|
@@ -153,7 +158,8 @@ export async function build(projectRoot = process.cwd()) {
|
|
|
153
158
|
.filter(entry => entry.isDirectory())
|
|
154
159
|
.map(entry => {
|
|
155
160
|
const kebabName = entry.name;
|
|
156
|
-
|
|
161
|
+
|
|
162
|
+
const resourceFile = `${kebabName}.tsx`;
|
|
157
163
|
const resourcePath = path.join(resourcesDir, kebabName, resourceFile);
|
|
158
164
|
|
|
159
165
|
// Skip directories without a resource file
|
|
@@ -163,10 +169,11 @@ export async function build(projectRoot = process.cwd()) {
|
|
|
163
169
|
|
|
164
170
|
// Convert kebab-case to PascalCase: 'review' -> 'Review', 'my-widget' -> 'MyWidget'
|
|
165
171
|
const pascalName = toPascalCase(kebabName);
|
|
172
|
+
const componentFile = resourceFile.replace(/\.tsx$/, '');
|
|
166
173
|
|
|
167
174
|
return {
|
|
168
175
|
componentName: `${pascalName}Resource`,
|
|
169
|
-
componentFile
|
|
176
|
+
componentFile,
|
|
170
177
|
kebabName,
|
|
171
178
|
resourceDir: path.join(resourcesDir, kebabName),
|
|
172
179
|
entry: `.tmp/index-${kebabName}.tsx`,
|
|
@@ -180,7 +187,7 @@ export async function build(projectRoot = process.cwd()) {
|
|
|
180
187
|
|
|
181
188
|
if (resourceFiles.length === 0) {
|
|
182
189
|
console.error('Error: No resource directories found in src/resources/');
|
|
183
|
-
console.error('Each resource should be a directory like: src/resources/review/review
|
|
190
|
+
console.error('Each resource should be a directory like: src/resources/review/review.tsx');
|
|
184
191
|
process.exit(1);
|
|
185
192
|
}
|
|
186
193
|
|
|
@@ -218,7 +225,7 @@ export async function build(projectRoot = process.cwd()) {
|
|
|
218
225
|
// Create entry file from template in temp directory
|
|
219
226
|
const entryContent = template
|
|
220
227
|
.replace('// RESOURCE_IMPORT', `import { ${componentName}, resource } from '../src/resources/${kebabName}/${componentFile}';`)
|
|
221
|
-
.replace('// RESOURCE_MOUNT', `createRoot(root).render(<AppProvider appInfo={{ name:
|
|
228
|
+
.replace('// RESOURCE_MOUNT', `createRoot(root).render(<AppProvider appInfo={{ name: ${JSON.stringify(appName)}, version: ${JSON.stringify(appVersion)} }}><${componentName} /></AppProvider>);`);
|
|
222
229
|
|
|
223
230
|
const entryPath = path.join(projectRoot, entry);
|
|
224
231
|
writeFileSync(entryPath, entryContent);
|
|
@@ -283,6 +290,8 @@ export async function build(projectRoot = process.cwd()) {
|
|
|
283
290
|
const destJson = path.join(distOutDir, `${kebabName}.json`);
|
|
284
291
|
|
|
285
292
|
const meta = await extractResourceExport(srcTsx);
|
|
293
|
+
// Inject name from directory key if not explicitly set
|
|
294
|
+
meta.name = meta.name ?? kebabName;
|
|
286
295
|
// Generate URI using resource name and build timestamp
|
|
287
296
|
meta.uri = `ui://${meta.name}-${timestamp}`;
|
|
288
297
|
writeFileSync(destJson, JSON.stringify(meta, null, 2));
|
|
@@ -331,10 +340,114 @@ ${jsContents}
|
|
|
331
340
|
|
|
332
341
|
console.log('\n✓ All resources built successfully!');
|
|
333
342
|
console.log('\nBuilt resources:');
|
|
334
|
-
for (const { kebabName
|
|
335
|
-
const files = readdirSync(distOutDir);
|
|
343
|
+
for (const { kebabName } of resourceFiles) {
|
|
336
344
|
console.log(` ${kebabName}`);
|
|
337
345
|
}
|
|
346
|
+
|
|
347
|
+
// ========================================================================
|
|
348
|
+
// Compile server-side code (tools + server entry) for `sunpeak start`
|
|
349
|
+
// ========================================================================
|
|
350
|
+
|
|
351
|
+
const toolsDir = path.join(projectRoot, 'src/tools');
|
|
352
|
+
const serverEntryPath = path.join(projectRoot, 'src/server.ts');
|
|
353
|
+
|
|
354
|
+
// Find tool files
|
|
355
|
+
const toolFiles = existsSync(toolsDir)
|
|
356
|
+
? readdirSync(toolsDir).filter(f => f.endsWith('.ts'))
|
|
357
|
+
: [];
|
|
358
|
+
|
|
359
|
+
const hasServerEntry = existsSync(serverEntryPath);
|
|
360
|
+
|
|
361
|
+
if (toolFiles.length > 0 || hasServerEntry) {
|
|
362
|
+
console.log('\nCompiling server-side code...');
|
|
363
|
+
|
|
364
|
+
let esbuild;
|
|
365
|
+
try {
|
|
366
|
+
const esbuildPath = resolveEsmEntry(require, 'esbuild');
|
|
367
|
+
esbuild = await import(esbuildPath);
|
|
368
|
+
} catch {
|
|
369
|
+
console.warn('Warning: esbuild not found — skipping tool/server compilation.');
|
|
370
|
+
console.warn('Install esbuild to enable `sunpeak start`: npm install -D esbuild');
|
|
371
|
+
esbuild = null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (esbuild) {
|
|
375
|
+
const toolsOutDir = path.join(distDir, 'tools');
|
|
376
|
+
if (toolFiles.length > 0) {
|
|
377
|
+
mkdirSync(toolsOutDir, { recursive: true });
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Compile each tool file
|
|
381
|
+
for (const toolFile of toolFiles) {
|
|
382
|
+
const toolName = toolFile.replace(/\.ts$/, '');
|
|
383
|
+
const toolPath = path.join(toolsDir, toolFile);
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
await esbuild.build({
|
|
387
|
+
entryPoints: [toolPath],
|
|
388
|
+
bundle: true,
|
|
389
|
+
format: 'esm',
|
|
390
|
+
platform: 'node',
|
|
391
|
+
target: 'node18',
|
|
392
|
+
outfile: path.join(toolsOutDir, `${toolName}.js`),
|
|
393
|
+
// Externalize bare specifiers (node_modules) — resolve at runtime
|
|
394
|
+
plugins: [{
|
|
395
|
+
name: 'externalize-bare-specifiers',
|
|
396
|
+
setup(build) {
|
|
397
|
+
build.onResolve({ filter: /.*/ }, (args) => {
|
|
398
|
+
if (args.kind !== 'import-statement') return;
|
|
399
|
+
if (!args.path.startsWith('.') && !args.path.startsWith('/')) {
|
|
400
|
+
return { external: true };
|
|
401
|
+
}
|
|
402
|
+
return undefined;
|
|
403
|
+
});
|
|
404
|
+
},
|
|
405
|
+
}],
|
|
406
|
+
loader: { '.tsx': 'tsx', '.ts': 'ts' },
|
|
407
|
+
logLevel: 'warning',
|
|
408
|
+
});
|
|
409
|
+
console.log(`✓ Compiled tools/${toolName}.js`);
|
|
410
|
+
} catch (err) {
|
|
411
|
+
console.error(`Failed to compile tool ${toolName}:`, err.message);
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Compile server entry if present
|
|
417
|
+
if (hasServerEntry) {
|
|
418
|
+
try {
|
|
419
|
+
await esbuild.build({
|
|
420
|
+
entryPoints: [serverEntryPath],
|
|
421
|
+
bundle: true,
|
|
422
|
+
format: 'esm',
|
|
423
|
+
platform: 'node',
|
|
424
|
+
target: 'node18',
|
|
425
|
+
outfile: path.join(distDir, 'server.js'),
|
|
426
|
+
plugins: [{
|
|
427
|
+
name: 'externalize-bare-specifiers',
|
|
428
|
+
setup(build) {
|
|
429
|
+
build.onResolve({ filter: /.*/ }, (args) => {
|
|
430
|
+
if (args.kind !== 'import-statement') return;
|
|
431
|
+
if (!args.path.startsWith('.') && !args.path.startsWith('/')) {
|
|
432
|
+
return { external: true };
|
|
433
|
+
}
|
|
434
|
+
return undefined;
|
|
435
|
+
});
|
|
436
|
+
},
|
|
437
|
+
}],
|
|
438
|
+
loader: { '.tsx': 'tsx', '.ts': 'ts' },
|
|
439
|
+
logLevel: 'warning',
|
|
440
|
+
});
|
|
441
|
+
console.log(`✓ Compiled server.js`);
|
|
442
|
+
} catch (err) {
|
|
443
|
+
console.error(`Failed to compile server entry:`, err.message);
|
|
444
|
+
process.exit(1);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
console.log('\n✓ Build complete!');
|
|
338
451
|
}
|
|
339
452
|
|
|
340
453
|
// Allow running directly
|
package/bin/commands/dev.mjs
CHANGED
|
@@ -178,8 +178,8 @@ export async function dev(projectRoot = process.cwd(), args = []) {
|
|
|
178
178
|
sunpeakMcp = await import(pathToFileURL(join(sunpeakBase, 'dist/mcp/index.js')).href);
|
|
179
179
|
sunpeakDiscovery = await import(pathToFileURL(join(sunpeakBase, 'dist/lib/discovery-cli.js')).href);
|
|
180
180
|
}
|
|
181
|
-
const { FAVICON_BUFFER: faviconBuffer, runMCPServer } = sunpeakMcp;
|
|
182
|
-
const { findResourceDirs,
|
|
181
|
+
const { FAVICON_BUFFER: faviconBuffer, runMCPServer, startProductionHttpServer } = sunpeakMcp;
|
|
182
|
+
const { findResourceDirs, findSimulationFilesFlat, findToolFiles, extractResourceExport, extractToolExport } = sunpeakDiscovery;
|
|
183
183
|
|
|
184
184
|
// Vite plugin to serve the sunpeak favicon
|
|
185
185
|
const sunpeakFaviconPlugin = () => ({
|
|
@@ -229,28 +229,73 @@ export async function dev(projectRoot = process.cwd(), args = []) {
|
|
|
229
229
|
// Discover simulations using sunpeak's discovery utilities
|
|
230
230
|
const resourcesDir = join(projectRoot, 'src/resources');
|
|
231
231
|
const simulationsDir = join(projectRoot, 'tests/simulations');
|
|
232
|
-
const
|
|
232
|
+
const toolsDir = join(projectRoot, 'src/tools');
|
|
233
233
|
|
|
234
|
-
const
|
|
235
|
-
|
|
234
|
+
const resourceDirs = findResourceDirs(resourcesDir, (key) => `${key}.tsx`, fs);
|
|
235
|
+
|
|
236
|
+
// Build resource metadata map
|
|
237
|
+
const resourceMap = new Map();
|
|
238
|
+
for (const { key, resourcePath } of resourceDirs) {
|
|
236
239
|
const resource = await extractResourceExport(resourcePath);
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
});
|
|
240
|
+
// Inject name from directory key if not explicitly set
|
|
241
|
+
resource.name = resource.name ?? key;
|
|
242
|
+
resourceMap.set(key, resource);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Discover tool files and extract metadata
|
|
246
|
+
const toolFiles = findToolFiles(toolsDir, fs);
|
|
247
|
+
const toolMap = new Map();
|
|
248
|
+
for (const { name: toolName, path: toolPath } of toolFiles) {
|
|
249
|
+
try {
|
|
250
|
+
const { tool } = await extractToolExport(toolPath);
|
|
251
|
+
toolMap.set(toolName, tool);
|
|
252
|
+
} catch (err) {
|
|
253
|
+
console.warn(`Warning: Could not extract metadata from tool ${toolName}: ${err.message}`);
|
|
251
254
|
}
|
|
252
255
|
}
|
|
253
256
|
|
|
257
|
+
// Discover simulations from flat directory
|
|
258
|
+
const simulations = [];
|
|
259
|
+
const simFiles = findSimulationFilesFlat(simulationsDir, fs);
|
|
260
|
+
|
|
261
|
+
for (const { name: simName, path: simPath } of simFiles) {
|
|
262
|
+
const simulation = JSON.parse(readFileSync(simPath, 'utf-8'));
|
|
263
|
+
const toolName = typeof simulation.tool === 'string' ? simulation.tool : simName;
|
|
264
|
+
|
|
265
|
+
// Look up tool metadata
|
|
266
|
+
const tool = toolMap.get(toolName);
|
|
267
|
+
if (!tool) {
|
|
268
|
+
console.warn(`Warning: Tool "${toolName}" not found for simulation "${simName}". Skipping.`);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// tool.resource is the resource name string — find matching resource key
|
|
273
|
+
const resourceName = tool.resource;
|
|
274
|
+
const resourceKey = resourceName
|
|
275
|
+
? Array.from(resourceMap.keys()).find((k) => resourceMap.get(k).name === resourceName)
|
|
276
|
+
: undefined;
|
|
277
|
+
|
|
278
|
+
if (!resourceKey) {
|
|
279
|
+
console.warn(`Warning: No resource found for tool "${toolName}" in simulation "${simName}". Skipping.`);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Determine source path for the resource
|
|
284
|
+
const resourceDir = resourceDirs.find((d) => d.key === resourceKey);
|
|
285
|
+
const srcPath = resourceDir
|
|
286
|
+
? `/src/resources/${resourceKey}/${basename(resourceDir.resourcePath)}`
|
|
287
|
+
: undefined;
|
|
288
|
+
|
|
289
|
+
simulations.push({
|
|
290
|
+
...simulation,
|
|
291
|
+
...(typeof simulation.tool === 'string' ? { tool: { name: toolName, ...tool } } : {}),
|
|
292
|
+
name: simName,
|
|
293
|
+
distPath: join(projectRoot, `dist/${resourceKey}/${resourceKey}.html`),
|
|
294
|
+
srcPath,
|
|
295
|
+
resource: resourceMap.get(resourceKey),
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
254
299
|
// Start MCP server with its own Vite instance (unless --prod-mcp is set)
|
|
255
300
|
if (simulations.length > 0) {
|
|
256
301
|
const mcpMode = prodMcp ? 'production build' : 'Vite HMR';
|
|
@@ -294,7 +339,7 @@ const Component = ResourceModule.default || ResourceModule['${componentName}'];
|
|
|
294
339
|
if (!Component) {
|
|
295
340
|
document.getElementById('root').innerHTML = '<pre style="color:red;padding:16px">Component not found: ${componentName}\\nExports: ' + Object.keys(ResourceModule).join(', ') + '</pre>';
|
|
296
341
|
} else {
|
|
297
|
-
const appInfo = { name:
|
|
342
|
+
const appInfo = { name: ${JSON.stringify(pkg.name || 'sunpeak-app')}, version: ${JSON.stringify(pkg.version || '0.1.0')} };
|
|
298
343
|
root.render(
|
|
299
344
|
createElement(AppProvider, { appInfo }, createElement(Component))
|
|
300
345
|
);
|
|
@@ -344,13 +389,109 @@ if (import.meta.hot) {
|
|
|
344
389
|
}
|
|
345
390
|
|
|
346
391
|
const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
392
|
+
|
|
393
|
+
let mcpHandle = null;
|
|
394
|
+
|
|
395
|
+
if (prodMcp) {
|
|
396
|
+
// --prod-mcp: Use real tool handlers via Vite SSR
|
|
397
|
+
// Load full tool modules (handler + schema + config) from TypeScript source
|
|
398
|
+
const ssrLoader = await createServer({
|
|
399
|
+
root: projectRoot,
|
|
400
|
+
server: { middlewareMode: true },
|
|
401
|
+
appType: 'custom',
|
|
402
|
+
logLevel: 'silent',
|
|
403
|
+
resolve: {
|
|
404
|
+
alias: {
|
|
405
|
+
...(isTemplate && { sunpeak: parentSrc }),
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const prodTools = [];
|
|
411
|
+
for (const { name: toolName, path: toolPath } of toolFiles) {
|
|
412
|
+
try {
|
|
413
|
+
const relativePath = './' + toolPath.replace(projectRoot + '/', '').replace(projectRoot + '\\', '');
|
|
414
|
+
const mod = await ssrLoader.ssrLoadModule(relativePath);
|
|
415
|
+
if (mod.tool && mod.default) {
|
|
416
|
+
prodTools.push({
|
|
417
|
+
name: toolName,
|
|
418
|
+
tool: mod.tool,
|
|
419
|
+
schema: mod.schema,
|
|
420
|
+
handler: mod.default,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
} catch (err) {
|
|
424
|
+
console.warn(`Warning: Could not load tool handler ${toolName}: ${err.message}`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Load server entry if present
|
|
429
|
+
const serverEntryPath = join(projectRoot, 'src/server.ts');
|
|
430
|
+
let auth = undefined;
|
|
431
|
+
let serverConfig = {};
|
|
432
|
+
if (existsSync(serverEntryPath)) {
|
|
433
|
+
try {
|
|
434
|
+
const serverMod = await ssrLoader.ssrLoadModule('./src/server.ts');
|
|
435
|
+
if (typeof serverMod.auth === 'function') {
|
|
436
|
+
auth = serverMod.auth;
|
|
437
|
+
console.log('Loaded auth from src/server.ts');
|
|
438
|
+
}
|
|
439
|
+
if (serverMod.server) serverConfig = serverMod.server;
|
|
440
|
+
} catch (err) {
|
|
441
|
+
console.warn(`Warning: Could not load server entry: ${err.message}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
await ssrLoader.close();
|
|
446
|
+
|
|
447
|
+
// Build production resources from dist/
|
|
448
|
+
const prodResources = [];
|
|
449
|
+
for (const { key } of resourceDirs) {
|
|
450
|
+
const meta = resourceMap.get(key);
|
|
451
|
+
if (!meta) continue;
|
|
452
|
+
const htmlPath = join(projectRoot, `dist/${key}/${key}.html`);
|
|
453
|
+
const jsonPath = join(projectRoot, `dist/${key}/${key}.json`);
|
|
454
|
+
if (!existsSync(htmlPath)) continue;
|
|
455
|
+
|
|
456
|
+
const html = readFileSync(htmlPath, 'utf-8');
|
|
457
|
+
let uri = `ui://${meta.name ?? key}`;
|
|
458
|
+
let _meta = meta._meta;
|
|
459
|
+
|
|
460
|
+
// Use URI from build JSON if available (has cache-bust timestamp)
|
|
461
|
+
if (existsSync(jsonPath)) {
|
|
462
|
+
try {
|
|
463
|
+
const buildMeta = JSON.parse(readFileSync(jsonPath, 'utf-8'));
|
|
464
|
+
if (buildMeta.uri) uri = buildMeta.uri;
|
|
465
|
+
} catch {}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
prodResources.push({
|
|
469
|
+
name: meta.name ?? key,
|
|
470
|
+
uri,
|
|
471
|
+
html,
|
|
472
|
+
description: meta.description,
|
|
473
|
+
_meta,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const name = serverConfig.name ?? pkg.name ?? 'Sunpeak';
|
|
478
|
+
const version = serverConfig.version ?? pkg.version ?? '0.1.0';
|
|
479
|
+
|
|
480
|
+
console.log(`Starting production MCP server with ${prodTools.length} tool(s)...`);
|
|
481
|
+
startProductionHttpServer(
|
|
482
|
+
{ name, version, tools: prodTools, resources: prodResources, auth },
|
|
483
|
+
8000
|
|
484
|
+
);
|
|
485
|
+
} else {
|
|
486
|
+
// Default: simulation-based MCP server with fixture data
|
|
487
|
+
mcpHandle = runMCPServer({
|
|
488
|
+
name: pkg.name || 'Sunpeak',
|
|
489
|
+
version: pkg.version || '0.1.0',
|
|
490
|
+
simulations,
|
|
491
|
+
port: 8000,
|
|
492
|
+
...(mcpViteServer && { viteServer: mcpViteServer }),
|
|
493
|
+
});
|
|
494
|
+
}
|
|
354
495
|
|
|
355
496
|
// Build production bundles and watch for changes.
|
|
356
497
|
// Tunnel clients (e.g. Claude via ngrok) get the pre-built HTML since they can't
|
package/bin/commands/new.mjs
CHANGED
|
@@ -153,9 +153,19 @@ export async function init(projectName, resourcesArg, deps = defaultDeps) {
|
|
|
153
153
|
if (src.includes('/resources/') && name === resource) {
|
|
154
154
|
return false;
|
|
155
155
|
}
|
|
156
|
-
// Skip simulation
|
|
157
|
-
if (src.includes('/tests/simulations/') && name
|
|
158
|
-
|
|
156
|
+
// Skip flat simulation files for excluded resources: tests/simulations/*.json
|
|
157
|
+
if (src.includes('/tests/simulations/') && name.endsWith('.json')) {
|
|
158
|
+
const baseName = name.replace(/\.json$/, '');
|
|
159
|
+
if (baseName === resource || baseName.startsWith(resource + '-') || baseName.endsWith('-' + resource)) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Skip tool files for excluded resources: src/tools/*.ts
|
|
164
|
+
if (src.includes('/src/tools/') && name.endsWith('.ts')) {
|
|
165
|
+
const baseName = name.replace(/\.ts$/, '');
|
|
166
|
+
if (baseName === resource || baseName.startsWith(resource + '-') || baseName.endsWith('-' + resource)) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
159
169
|
}
|
|
160
170
|
// Skip e2e test files for excluded resources
|
|
161
171
|
if (src.includes('/tests/e2e/') && name === `${resource}.spec.ts`) {
|