sunpeak 0.16.29 → 0.17.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/bin/commands/dev.mjs +169 -342
- package/bin/commands/inspect.mjs +763 -0
- package/bin/commands/new.mjs +2 -2
- package/bin/lib/inspect/inspect-config.d.mts +20 -0
- package/bin/lib/inspect/inspect-config.mjs +76 -0
- package/bin/lib/live/global-setup.mjs +6 -1
- package/bin/sunpeak.js +11 -1
- package/dist/chatgpt/globals.css +8 -0
- package/dist/chatgpt/index.cjs +3 -11
- package/dist/chatgpt/index.cjs.map +1 -1
- package/dist/chatgpt/index.d.ts +2 -2
- package/dist/chatgpt/index.js +4 -8
- package/dist/chatgpt/index.js.map +1 -1
- package/dist/claude/index.cjs +1 -1
- package/dist/claude/index.js +1 -1
- package/dist/discovery-Cgoegt62.js +114 -0
- package/dist/discovery-Cgoegt62.js.map +1 -0
- package/dist/discovery-Clu4uHp1.cjs +161 -0
- package/dist/discovery-Clu4uHp1.cjs.map +1 -0
- package/dist/index.cjs +1 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/dist/lib/discovery-cli.cjs +1 -1
- package/dist/lib/discovery-cli.js +1 -1
- package/dist/lib/discovery.d.ts +7 -67
- package/dist/lib/index.d.ts +0 -1
- package/dist/mcp/index.cjs +34 -23
- package/dist/mcp/index.cjs.map +1 -1
- package/dist/mcp/index.js +34 -23
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/types.d.ts +5 -0
- package/dist/simulator/index.cjs +5 -11
- package/dist/simulator/index.cjs.map +1 -1
- package/dist/simulator/index.d.ts +4 -2
- package/dist/simulator/index.js +5 -8
- package/dist/simulator/index.js.map +1 -1
- package/dist/simulator/simple-sidebar.d.ts +7 -4
- package/dist/simulator/simulator-url.d.ts +8 -0
- package/dist/simulator/simulator.d.ts +15 -2
- package/dist/simulator/use-mcp-connection.d.ts +19 -0
- package/dist/{simulator-DIVvI69i.cjs → simulator-CH9hs0N6.cjs} +129 -21
- package/dist/simulator-CH9hs0N6.cjs.map +1 -0
- package/dist/{simulator-C7mkK7Sz.js → simulator-Dl8B-Ljb.js} +124 -22
- package/dist/simulator-Dl8B-Ljb.js.map +1 -0
- package/dist/{simulator-url-BDGD4vZD.cjs → simulator-url-CozKF1jf.cjs} +3 -1
- package/dist/simulator-url-CozKF1jf.cjs.map +1 -0
- package/dist/{simulator-url-Bkxj43yT.js → simulator-url-KoS_ToP6.js} +3 -1
- package/dist/simulator-url-KoS_ToP6.js.map +1 -0
- package/dist/style.css +8 -0
- package/package.json +11 -3
- package/template/dist/albums/albums.html +105 -0
- package/template/dist/albums/albums.json +16 -0
- package/template/dist/carousel/carousel.html +105 -0
- package/template/dist/carousel/carousel.json +16 -0
- package/template/dist/map/map.html +3060 -0
- package/template/dist/map/map.json +22 -0
- package/template/dist/review/review.html +105 -0
- package/template/dist/review/review.json +16 -0
- package/template/dist/server.js +15 -0
- package/template/dist/tools/review-diff.js +50 -0
- package/template/dist/tools/review-post.js +50 -0
- package/template/dist/tools/review-purchase.js +61 -0
- package/template/dist/tools/review.js +31 -0
- package/template/dist/tools/show-albums.js +56 -0
- package/template/dist/tools/show-carousel.js +41 -0
- package/template/dist/tools/show-map.js +47 -0
- package/template/node_modules/.vite/deps/_metadata.json +8 -0
- package/template/node_modules/.vite/deps/package.json +3 -0
- package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps.js +500 -0
- package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_app-bridge.js +563 -0
- package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_app-bridge.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_react.js +575 -0
- package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_react.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/@testing-library_react.js +11363 -0
- package/template/node_modules/.vite-mcp/deps/@testing-library_react.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/_metadata.json +130 -0
- package/template/node_modules/.vite-mcp/deps/chunk-BoAXSpZd.js +33 -0
- package/template/node_modules/.vite-mcp/deps/client-CU1wWud4.js +14385 -0
- package/template/node_modules/.vite-mcp/deps/client-CU1wWud4.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/clsx.js +18 -0
- package/template/node_modules/.vite-mcp/deps/clsx.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/dist-uWX8WbjY.js +505 -0
- package/template/node_modules/.vite-mcp/deps/dist-uWX8WbjY.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/embla-carousel-react.js +1461 -0
- package/template/node_modules/.vite-mcp/deps/embla-carousel-react.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/embla-carousel-wheel-gestures.js +536 -0
- package/template/node_modules/.vite-mcp/deps/embla-carousel-wheel-gestures.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/magic-string.es-Cklsmr-5.js +1013 -0
- package/template/node_modules/.vite-mcp/deps/magic-string.es-Cklsmr-5.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/mapbox-gl.js +46311 -0
- package/template/node_modules/.vite-mcp/deps/mapbox-gl.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/package.json +3 -0
- package/template/node_modules/.vite-mcp/deps/protocol-CTflwIfG.js +2090 -0
- package/template/node_modules/.vite-mcp/deps/protocol-CTflwIfG.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/react-dom.js +186 -0
- package/template/node_modules/.vite-mcp/deps/react-dom.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/react-dom_client.js +2 -0
- package/template/node_modules/.vite-mcp/deps/react.js +769 -0
- package/template/node_modules/.vite-mcp/deps/react.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/react_jsx-dev-runtime.js +205 -0
- package/template/node_modules/.vite-mcp/deps/react_jsx-dev-runtime.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/react_jsx-runtime.js +209 -0
- package/template/node_modules/.vite-mcp/deps/react_jsx-runtime.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/schemas-NsgmY9QV.js +12157 -0
- package/template/node_modules/.vite-mcp/deps/schemas-NsgmY9QV.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/tailwind-merge.js +2025 -0
- package/template/node_modules/.vite-mcp/deps/tailwind-merge.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/vitest.js +14021 -0
- package/template/node_modules/.vite-mcp/deps/vitest.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/zod.js +624 -0
- package/template/node_modules/.vite-mcp/deps/zod.js.map +1 -0
- package/template/src/tools/review-diff.test.ts +5 -1
- package/template/src/tools/review-diff.ts +1 -1
- package/template/src/tools/review-post.test.ts +5 -1
- package/template/src/tools/review-post.ts +1 -1
- package/template/src/tools/review-purchase.test.ts +5 -1
- package/template/src/tools/review-purchase.ts +1 -1
- package/template/src/tools/review.test.ts +5 -1
- package/template/src/tools/review.ts +1 -1
- package/template/src/tools/show-albums.test.ts +5 -1
- package/template/src/tools/show-albums.ts +1 -1
- package/template/src/tools/show-carousel.test.ts +5 -1
- package/template/src/tools/show-carousel.ts +1 -1
- package/template/src/tools/show-map.test.ts +5 -1
- package/template/src/tools/show-map.ts +1 -1
- package/dist/discovery-BxKCIgG5.cjs +0 -332
- package/dist/discovery-BxKCIgG5.cjs.map +0 -1
- package/dist/discovery-Du4LHrih.js +0 -261
- package/dist/discovery-Du4LHrih.js.map +0 -1
- package/dist/simulator-C7mkK7Sz.js.map +0 -1
- package/dist/simulator-DIVvI69i.cjs.map +0 -1
- package/dist/simulator-url-BDGD4vZD.cjs.map +0 -1
- package/dist/simulator-url-Bkxj43yT.js.map +0 -1
- package/template/.sunpeak/dev.tsx +0 -79
- package/template/.sunpeak/resource-loader.html +0 -20
- package/template/.sunpeak/resource-loader.tsx +0 -57
- package/template/index.html +0 -14
- package/template/src/resources/index.ts +0 -17
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sunpeak inspect` — Connect to an external MCP server and launch the simulator.
|
|
3
|
+
*
|
|
4
|
+
* This command lets users test their own MCP server in the sunpeak simulator
|
|
5
|
+
* without adopting the sunpeak framework conventions. It connects to the server
|
|
6
|
+
* via MCP protocol, discovers tools and resources, and serves the simulator UI.
|
|
7
|
+
*
|
|
8
|
+
* The core logic lives in `inspectServer()`, which is also used by `sunpeak dev`
|
|
9
|
+
* to serve the simulator UI pointed at the local MCP server.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* sunpeak inspect --server http://localhost:8000/mcp
|
|
13
|
+
* sunpeak inspect --server "python my_server.py"
|
|
14
|
+
* sunpeak inspect --server http://localhost:8000/mcp --simulations tests/simulations
|
|
15
|
+
*/
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
const { existsSync, readdirSync, readFileSync } = fs;
|
|
19
|
+
const { join, resolve, dirname } = path;
|
|
20
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
21
|
+
import { getPort } from '../lib/get-port.mjs';
|
|
22
|
+
import { startSandboxServer } from '../lib/sandbox-server.mjs';
|
|
23
|
+
|
|
24
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const SUNPEAK_PKG_DIR = resolve(__dirname, '..', '..');
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parse CLI arguments.
|
|
29
|
+
* @param {string[]} args
|
|
30
|
+
*/
|
|
31
|
+
function parseArgs(args) {
|
|
32
|
+
const opts = {
|
|
33
|
+
server: undefined,
|
|
34
|
+
simulations: undefined,
|
|
35
|
+
port: undefined,
|
|
36
|
+
name: undefined,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
for (let i = 0; i < args.length; i++) {
|
|
40
|
+
const arg = args[i];
|
|
41
|
+
if ((arg === '--server' || arg === '-s') && i + 1 < args.length) {
|
|
42
|
+
opts.server = args[++i];
|
|
43
|
+
} else if (arg === '--simulations' && i + 1 < args.length) {
|
|
44
|
+
opts.simulations = args[++i];
|
|
45
|
+
} else if ((arg === '--port' || arg === '-p') && i + 1 < args.length) {
|
|
46
|
+
opts.port = Number(args[++i]);
|
|
47
|
+
} else if (arg === '--name' && i + 1 < args.length) {
|
|
48
|
+
opts.name = args[++i];
|
|
49
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
50
|
+
printHelp();
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return opts;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function printHelp() {
|
|
59
|
+
console.log(`
|
|
60
|
+
sunpeak inspect — Test an external MCP server in the simulator
|
|
61
|
+
|
|
62
|
+
Usage:
|
|
63
|
+
sunpeak inspect --server <url-or-command>
|
|
64
|
+
|
|
65
|
+
Options:
|
|
66
|
+
--server, -s <url|cmd> MCP server URL or stdio command (required)
|
|
67
|
+
--simulations <dir> Simulation JSON directory (opt-in, no default)
|
|
68
|
+
--port, -p <number> Dev server port (default: 3000)
|
|
69
|
+
--name <string> App name in simulator chrome
|
|
70
|
+
--help, -h Show this help
|
|
71
|
+
|
|
72
|
+
Examples:
|
|
73
|
+
sunpeak inspect --server http://localhost:8000/mcp
|
|
74
|
+
sunpeak inspect --server "python my_server.py"
|
|
75
|
+
sunpeak inspect --server http://localhost:8000/mcp --simulations tests/simulations
|
|
76
|
+
`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Create an MCP client connection.
|
|
81
|
+
* @param {string} serverArg - URL or command string
|
|
82
|
+
* @returns {Promise<{ client: import('@modelcontextprotocol/sdk/client/index.js').Client, transport: import('@modelcontextprotocol/sdk/types.js').Transport }>}
|
|
83
|
+
*/
|
|
84
|
+
async function createMcpConnection(serverArg) {
|
|
85
|
+
const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');
|
|
86
|
+
const client = new Client({ name: 'sunpeak-inspector', version: '1.0.0' });
|
|
87
|
+
|
|
88
|
+
if (serverArg.startsWith('http://') || serverArg.startsWith('https://')) {
|
|
89
|
+
// HTTP/SSE transport
|
|
90
|
+
const { StreamableHTTPClientTransport } = await import(
|
|
91
|
+
'@modelcontextprotocol/sdk/client/streamableHttp.js'
|
|
92
|
+
);
|
|
93
|
+
const transport = new StreamableHTTPClientTransport(new URL(serverArg));
|
|
94
|
+
await client.connect(transport);
|
|
95
|
+
return { client, transport };
|
|
96
|
+
} else {
|
|
97
|
+
// Stdio transport — parse command string
|
|
98
|
+
const parts = serverArg.split(/\s+/);
|
|
99
|
+
const command = parts[0];
|
|
100
|
+
const cmdArgs = parts.slice(1);
|
|
101
|
+
const { StdioClientTransport } = await import(
|
|
102
|
+
'@modelcontextprotocol/sdk/client/stdio.js'
|
|
103
|
+
);
|
|
104
|
+
const transport = new StdioClientTransport({ command, args: cmdArgs });
|
|
105
|
+
await client.connect(transport);
|
|
106
|
+
return { client, transport };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Discover tools and resources from the MCP server and build Simulation objects.
|
|
112
|
+
* @param {import('@modelcontextprotocol/sdk/client/index.js').Client} client
|
|
113
|
+
* @returns {Promise<Record<string, object>>} Map of simulation name → Simulation-shaped objects
|
|
114
|
+
*/
|
|
115
|
+
async function discoverSimulations(client) {
|
|
116
|
+
const { tools } = await client.listTools();
|
|
117
|
+
|
|
118
|
+
// Try to list resources (server may not support them)
|
|
119
|
+
let resources = [];
|
|
120
|
+
try {
|
|
121
|
+
const result = await client.listResources();
|
|
122
|
+
resources = result.resources || [];
|
|
123
|
+
} catch {
|
|
124
|
+
// Server doesn't support resources — that's fine
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Build resource URI map
|
|
128
|
+
const resourceByUri = new Map();
|
|
129
|
+
for (const resource of resources) {
|
|
130
|
+
resourceByUri.set(resource.uri, resource);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const simulations = {};
|
|
134
|
+
|
|
135
|
+
for (const tool of tools) {
|
|
136
|
+
const simName = tool.name;
|
|
137
|
+
|
|
138
|
+
// Match tool to resource via _meta.ui.resourceUri (MCP Apps extension).
|
|
139
|
+
// Supports both nested format (_meta.ui.resourceUri) and deprecated flat
|
|
140
|
+
// format (_meta["ui/resourceUri"]).
|
|
141
|
+
let resource;
|
|
142
|
+
let resourceUrl;
|
|
143
|
+
const uri = tool._meta?.ui?.resourceUri ?? tool._meta?.['ui/resourceUri'];
|
|
144
|
+
if (uri) {
|
|
145
|
+
resource = resourceByUri.get(uri);
|
|
146
|
+
if (resource) {
|
|
147
|
+
resourceUrl = `/__sunpeak/read-resource?uri=${encodeURIComponent(uri)}`;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
simulations[simName] = {
|
|
152
|
+
name: simName,
|
|
153
|
+
tool,
|
|
154
|
+
resource,
|
|
155
|
+
resourceUrl,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return simulations;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Load simulation JSON fixtures from a directory and merge into discovered simulations.
|
|
164
|
+
* @param {string} dir - Simulation directory path
|
|
165
|
+
* @param {Record<string, object>} simulations - Discovered simulations to merge into
|
|
166
|
+
*/
|
|
167
|
+
function mergeSimulationFixtures(dir, simulations) {
|
|
168
|
+
if (!existsSync(dir)) return;
|
|
169
|
+
|
|
170
|
+
const files = readdirSync(dir).filter((f) => f.endsWith('.json'));
|
|
171
|
+
for (const file of files) {
|
|
172
|
+
try {
|
|
173
|
+
const fixture = JSON.parse(readFileSync(join(dir, file), 'utf-8'));
|
|
174
|
+
const toolName = fixture.tool;
|
|
175
|
+
if (!toolName) continue;
|
|
176
|
+
|
|
177
|
+
// Find matching simulation by tool name
|
|
178
|
+
const sim = simulations[toolName];
|
|
179
|
+
if (sim) {
|
|
180
|
+
// Merge fixture data into discovered simulation
|
|
181
|
+
if (fixture.toolInput !== undefined) sim.toolInput = fixture.toolInput;
|
|
182
|
+
if (fixture.toolResult !== undefined) sim.toolResult = fixture.toolResult;
|
|
183
|
+
if (fixture.serverTools !== undefined) sim.serverTools = fixture.serverTools;
|
|
184
|
+
if (fixture.userMessage !== undefined) sim.userMessage = fixture.userMessage;
|
|
185
|
+
if (fixture.hostContext !== undefined) sim.hostContext = fixture.hostContext;
|
|
186
|
+
} else {
|
|
187
|
+
// Create a new simulation from the fixture (tool not on server, but user wants to mock it)
|
|
188
|
+
const simName = file.replace(/\.json$/, '');
|
|
189
|
+
simulations[simName] = {
|
|
190
|
+
name: simName,
|
|
191
|
+
tool: { name: toolName, inputSchema: { type: 'object' } },
|
|
192
|
+
toolInput: fixture.toolInput,
|
|
193
|
+
toolResult: fixture.toolResult,
|
|
194
|
+
serverTools: fixture.serverTools,
|
|
195
|
+
userMessage: fixture.userMessage,
|
|
196
|
+
hostContext: fixture.hostContext,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
} catch (err) {
|
|
200
|
+
console.warn(`Warning: Failed to parse simulation fixture ${file}:`, err.message);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Vite plugin that serves virtual modules for the inspect entry point.
|
|
207
|
+
*
|
|
208
|
+
* @param {Record<string, object>} simulations - Simulation objects
|
|
209
|
+
* @param {string|null} serverUrl - MCP server URL (null in framework mode to hide server UI)
|
|
210
|
+
* @param {string} appName - Display name
|
|
211
|
+
* @param {string|null} appIcon - Icon URL or emoji
|
|
212
|
+
* @param {string} sandboxUrl - Sandbox server URL
|
|
213
|
+
* @param {{ defaultProdTools?: boolean, defaultProdResources?: boolean }} [modeFlags] - Mode toggles
|
|
214
|
+
*/
|
|
215
|
+
function sunpeakInspectVirtualPlugin(simulations, serverUrl, appName, appIcon, sandboxUrl, modeFlags = {}) {
|
|
216
|
+
const ENTRY_ID = 'virtual:sunpeak-inspect-entry';
|
|
217
|
+
const RESOLVED_ENTRY_ID = '\0' + ENTRY_ID;
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
name: 'sunpeak-inspect-virtual',
|
|
221
|
+
resolveId(id) {
|
|
222
|
+
if (id === ENTRY_ID) return RESOLVED_ENTRY_ID;
|
|
223
|
+
},
|
|
224
|
+
load(id) {
|
|
225
|
+
if (id !== RESOLVED_ENTRY_ID) return;
|
|
226
|
+
|
|
227
|
+
// In framework mode (serverUrl is null), don't pass mcpServerUrl to the
|
|
228
|
+
// Simulator — this enables the prod-tools/prod-resources toggles and hides
|
|
229
|
+
// the server URL input in the sidebar.
|
|
230
|
+
const mcpServerUrlProp = serverUrl != null
|
|
231
|
+
? `mcpServerUrl: ${JSON.stringify(serverUrl)},`
|
|
232
|
+
: '';
|
|
233
|
+
|
|
234
|
+
return `
|
|
235
|
+
import { createElement, StrictMode } from 'react';
|
|
236
|
+
import { createRoot } from 'react-dom/client';
|
|
237
|
+
import { Simulator } from 'sunpeak/simulator';
|
|
238
|
+
import 'sunpeak/style.css';
|
|
239
|
+
import 'sunpeak/chatgpt/globals.css';
|
|
240
|
+
|
|
241
|
+
const simulations = ${JSON.stringify(simulations)};
|
|
242
|
+
const appName = ${JSON.stringify(appName ?? 'MCP Inspector')};
|
|
243
|
+
const appIcon = ${JSON.stringify(appIcon ?? null)};
|
|
244
|
+
const sandboxUrl = ${JSON.stringify(sandboxUrl)};
|
|
245
|
+
const defaultProdTools = ${JSON.stringify(modeFlags.defaultProdTools ?? false)};
|
|
246
|
+
const defaultProdResources = ${JSON.stringify(modeFlags.defaultProdResources ?? false)};
|
|
247
|
+
|
|
248
|
+
const onCallTool = async (params) => {
|
|
249
|
+
const res = await fetch('/__sunpeak/call-tool', {
|
|
250
|
+
method: 'POST',
|
|
251
|
+
headers: { 'Content-Type': 'application/json' },
|
|
252
|
+
body: JSON.stringify(params),
|
|
253
|
+
});
|
|
254
|
+
return res.json();
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const onCallToolDirect = async (params) => {
|
|
258
|
+
const res = await fetch('/__sunpeak/call-tool-direct', {
|
|
259
|
+
method: 'POST',
|
|
260
|
+
headers: { 'Content-Type': 'application/json' },
|
|
261
|
+
body: JSON.stringify(params),
|
|
262
|
+
});
|
|
263
|
+
return res.json();
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const root = createRoot(document.getElementById('root'));
|
|
267
|
+
root.render(
|
|
268
|
+
createElement(StrictMode, null,
|
|
269
|
+
createElement(Simulator, {
|
|
270
|
+
simulations,
|
|
271
|
+
${mcpServerUrlProp}
|
|
272
|
+
appName,
|
|
273
|
+
appIcon,
|
|
274
|
+
sandboxUrl,
|
|
275
|
+
onCallTool,
|
|
276
|
+
onCallToolDirect,
|
|
277
|
+
defaultProdTools,
|
|
278
|
+
defaultProdResources,
|
|
279
|
+
})
|
|
280
|
+
)
|
|
281
|
+
);
|
|
282
|
+
`;
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Vite plugin for MCP server proxy endpoints.
|
|
289
|
+
* @param {() => import('@modelcontextprotocol/sdk/client/index.js').Client} getClient
|
|
290
|
+
* @param {{ callToolDirect?: (name: string, args: Record<string, unknown>) => Promise<object> }} [pluginOpts]
|
|
291
|
+
*/
|
|
292
|
+
function sunpeakInspectEndpointsPlugin(getClient, pluginOpts = {}) {
|
|
293
|
+
return {
|
|
294
|
+
name: 'sunpeak-inspect-endpoints',
|
|
295
|
+
configureServer(server) {
|
|
296
|
+
// List tools from connected server
|
|
297
|
+
server.middlewares.use('/__sunpeak/list-tools', async (_req, res) => {
|
|
298
|
+
try {
|
|
299
|
+
const client = getClient();
|
|
300
|
+
const result = await client.listTools();
|
|
301
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
302
|
+
res.end(JSON.stringify(result));
|
|
303
|
+
} catch (err) {
|
|
304
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
305
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// Call tool on connected server
|
|
310
|
+
server.middlewares.use('/__sunpeak/call-tool', async (req, res) => {
|
|
311
|
+
if (req.method !== 'POST') {
|
|
312
|
+
res.writeHead(405);
|
|
313
|
+
res.end('Method not allowed');
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const body = await readRequestBody(req);
|
|
318
|
+
let parsed;
|
|
319
|
+
try {
|
|
320
|
+
parsed = JSON.parse(body);
|
|
321
|
+
} catch {
|
|
322
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
323
|
+
res.end(JSON.stringify({ error: 'Invalid JSON in request body' }));
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
const { name, arguments: args } = parsed;
|
|
329
|
+
const client = getClient();
|
|
330
|
+
const result = await client.callTool({ name, arguments: args });
|
|
331
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
332
|
+
res.end(JSON.stringify(result));
|
|
333
|
+
} catch (err) {
|
|
334
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
335
|
+
res.end(
|
|
336
|
+
JSON.stringify({
|
|
337
|
+
content: [{ type: 'text', text: `Error: ${err.message}` }],
|
|
338
|
+
isError: true,
|
|
339
|
+
})
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// Call tool handler directly, bypassing MCP server mock data.
|
|
345
|
+
// Used by the Prod Tools Run button so the real handler executes even
|
|
346
|
+
// when the MCP server would return simulation fixture data.
|
|
347
|
+
server.middlewares.use('/__sunpeak/call-tool-direct', async (req, res) => {
|
|
348
|
+
if (req.method !== 'POST') {
|
|
349
|
+
res.writeHead(405);
|
|
350
|
+
res.end('Method not allowed');
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (!pluginOpts.callToolDirect) {
|
|
355
|
+
// No direct handler available (pure inspect mode) — fall back to MCP
|
|
356
|
+
const body = await readRequestBody(req);
|
|
357
|
+
let parsed;
|
|
358
|
+
try { parsed = JSON.parse(body); } catch {
|
|
359
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
360
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
try {
|
|
364
|
+
const client = getClient();
|
|
365
|
+
const result = await client.callTool({ name: parsed.name, arguments: parsed.arguments });
|
|
366
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
367
|
+
res.end(JSON.stringify(result));
|
|
368
|
+
} catch (err) {
|
|
369
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
370
|
+
res.end(JSON.stringify({ content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true }));
|
|
371
|
+
}
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const body = await readRequestBody(req);
|
|
376
|
+
let parsed;
|
|
377
|
+
try { parsed = JSON.parse(body); } catch {
|
|
378
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
379
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
try {
|
|
384
|
+
const result = await pluginOpts.callToolDirect(parsed.name, parsed.arguments ?? {});
|
|
385
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
386
|
+
res.end(JSON.stringify(result));
|
|
387
|
+
} catch (err) {
|
|
388
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
389
|
+
res.end(JSON.stringify({ content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true }));
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// Reconnect to a new server URL.
|
|
394
|
+
// Currently acknowledges without actually reconnecting — the MCP client
|
|
395
|
+
// connection is established at startup. Changing the URL in the sidebar
|
|
396
|
+
// requires restarting the inspect server. This endpoint exists so the
|
|
397
|
+
// useMcpConnection hook can verify the server is reachable.
|
|
398
|
+
server.middlewares.use('/__sunpeak/connect', async (req, res) => {
|
|
399
|
+
if (req.method !== 'POST') {
|
|
400
|
+
res.writeHead(405);
|
|
401
|
+
res.end('Method not allowed');
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
// Verify the MCP client is still connected by listing tools
|
|
407
|
+
const client = getClient();
|
|
408
|
+
await client.listTools();
|
|
409
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
410
|
+
res.end(JSON.stringify({ status: 'ok' }));
|
|
411
|
+
} catch (err) {
|
|
412
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
413
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// Read resource from connected server
|
|
418
|
+
server.middlewares.use('/__sunpeak/read-resource', async (req, res) => {
|
|
419
|
+
const url = new URL(req.url, 'http://localhost');
|
|
420
|
+
const uri = url.searchParams.get('uri');
|
|
421
|
+
if (!uri) {
|
|
422
|
+
res.writeHead(400);
|
|
423
|
+
res.end('Missing uri parameter');
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
try {
|
|
428
|
+
const client = getClient();
|
|
429
|
+
const result = await client.readResource({ uri });
|
|
430
|
+
const content = result.contents?.[0];
|
|
431
|
+
if (!content) {
|
|
432
|
+
res.writeHead(404);
|
|
433
|
+
res.end('Resource not found');
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const mimeType = content.mimeType || 'text/html';
|
|
438
|
+
res.writeHead(200, { 'Content-Type': `${mimeType}; charset=utf-8` });
|
|
439
|
+
if (typeof content.text === 'string') {
|
|
440
|
+
res.end(content.text);
|
|
441
|
+
} else if (content.blob) {
|
|
442
|
+
res.end(Buffer.from(content.blob, 'base64'));
|
|
443
|
+
} else {
|
|
444
|
+
res.end('');
|
|
445
|
+
}
|
|
446
|
+
} catch (err) {
|
|
447
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
448
|
+
res.end(`Error reading resource: ${err.message}`);
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
},
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Read the full body of an HTTP request.
|
|
457
|
+
*/
|
|
458
|
+
function readRequestBody(req) {
|
|
459
|
+
return new Promise((resolve, reject) => {
|
|
460
|
+
let data = '';
|
|
461
|
+
req.on('data', (chunk) => (data += chunk));
|
|
462
|
+
req.on('end', () => resolve(data));
|
|
463
|
+
req.on('error', reject);
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Core inspect server logic. Connects to an MCP server, discovers tools/resources,
|
|
469
|
+
* merges simulation fixtures, and serves the simulator UI via Vite.
|
|
470
|
+
*
|
|
471
|
+
* Used by both `sunpeak inspect` (CLI) and `sunpeak dev` (programmatic).
|
|
472
|
+
*
|
|
473
|
+
* @param {object} opts
|
|
474
|
+
* @param {string} opts.server - MCP server URL or stdio command
|
|
475
|
+
* @param {string|null} [opts.simulationsDir] - Path to simulation fixtures directory
|
|
476
|
+
* @param {number} [opts.port] - Dev server port (default: 3000)
|
|
477
|
+
* @param {string} [opts.name] - App name override
|
|
478
|
+
* @param {string} [opts.sandboxUrl] - Existing sandbox server URL (skips creating one)
|
|
479
|
+
* @param {boolean} [opts.frameworkMode] - If true, hide server URL UI and show mode toggles
|
|
480
|
+
* @param {boolean} [opts.defaultProdTools] - Initial prod tools state
|
|
481
|
+
* @param {boolean} [opts.defaultProdResources] - Initial prod resources state
|
|
482
|
+
* @param {string} [opts.projectRoot] - Project directory for serving /dist/ files (prod resources)
|
|
483
|
+
* @param {boolean} [opts.noBegging] - Suppress star message
|
|
484
|
+
* @param {boolean} [opts.open] - Whether to open browser (default: !CI && !SUNPEAK_LIVE_TEST)
|
|
485
|
+
* @param {(name: string, args: Record<string, unknown>) => Promise<object>} [opts.callToolDirect] - Direct handler call (bypasses MCP, for prod-tools)
|
|
486
|
+
* @param {() => Promise<void>} [opts.onCleanup] - Additional cleanup callback on exit
|
|
487
|
+
*/
|
|
488
|
+
export async function inspectServer(opts) {
|
|
489
|
+
const {
|
|
490
|
+
server: serverArg,
|
|
491
|
+
simulationsDir = null,
|
|
492
|
+
port: preferredPort,
|
|
493
|
+
name: nameOverride,
|
|
494
|
+
sandboxUrl: existingSandboxUrl,
|
|
495
|
+
frameworkMode = false,
|
|
496
|
+
defaultProdTools = false,
|
|
497
|
+
defaultProdResources = false,
|
|
498
|
+
projectRoot = null,
|
|
499
|
+
noBegging = false,
|
|
500
|
+
open,
|
|
501
|
+
onCleanup,
|
|
502
|
+
} = opts;
|
|
503
|
+
|
|
504
|
+
// Load favicon from sunpeak package for the inspector UI.
|
|
505
|
+
let faviconDataUri = null;
|
|
506
|
+
let faviconBuffer = null;
|
|
507
|
+
try {
|
|
508
|
+
const distMcp = join(SUNPEAK_PKG_DIR, 'dist/mcp/index.js');
|
|
509
|
+
if (existsSync(distMcp)) {
|
|
510
|
+
const mod = await import(pathToFileURL(distMcp).href);
|
|
511
|
+
faviconDataUri = mod.FAVICON_DATA_URI;
|
|
512
|
+
faviconBuffer = mod.FAVICON_BUFFER;
|
|
513
|
+
}
|
|
514
|
+
} catch {
|
|
515
|
+
// Non-fatal — inspector will just not have a favicon
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
console.log(`Connecting to MCP server: ${serverArg}`);
|
|
519
|
+
|
|
520
|
+
// Connect to the MCP server (with retry for local servers that may still be starting)
|
|
521
|
+
let mcpConnection;
|
|
522
|
+
const maxRetries = 5;
|
|
523
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
524
|
+
try {
|
|
525
|
+
mcpConnection = await createMcpConnection(serverArg);
|
|
526
|
+
break;
|
|
527
|
+
} catch (err) {
|
|
528
|
+
if (attempt === maxRetries) {
|
|
529
|
+
console.error(`Failed to connect to MCP server: ${err.message}`);
|
|
530
|
+
process.exit(1);
|
|
531
|
+
}
|
|
532
|
+
console.log(`Connection attempt ${attempt}/${maxRetries} failed, retrying...`);
|
|
533
|
+
await new Promise(r => setTimeout(r, 500));
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
console.log('Connected. Discovering tools and resources...');
|
|
538
|
+
|
|
539
|
+
// Extract app name and icon from server info (reported during MCP initialize)
|
|
540
|
+
const serverInfo = mcpConnection.client.getServerVersion();
|
|
541
|
+
const serverAppName = nameOverride ?? serverInfo?.name;
|
|
542
|
+
const serverAppIcon = serverInfo?.icons?.[0]?.src;
|
|
543
|
+
|
|
544
|
+
// Discover tools/resources and build simulations
|
|
545
|
+
const simulations = await discoverSimulations(mcpConnection.client);
|
|
546
|
+
const toolCount = Object.keys(simulations).length;
|
|
547
|
+
const resourceCount = Object.values(simulations).filter((s) => s.resource).length;
|
|
548
|
+
console.log(`Found ${toolCount} tool(s), ${resourceCount} resource(s).`);
|
|
549
|
+
|
|
550
|
+
// Merge simulation fixtures when a directory is provided
|
|
551
|
+
if (simulationsDir) {
|
|
552
|
+
mergeSimulationFixtures(simulationsDir, simulations);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Start or reuse sandbox server
|
|
556
|
+
let sandbox;
|
|
557
|
+
let ownsSandbox = false;
|
|
558
|
+
if (existingSandboxUrl) {
|
|
559
|
+
sandbox = { url: existingSandboxUrl, close: async () => {} };
|
|
560
|
+
} else {
|
|
561
|
+
const sandboxPort = Number(process.env.SUNPEAK_SANDBOX_PORT) || undefined;
|
|
562
|
+
sandbox = await startSandboxServer({
|
|
563
|
+
preferredPort: sandboxPort ?? 24680,
|
|
564
|
+
});
|
|
565
|
+
ownsSandbox = true;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Determine server port
|
|
569
|
+
const port = preferredPort || Number(process.env.PORT) || (await getPort(3000));
|
|
570
|
+
|
|
571
|
+
// Import Vite
|
|
572
|
+
const { createServer } = await import('vite');
|
|
573
|
+
const react = (await import('@vitejs/plugin-react')).default;
|
|
574
|
+
|
|
575
|
+
// Build the virtual index.html
|
|
576
|
+
const appTitle = (serverAppName ?? 'MCP Inspector').replace(/[<>&"']/g, (c) =>
|
|
577
|
+
({ '<': '<', '>': '>', '&': '&', '"': '"', "'": ''' })[c]
|
|
578
|
+
);
|
|
579
|
+
const indexHtml = `<!DOCTYPE html>
|
|
580
|
+
<html lang="en">
|
|
581
|
+
<head>
|
|
582
|
+
<meta charset="UTF-8" />
|
|
583
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
584
|
+
<title>${appTitle} — sunpeak</title>${faviconDataUri ? `\n <link rel="icon" type="image/png" href="${faviconDataUri}" />` : ''}
|
|
585
|
+
<style>html, body, #root { margin: 0; padding: 0; height: 100%; }</style>
|
|
586
|
+
</head>
|
|
587
|
+
<body>
|
|
588
|
+
<div id="root"></div>
|
|
589
|
+
<script type="module" src="/@id/__x00__virtual:sunpeak-inspect-entry"></script>
|
|
590
|
+
</body>
|
|
591
|
+
</html>`;
|
|
592
|
+
|
|
593
|
+
// In framework mode, don't pass serverUrl to the Simulator (hides the server
|
|
594
|
+
// URL input in the sidebar, enables prod-tools/prod-resources toggles).
|
|
595
|
+
const simulatorServerUrl = frameworkMode ? null : serverArg;
|
|
596
|
+
|
|
597
|
+
// Create the Vite server.
|
|
598
|
+
// Use the sunpeak package dir as root to avoid scanning the user's project
|
|
599
|
+
// files for dependencies (which can cause resolution errors for @ aliases etc.)
|
|
600
|
+
const server = await createServer({
|
|
601
|
+
root: SUNPEAK_PKG_DIR,
|
|
602
|
+
configFile: false,
|
|
603
|
+
plugins: [
|
|
604
|
+
react(),
|
|
605
|
+
sunpeakInspectVirtualPlugin(
|
|
606
|
+
simulations,
|
|
607
|
+
simulatorServerUrl,
|
|
608
|
+
serverAppName,
|
|
609
|
+
serverAppIcon,
|
|
610
|
+
sandbox.url,
|
|
611
|
+
{ defaultProdTools, defaultProdResources }
|
|
612
|
+
),
|
|
613
|
+
sunpeakInspectEndpointsPlugin(() => mcpConnection.client, {
|
|
614
|
+
callToolDirect: opts.callToolDirect,
|
|
615
|
+
}),
|
|
616
|
+
// Serve /dist/{name}/{name}.html from the project directory (for Prod Resources mode).
|
|
617
|
+
// The Simulator polls these paths via HEAD to check if built resources exist.
|
|
618
|
+
// Only intercepts .html files under /dist/ — other /dist/ paths (like sunpeak's
|
|
619
|
+
// own dist/simulator/index.js) must fall through to Vite's module resolution.
|
|
620
|
+
...(projectRoot ? [{
|
|
621
|
+
name: 'sunpeak-dist-serve',
|
|
622
|
+
configureServer(server) {
|
|
623
|
+
server.middlewares.use((req, res, next) => {
|
|
624
|
+
if (!req.url?.startsWith('/dist/') || !req.url.endsWith('.html')) return next();
|
|
625
|
+
const filePath = join(projectRoot, req.url);
|
|
626
|
+
if (existsSync(filePath)) {
|
|
627
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
628
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
629
|
+
res.end(content);
|
|
630
|
+
} else {
|
|
631
|
+
res.writeHead(404);
|
|
632
|
+
res.end('Not found');
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
},
|
|
636
|
+
}] : []),
|
|
637
|
+
// Serve virtual index.html
|
|
638
|
+
{
|
|
639
|
+
name: 'sunpeak-inspect-index-html',
|
|
640
|
+
configureServer(server) {
|
|
641
|
+
// Serve index.html for all non-API, non-asset requests (SPA fallback)
|
|
642
|
+
server.middlewares.use((req, res, next) => {
|
|
643
|
+
if (
|
|
644
|
+
req.url === '/' ||
|
|
645
|
+
req.url === '/index.html' ||
|
|
646
|
+
(!req.url.startsWith('/__sunpeak/') &&
|
|
647
|
+
!req.url.startsWith('/@') &&
|
|
648
|
+
!req.url.startsWith('/node_modules/') &&
|
|
649
|
+
req.url !== '/health' &&
|
|
650
|
+
!req.url.includes('.'))
|
|
651
|
+
) {
|
|
652
|
+
// Transform through Vite to resolve module imports
|
|
653
|
+
server.transformIndexHtml(req.url, indexHtml).then((html) => {
|
|
654
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
655
|
+
res.end(html);
|
|
656
|
+
}).catch(next);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
next();
|
|
660
|
+
});
|
|
661
|
+
},
|
|
662
|
+
},
|
|
663
|
+
// Paint fence responder
|
|
664
|
+
{
|
|
665
|
+
name: 'sunpeak-fence-responder',
|
|
666
|
+
transformIndexHtml(html) {
|
|
667
|
+
const fenceScript = `<script>window.addEventListener("message",function(e){if(e.data&&e.data.method==="sunpeak/fence"){var fid=e.data.params&&e.data.params.fenceId;requestAnimationFrame(function(){e.source.postMessage({jsonrpc:"2.0",method:"sunpeak/fence-ack",params:{fenceId:fid}},"*");});}});</script>`;
|
|
668
|
+
return html.replace('</head>', fenceScript + '</head>');
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
// Favicon
|
|
672
|
+
...(faviconBuffer ? [{
|
|
673
|
+
name: 'sunpeak-favicon',
|
|
674
|
+
configureServer(server) {
|
|
675
|
+
server.middlewares.use('/favicon.ico', (_req, res) => {
|
|
676
|
+
res.writeHead(200, {
|
|
677
|
+
'Content-Type': 'image/png',
|
|
678
|
+
'Content-Length': faviconBuffer.length,
|
|
679
|
+
'Cache-Control': 'public, max-age=86400',
|
|
680
|
+
});
|
|
681
|
+
res.end(faviconBuffer);
|
|
682
|
+
});
|
|
683
|
+
},
|
|
684
|
+
}] : []),
|
|
685
|
+
// Health endpoint
|
|
686
|
+
{
|
|
687
|
+
name: 'sunpeak-health',
|
|
688
|
+
configureServer(server) {
|
|
689
|
+
server.middlewares.use('/health', (_req, res) => {
|
|
690
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
691
|
+
res.end(JSON.stringify({ status: 'ok' }));
|
|
692
|
+
});
|
|
693
|
+
},
|
|
694
|
+
},
|
|
695
|
+
],
|
|
696
|
+
server: {
|
|
697
|
+
port,
|
|
698
|
+
open: open ?? (!process.env.CI && !process.env.SUNPEAK_LIVE_TEST),
|
|
699
|
+
allowedHosts: 'all',
|
|
700
|
+
},
|
|
701
|
+
optimizeDeps: {
|
|
702
|
+
// Only pre-bundle React — the virtual entry module imports sunpeak from
|
|
703
|
+
// node_modules, so no user source scanning needed.
|
|
704
|
+
include: ['react', 'react-dom', 'react/jsx-runtime'],
|
|
705
|
+
// Disable scanning user's project files (avoids @ alias resolution errors)
|
|
706
|
+
entries: [],
|
|
707
|
+
},
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
await server.listen();
|
|
711
|
+
server.printUrls();
|
|
712
|
+
server.bindCLIShortcuts({ print: true });
|
|
713
|
+
|
|
714
|
+
// Print star-begging message unless suppressed
|
|
715
|
+
if (!noBegging) {
|
|
716
|
+
// #FFB800 in 24-bit ANSI color
|
|
717
|
+
console.log('\n\n\x1b[38;2;255;184;0m\u2b50\ufe0f \u2192 \u2764\ufe0f https://github.com/Sunpeak-AI/sunpeak\x1b[0m\n');
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Cleanup on exit
|
|
721
|
+
const cleanup = async () => {
|
|
722
|
+
if (ownsSandbox) await sandbox.close();
|
|
723
|
+
try {
|
|
724
|
+
await mcpConnection.client.close();
|
|
725
|
+
} catch {
|
|
726
|
+
// Ignore close errors
|
|
727
|
+
}
|
|
728
|
+
await server.close();
|
|
729
|
+
if (onCleanup) await onCleanup();
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
process.on('SIGINT', async () => {
|
|
733
|
+
await cleanup();
|
|
734
|
+
process.exit(0);
|
|
735
|
+
});
|
|
736
|
+
process.on('SIGTERM', async () => {
|
|
737
|
+
await cleanup();
|
|
738
|
+
process.exit(0);
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* CLI entry point for `sunpeak inspect`.
|
|
744
|
+
*/
|
|
745
|
+
export async function inspect(args) {
|
|
746
|
+
const opts = parseArgs(args);
|
|
747
|
+
|
|
748
|
+
if (!opts.server) {
|
|
749
|
+
console.error('Error: --server is required.');
|
|
750
|
+
console.error('Run "sunpeak inspect --help" for usage.');
|
|
751
|
+
process.exit(1);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const projectRoot = process.cwd();
|
|
755
|
+
const simulationsDir = opts.simulations ? resolve(projectRoot, opts.simulations) : null;
|
|
756
|
+
|
|
757
|
+
await inspectServer({
|
|
758
|
+
server: opts.server,
|
|
759
|
+
simulationsDir,
|
|
760
|
+
port: opts.port,
|
|
761
|
+
name: opts.name,
|
|
762
|
+
});
|
|
763
|
+
}
|