sunpeak 0.16.4 → 0.16.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/README.md +0 -2
- package/bin/commands/build.mjs +19 -16
- package/bin/commands/dev.mjs +7 -6
- package/bin/commands/new.mjs +69 -40
- package/dist/mcp/index.cjs +52 -30
- package/dist/mcp/index.cjs.map +1 -1
- package/dist/mcp/index.js +52 -30
- package/dist/mcp/index.js.map +1 -1
- package/package.json +3 -2
- package/template/src/resources/review/review.tsx +4 -4
package/README.md
CHANGED
|
@@ -43,8 +43,6 @@ pnpm add -g sunpeak
|
|
|
43
43
|
sunpeak new
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
To add `sunpeak` to an existing project, refer to the [documentation](https://sunpeak.ai/docs/add-to-existing-project).
|
|
47
|
-
|
|
48
46
|
## Overview
|
|
49
47
|
|
|
50
48
|
`sunpeak` is an npm package that helps you build MCP Apps (interactive UI resources) while keeping your MCP server client-agnostic. Built on the [MCP Apps SDK](https://github.com/modelcontextprotocol/ext-apps) (`@modelcontextprotocol/ext-apps`). `sunpeak` consists of:
|
package/bin/commands/build.mjs
CHANGED
|
@@ -49,7 +49,8 @@ function resolveEsmEntry(require, packageName) {
|
|
|
49
49
|
* Build all resources for a Sunpeak project
|
|
50
50
|
* Runs in the context of a user's project directory
|
|
51
51
|
*/
|
|
52
|
-
export async function build(projectRoot = process.cwd()) {
|
|
52
|
+
export async function build(projectRoot = process.cwd(), { quiet = false } = {}) {
|
|
53
|
+
const log = quiet ? () => {} : console.log.bind(console);
|
|
53
54
|
|
|
54
55
|
// Check for package.json first
|
|
55
56
|
const pkgJsonPath = path.join(projectRoot, 'package.json');
|
|
@@ -191,7 +192,7 @@ export async function build(projectRoot = process.cwd()) {
|
|
|
191
192
|
process.exit(1);
|
|
192
193
|
}
|
|
193
194
|
|
|
194
|
-
|
|
195
|
+
log('Building all resources...\n');
|
|
195
196
|
|
|
196
197
|
// Read and validate the template
|
|
197
198
|
const template = readFileSync(templateFile, 'utf-8');
|
|
@@ -214,7 +215,7 @@ export async function build(projectRoot = process.cwd()) {
|
|
|
214
215
|
// Build all resources (but don't copy yet)
|
|
215
216
|
for (let i = 0; i < resourceFiles.length; i++) {
|
|
216
217
|
const { componentName, componentFile, kebabName, entry, jsOutput, buildOutDir } = resourceFiles[i];
|
|
217
|
-
|
|
218
|
+
log(`[${i + 1}/${resourceFiles.length}] Building ${kebabName}...`);
|
|
218
219
|
|
|
219
220
|
try {
|
|
220
221
|
// Create build directory if it doesn't exist
|
|
@@ -234,6 +235,7 @@ export async function build(projectRoot = process.cwd()) {
|
|
|
234
235
|
await viteBuild({
|
|
235
236
|
mode: 'production',
|
|
236
237
|
root: projectRoot,
|
|
238
|
+
...(quiet && { logLevel: 'silent' }),
|
|
237
239
|
plugins: [react(), tailwindcss(), inlineCssPlugin(buildOutDir)],
|
|
238
240
|
define: {
|
|
239
241
|
'process.env.NODE_ENV': JSON.stringify('production'),
|
|
@@ -277,7 +279,7 @@ export async function build(projectRoot = process.cwd()) {
|
|
|
277
279
|
}
|
|
278
280
|
|
|
279
281
|
// Now copy all files from build-output to dist/{resource}/
|
|
280
|
-
|
|
282
|
+
log('\nCopying built files to dist/...');
|
|
281
283
|
const timestamp = Date.now().toString(36);
|
|
282
284
|
|
|
283
285
|
for (const { jsOutput, htmlOutput, buildOutDir, distOutDir, kebabName, componentFile, resourceDir } of resourceFiles) {
|
|
@@ -296,7 +298,7 @@ export async function build(projectRoot = process.cwd()) {
|
|
|
296
298
|
// Generate URI using resource name and build timestamp
|
|
297
299
|
meta.uri = `ui://${meta.name}-${timestamp}`;
|
|
298
300
|
writeFileSync(destJson, JSON.stringify(meta, null, 2));
|
|
299
|
-
|
|
301
|
+
log(`✓ Generated ${kebabName}/${kebabName}.json (uri: ${meta.uri})`);
|
|
300
302
|
|
|
301
303
|
// Read built JS file and wrap in HTML shell
|
|
302
304
|
const builtJsFile = path.join(buildOutDir, jsOutput);
|
|
@@ -318,13 +320,13 @@ ${jsContents}
|
|
|
318
320
|
</body>
|
|
319
321
|
</html>`;
|
|
320
322
|
writeFileSync(destHtmlFile, html);
|
|
321
|
-
|
|
323
|
+
log(`✓ Built ${kebabName}/${htmlOutput}`);
|
|
322
324
|
} else {
|
|
323
325
|
console.error(`Built file not found: ${builtJsFile}`);
|
|
324
326
|
if (existsSync(buildOutDir)) {
|
|
325
|
-
|
|
327
|
+
log(` Files in ${buildOutDir}:`, readdirSync(buildOutDir));
|
|
326
328
|
} else {
|
|
327
|
-
|
|
329
|
+
log(` Build directory doesn't exist: ${buildOutDir}`);
|
|
328
330
|
}
|
|
329
331
|
process.exit(1);
|
|
330
332
|
}
|
|
@@ -339,10 +341,10 @@ ${jsContents}
|
|
|
339
341
|
rmSync(buildDir, { recursive: true });
|
|
340
342
|
}
|
|
341
343
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
+
log('\n✓ All resources built successfully!');
|
|
345
|
+
log('\nBuilt resources:');
|
|
344
346
|
for (const { kebabName } of resourceFiles) {
|
|
345
|
-
|
|
347
|
+
log(` ${kebabName}`);
|
|
346
348
|
}
|
|
347
349
|
|
|
348
350
|
// ========================================================================
|
|
@@ -360,7 +362,7 @@ ${jsContents}
|
|
|
360
362
|
const hasServerEntry = existsSync(serverEntryPath);
|
|
361
363
|
|
|
362
364
|
if (toolFiles.length > 0 || hasServerEntry) {
|
|
363
|
-
|
|
365
|
+
log('\nCompiling server-side code...');
|
|
364
366
|
|
|
365
367
|
let esbuild;
|
|
366
368
|
try {
|
|
@@ -407,7 +409,7 @@ ${jsContents}
|
|
|
407
409
|
loader: { '.tsx': 'tsx', '.ts': 'ts' },
|
|
408
410
|
logLevel: 'warning',
|
|
409
411
|
});
|
|
410
|
-
|
|
412
|
+
log(`✓ Compiled tools/${toolName}.js`);
|
|
411
413
|
} catch (err) {
|
|
412
414
|
console.error(`Failed to compile tool ${toolName}:`, err.message);
|
|
413
415
|
process.exit(1);
|
|
@@ -439,7 +441,7 @@ ${jsContents}
|
|
|
439
441
|
loader: { '.tsx': 'tsx', '.ts': 'ts' },
|
|
440
442
|
logLevel: 'warning',
|
|
441
443
|
});
|
|
442
|
-
|
|
444
|
+
log(`✓ Compiled server.js`);
|
|
443
445
|
} catch (err) {
|
|
444
446
|
console.error(`Failed to compile server entry:`, err.message);
|
|
445
447
|
process.exit(1);
|
|
@@ -448,12 +450,13 @@ ${jsContents}
|
|
|
448
450
|
}
|
|
449
451
|
}
|
|
450
452
|
|
|
451
|
-
|
|
453
|
+
log('\n✓ Build complete!');
|
|
452
454
|
}
|
|
453
455
|
|
|
454
456
|
// Allow running directly
|
|
455
457
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
456
|
-
|
|
458
|
+
const quiet = process.argv.includes('--quiet');
|
|
459
|
+
build(process.cwd(), { quiet }).catch(error => {
|
|
457
460
|
console.error(error);
|
|
458
461
|
process.exit(1);
|
|
459
462
|
});
|
package/bin/commands/dev.mjs
CHANGED
|
@@ -66,17 +66,17 @@ function startBuildWatcher(projectRoot, resourcesDir, mcpHandle) {
|
|
|
66
66
|
let activeChild = null;
|
|
67
67
|
const sunpeakBin = join(dirname(new URL(import.meta.url).pathname), '..', 'sunpeak.js');
|
|
68
68
|
|
|
69
|
-
const runBuild = (
|
|
69
|
+
const runBuild = () => {
|
|
70
70
|
// Kill any in-progress build and start fresh
|
|
71
71
|
if (activeChild) {
|
|
72
72
|
activeChild.kill('SIGTERM');
|
|
73
73
|
activeChild = null;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
console.log(`[build]
|
|
77
|
-
const child = spawn(process.execPath, [sunpeakBin, 'build'], {
|
|
76
|
+
console.log(`[build] Building resources for the MCP server for non-ChatGPT hosts...`);
|
|
77
|
+
const child = spawn(process.execPath, [sunpeakBin, 'build', '--quiet'], {
|
|
78
78
|
cwd: projectRoot,
|
|
79
|
-
stdio: ['ignore', '
|
|
79
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
80
80
|
env: { ...process.env, NODE_ENV: 'production' },
|
|
81
81
|
});
|
|
82
82
|
activeChild = child;
|
|
@@ -85,6 +85,7 @@ function startBuildWatcher(projectRoot, resourcesDir, mcpHandle) {
|
|
|
85
85
|
if (child !== activeChild) return; // Superseded by a newer build
|
|
86
86
|
activeChild = null;
|
|
87
87
|
if (code === 0) {
|
|
88
|
+
console.log(`[build] Built resources for the MCP server for non-ChatGPT hosts.`);
|
|
88
89
|
// Notify non-local sessions (Claude, etc.) that resources changed
|
|
89
90
|
mcpHandle?.invalidateResources();
|
|
90
91
|
} else if (code !== null) {
|
|
@@ -94,7 +95,7 @@ function startBuildWatcher(projectRoot, resourcesDir, mcpHandle) {
|
|
|
94
95
|
};
|
|
95
96
|
|
|
96
97
|
// Initial build
|
|
97
|
-
runBuild(
|
|
98
|
+
runBuild();
|
|
98
99
|
|
|
99
100
|
// Watch src/resources/ for changes using fs.watch (recursive supported on macOS/Windows)
|
|
100
101
|
let debounceTimer = null;
|
|
@@ -108,7 +109,7 @@ function startBuildWatcher(projectRoot, resourcesDir, mcpHandle) {
|
|
|
108
109
|
|
|
109
110
|
clearTimeout(debounceTimer);
|
|
110
111
|
debounceTimer = setTimeout(() => {
|
|
111
|
-
runBuild(
|
|
112
|
+
runBuild();
|
|
112
113
|
}, 500);
|
|
113
114
|
});
|
|
114
115
|
console.log('[build] Watching src/resources/ for changes...');
|
package/bin/commands/new.mjs
CHANGED
|
@@ -2,26 +2,59 @@
|
|
|
2
2
|
import { existsSync, mkdirSync, cpSync, readFileSync, writeFileSync, renameSync } from 'fs';
|
|
3
3
|
import { join, dirname, basename } from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { execSync, exec } from 'child_process';
|
|
6
|
+
import { promisify } from 'util';
|
|
7
|
+
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
import * as clack from '@clack/prompts';
|
|
7
10
|
import { discoverResources } from '../lib/patterns.mjs';
|
|
8
11
|
import { detectPackageManager } from '../utils.mjs';
|
|
9
12
|
|
|
10
13
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
14
|
|
|
12
15
|
/**
|
|
13
|
-
* Default prompt
|
|
14
|
-
* @param {string} question
|
|
16
|
+
* Default prompt for project name using clack text input.
|
|
15
17
|
* @returns {Promise<string>}
|
|
16
18
|
*/
|
|
17
|
-
function
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
async function defaultPromptName() {
|
|
20
|
+
const value = await clack.text({
|
|
21
|
+
message: 'Project name',
|
|
22
|
+
placeholder: 'my-app',
|
|
23
|
+
defaultValue: 'my-app',
|
|
24
|
+
validate: (v) => {
|
|
25
|
+
if (v === 'template') return '"template" is a reserved name';
|
|
26
|
+
},
|
|
24
27
|
});
|
|
28
|
+
if (clack.isCancel(value)) {
|
|
29
|
+
clack.cancel('Cancelled.');
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Default resource selection using clack multiselect.
|
|
37
|
+
* @param {string[]} availableResources
|
|
38
|
+
* @returns {Promise<string[]>}
|
|
39
|
+
*/
|
|
40
|
+
async function defaultSelectResources(availableResources) {
|
|
41
|
+
const selected = await clack.multiselect({
|
|
42
|
+
message: 'Resources (UIs) to include (space to toggle)',
|
|
43
|
+
options: (() => {
|
|
44
|
+
const maxLen = Math.max(...availableResources.map((r) => r.length));
|
|
45
|
+
return availableResources.map((r) => ({
|
|
46
|
+
value: r,
|
|
47
|
+
label: `${r.padEnd(maxLen)} (https://sunpeak.ai/docs/api-reference/resources/${r})`,
|
|
48
|
+
}));
|
|
49
|
+
})(),
|
|
50
|
+
initialValues: availableResources,
|
|
51
|
+
required: true,
|
|
52
|
+
});
|
|
53
|
+
if (clack.isCancel(selected)) {
|
|
54
|
+
clack.cancel('Cancelled.');
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
return selected;
|
|
25
58
|
}
|
|
26
59
|
|
|
27
60
|
/**
|
|
@@ -37,7 +70,12 @@ export const defaultDeps = {
|
|
|
37
70
|
writeFileSync,
|
|
38
71
|
renameSync,
|
|
39
72
|
execSync,
|
|
40
|
-
|
|
73
|
+
execAsync,
|
|
74
|
+
promptName: defaultPromptName,
|
|
75
|
+
selectResources: defaultSelectResources,
|
|
76
|
+
intro: clack.intro,
|
|
77
|
+
outro: clack.outro,
|
|
78
|
+
spinner: clack.spinner,
|
|
41
79
|
console,
|
|
42
80
|
process,
|
|
43
81
|
cwd: () => process.cwd(),
|
|
@@ -88,6 +126,8 @@ export function parseResourcesInput(input, validResources, deps = defaultDeps) {
|
|
|
88
126
|
export async function init(projectName, resourcesArg, deps = defaultDeps) {
|
|
89
127
|
const d = { ...defaultDeps, ...deps };
|
|
90
128
|
|
|
129
|
+
d.intro('☀️ sunpeak');
|
|
130
|
+
|
|
91
131
|
// Discover available resources from template
|
|
92
132
|
const availableResources = d.discoverResources();
|
|
93
133
|
if (availableResources.length === 0) {
|
|
@@ -96,10 +136,7 @@ export async function init(projectName, resourcesArg, deps = defaultDeps) {
|
|
|
96
136
|
}
|
|
97
137
|
|
|
98
138
|
if (!projectName) {
|
|
99
|
-
projectName = await d.
|
|
100
|
-
if (!projectName) {
|
|
101
|
-
projectName = 'my-app';
|
|
102
|
-
}
|
|
139
|
+
projectName = await d.promptName();
|
|
103
140
|
}
|
|
104
141
|
|
|
105
142
|
if (projectName === 'template') {
|
|
@@ -107,17 +144,13 @@ export async function init(projectName, resourcesArg, deps = defaultDeps) {
|
|
|
107
144
|
d.process.exit(1);
|
|
108
145
|
}
|
|
109
146
|
|
|
110
|
-
// Use resources from args or
|
|
111
|
-
let
|
|
112
|
-
if (resourcesArg) {
|
|
113
|
-
|
|
114
|
-
d.console.log(`☀️ 🏔️ Resources: ${resourcesArg}`);
|
|
147
|
+
// Use resources from args or interactively select them
|
|
148
|
+
let selectedResources;
|
|
149
|
+
if (resourcesArg !== undefined) {
|
|
150
|
+
selectedResources = parseResourcesInput(resourcesArg, availableResources, d);
|
|
115
151
|
} else {
|
|
116
|
-
|
|
117
|
-
`☀️ 🏔️ Resources (UIs) to include [${availableResources.join(', ')}]: `
|
|
118
|
-
);
|
|
152
|
+
selectedResources = await d.selectResources(availableResources);
|
|
119
153
|
}
|
|
120
|
-
const selectedResources = parseResourcesInput(resourcesInput, availableResources, d);
|
|
121
154
|
|
|
122
155
|
const targetDir = join(d.cwd(), projectName);
|
|
123
156
|
|
|
@@ -126,13 +159,11 @@ export async function init(projectName, resourcesArg, deps = defaultDeps) {
|
|
|
126
159
|
d.process.exit(1);
|
|
127
160
|
}
|
|
128
161
|
|
|
129
|
-
d.console.log(`☀️ 🏔️ Creating ${projectName}...`);
|
|
130
|
-
|
|
131
|
-
d.mkdirSync(targetDir, { recursive: true });
|
|
132
|
-
|
|
133
162
|
// Filter resource directories based on selection
|
|
134
163
|
const excludedResources = availableResources.filter((r) => !selectedResources.includes(r));
|
|
135
164
|
|
|
165
|
+
d.mkdirSync(targetDir, { recursive: true });
|
|
166
|
+
|
|
136
167
|
d.cpSync(d.templateDir, targetDir, {
|
|
137
168
|
recursive: true,
|
|
138
169
|
filter: (src) => {
|
|
@@ -223,32 +254,30 @@ export async function init(projectName, resourcesArg, deps = defaultDeps) {
|
|
|
223
254
|
|
|
224
255
|
d.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
225
256
|
|
|
226
|
-
//
|
|
257
|
+
// Install dependencies with spinner
|
|
227
258
|
const pm = d.detectPackageManager();
|
|
228
|
-
d.
|
|
259
|
+
const s = d.spinner();
|
|
260
|
+
s.start(`Installing dependencies with ${pm}...`);
|
|
229
261
|
|
|
230
262
|
try {
|
|
231
|
-
d.
|
|
263
|
+
await d.execAsync(`${pm} install`, { cwd: targetDir });
|
|
264
|
+
s.stop(`Installed dependencies with ${pm}`);
|
|
232
265
|
} catch {
|
|
233
|
-
|
|
266
|
+
s.stop(`Install failed. You can try running "${pm} install" manually.`);
|
|
234
267
|
}
|
|
235
268
|
|
|
236
269
|
const runCmd = pm === 'npm' ? 'npm run' : pm;
|
|
237
270
|
|
|
238
|
-
d.
|
|
239
|
-
Done! To get started:
|
|
271
|
+
d.outro(`Done! To get started:
|
|
240
272
|
|
|
241
273
|
cd ${projectName}
|
|
242
274
|
sunpeak dev
|
|
243
275
|
|
|
244
|
-
|
|
276
|
+
Your project commands:
|
|
245
277
|
|
|
246
278
|
sunpeak dev # Start dev server + MCP endpoint
|
|
247
279
|
sunpeak build # Build for production
|
|
248
|
-
${runCmd} test # Run tests
|
|
249
|
-
|
|
250
|
-
See README.md for more details.
|
|
251
|
-
`);
|
|
280
|
+
${runCmd} test # Run tests`);
|
|
252
281
|
}
|
|
253
282
|
|
|
254
283
|
// Allow running directly
|
package/dist/mcp/index.cjs
CHANGED
|
@@ -15520,27 +15520,30 @@ function createAppServer(config, simulations, viteMode) {
|
|
|
15520
15520
|
},
|
|
15521
15521
|
{ capabilities: { resources: {}, tools: {} } }
|
|
15522
15522
|
);
|
|
15523
|
-
const
|
|
15523
|
+
const registeredUriSet = /* @__PURE__ */ new Set();
|
|
15524
|
+
const resourceHandles = /* @__PURE__ */ new Map();
|
|
15525
|
+
const toolHandles = [];
|
|
15524
15526
|
for (const simulation of simulations) {
|
|
15525
15527
|
const resource = simulation.resource;
|
|
15526
15528
|
const tool = simulation.tool;
|
|
15527
15529
|
const toolResult = simulation.toolResult ?? { structuredContent: null };
|
|
15528
15530
|
const uri2 = resource.uri ?? `ui://${resource.name}`;
|
|
15531
|
+
const resourceName = resource.name;
|
|
15529
15532
|
const resourceMeta = resource._meta ?? {};
|
|
15530
15533
|
const toolMeta = tool._meta ?? {};
|
|
15531
|
-
if (!
|
|
15532
|
-
|
|
15534
|
+
if (!registeredUriSet.has(uri2)) {
|
|
15535
|
+
registeredUriSet.add(uri2);
|
|
15533
15536
|
const listMeta = viteMode ? injectViteCSP(resourceMeta) : resourceMeta;
|
|
15534
15537
|
console.log(`[MCP] RegisterResource: ${uri2}`);
|
|
15535
|
-
|
|
15536
|
-
|
|
15537
|
-
resource.name,
|
|
15538
|
+
const handle = mcpServer.registerResource(
|
|
15539
|
+
resourceName,
|
|
15538
15540
|
uri2,
|
|
15539
15541
|
{
|
|
15542
|
+
mimeType: EI,
|
|
15540
15543
|
description: resource.description,
|
|
15541
15544
|
_meta: listMeta
|
|
15542
15545
|
},
|
|
15543
|
-
async (
|
|
15546
|
+
async (readUri, extra) => {
|
|
15544
15547
|
const prodBuild = needsProdBuild(
|
|
15545
15548
|
extra?.requestInfo?.headers ?? {}
|
|
15546
15549
|
);
|
|
@@ -15549,17 +15552,18 @@ function createAppServer(config, simulations, viteMode) {
|
|
|
15549
15552
|
try {
|
|
15550
15553
|
content = getResourceHtml(simulation, viteMode, prodBuild);
|
|
15551
15554
|
} catch (error) {
|
|
15552
|
-
console.error(`[MCP] ReadResource error for ${
|
|
15555
|
+
console.error(`[MCP] ReadResource error for ${readUri.href}:`, error);
|
|
15553
15556
|
throw error;
|
|
15554
15557
|
}
|
|
15555
15558
|
const sizeKB = (content.length / 1024).toFixed(1);
|
|
15556
15559
|
console.log(
|
|
15557
|
-
`[MCP] ReadResource: ${
|
|
15560
|
+
`[MCP] ReadResource: ${readUri.href} → ${sizeKB}KB${prodBuild ? " (prod build)" : " (vite)"}`
|
|
15558
15561
|
);
|
|
15559
15562
|
return {
|
|
15560
15563
|
contents: [
|
|
15561
15564
|
{
|
|
15562
|
-
|
|
15565
|
+
// Use readUri (not closure variable) so the response URI matches after updates
|
|
15566
|
+
uri: readUri.href,
|
|
15563
15567
|
mimeType: EI,
|
|
15564
15568
|
text: content,
|
|
15565
15569
|
_meta: readMeta
|
|
@@ -15568,20 +15572,22 @@ function createAppServer(config, simulations, viteMode) {
|
|
|
15568
15572
|
};
|
|
15569
15573
|
}
|
|
15570
15574
|
);
|
|
15575
|
+
resourceHandles.set(resourceName, handle);
|
|
15571
15576
|
}
|
|
15572
|
-
|
|
15577
|
+
const fullToolMeta = {
|
|
15578
|
+
...toolMeta,
|
|
15579
|
+
ui: {
|
|
15580
|
+
resourceUri: uri2,
|
|
15581
|
+
// Preserve tool visibility from simulation metadata if declared
|
|
15582
|
+
...toolMeta.ui?.visibility ? { visibility: toolMeta.ui.visibility } : {}
|
|
15583
|
+
}
|
|
15584
|
+
};
|
|
15585
|
+
const toolHandle = hk(
|
|
15573
15586
|
mcpServer,
|
|
15574
15587
|
tool.name,
|
|
15575
15588
|
{
|
|
15576
15589
|
description: tool.description,
|
|
15577
|
-
_meta:
|
|
15578
|
-
...toolMeta,
|
|
15579
|
-
ui: {
|
|
15580
|
-
resourceUri: uri2,
|
|
15581
|
-
// Preserve tool visibility from simulation metadata if declared
|
|
15582
|
-
...toolMeta.ui?.visibility ? { visibility: toolMeta.ui.visibility } : {}
|
|
15583
|
-
}
|
|
15584
|
-
}
|
|
15590
|
+
_meta: fullToolMeta
|
|
15585
15591
|
},
|
|
15586
15592
|
async (extra) => {
|
|
15587
15593
|
const args = extra.request?.params?.arguments ?? {};
|
|
@@ -15602,12 +15608,13 @@ function createAppServer(config, simulations, viteMode) {
|
|
|
15602
15608
|
};
|
|
15603
15609
|
}
|
|
15604
15610
|
);
|
|
15611
|
+
toolHandles.push({ handle: toolHandle, resourceName, toolMeta: fullToolMeta });
|
|
15605
15612
|
}
|
|
15606
|
-
const registeredUris = Array.from(
|
|
15613
|
+
const registeredUris = Array.from(registeredUriSet).join(", ");
|
|
15607
15614
|
console.log(
|
|
15608
15615
|
`[MCP] Registered ${simulations.length} tool(s) and resource(s)${viteMode ? " (vite mode)" : ""}: ${registeredUris}`
|
|
15609
15616
|
);
|
|
15610
|
-
return mcpServer;
|
|
15617
|
+
return { server: mcpServer, resourceHandles, toolHandles };
|
|
15611
15618
|
}
|
|
15612
15619
|
const SESSION_IDLE_TIMEOUT_MS$1 = 5 * 60 * 1e3;
|
|
15613
15620
|
function isLocalConnection(req) {
|
|
@@ -15674,11 +15681,18 @@ async function handleMcpRequest(req, res, config, simulations, viteMode) {
|
|
|
15674
15681
|
}
|
|
15675
15682
|
if (req.method === "POST") {
|
|
15676
15683
|
const isLocal = isLocalConnection(req);
|
|
15677
|
-
const server = createAppServer(config, simulations, viteMode);
|
|
15684
|
+
const { server, resourceHandles, toolHandles } = createAppServer(config, simulations, viteMode);
|
|
15678
15685
|
const transport = new StreamableHTTPServerTransport({
|
|
15679
15686
|
sessionIdGenerator: () => node_crypto.randomUUID(),
|
|
15680
15687
|
onsessioninitialized: (id2) => {
|
|
15681
|
-
sessions.set(id2, {
|
|
15688
|
+
sessions.set(id2, {
|
|
15689
|
+
server,
|
|
15690
|
+
transport,
|
|
15691
|
+
isLocal,
|
|
15692
|
+
lastActivity: Date.now(),
|
|
15693
|
+
resourceHandles,
|
|
15694
|
+
toolHandles
|
|
15695
|
+
});
|
|
15682
15696
|
const origin = isLocal ? "local" : "tunnel";
|
|
15683
15697
|
console.log(
|
|
15684
15698
|
`[MCP] Session started: ${id2.substring(0, 8)}... (${origin}, ${sessions.size} active)`
|
|
@@ -15793,16 +15807,24 @@ function runMCPServer(config) {
|
|
|
15793
15807
|
process.on("SIGINT", () => void shutdown());
|
|
15794
15808
|
return {
|
|
15795
15809
|
invalidateResources() {
|
|
15796
|
-
|
|
15810
|
+
if (sessions.size === 0) return;
|
|
15811
|
+
const timestamp = Date.now();
|
|
15797
15812
|
for (const [, session] of sessions) {
|
|
15798
|
-
|
|
15799
|
-
|
|
15800
|
-
|
|
15813
|
+
for (const [name, handle] of session.resourceHandles) {
|
|
15814
|
+
handle.update({ uri: `ui://${name}-${timestamp}` });
|
|
15815
|
+
}
|
|
15816
|
+
for (const { handle, resourceName, toolMeta } of session.toolHandles) {
|
|
15817
|
+
const newUri = `ui://${resourceName}-${timestamp}`;
|
|
15818
|
+
handle.update({
|
|
15819
|
+
_meta: {
|
|
15820
|
+
...toolMeta,
|
|
15821
|
+
ui: { ...toolMeta.ui, resourceUri: newUri },
|
|
15822
|
+
"ui/resourceUri": newUri
|
|
15823
|
+
}
|
|
15824
|
+
});
|
|
15801
15825
|
}
|
|
15802
15826
|
}
|
|
15803
|
-
|
|
15804
|
-
console.log(`[MCP] Notified ${notified} session(s) of resource changes`);
|
|
15805
|
-
}
|
|
15827
|
+
console.log(`[MCP] Cache-busted ${sessions.size} session(s) with timestamp ${timestamp}`);
|
|
15806
15828
|
}
|
|
15807
15829
|
};
|
|
15808
15830
|
}
|