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.
Files changed (140) hide show
  1. package/bin/commands/dev.mjs +169 -342
  2. package/bin/commands/inspect.mjs +763 -0
  3. package/bin/commands/new.mjs +2 -2
  4. package/bin/lib/inspect/inspect-config.d.mts +20 -0
  5. package/bin/lib/inspect/inspect-config.mjs +76 -0
  6. package/bin/lib/live/global-setup.mjs +6 -1
  7. package/bin/sunpeak.js +11 -1
  8. package/dist/chatgpt/globals.css +8 -0
  9. package/dist/chatgpt/index.cjs +3 -11
  10. package/dist/chatgpt/index.cjs.map +1 -1
  11. package/dist/chatgpt/index.d.ts +2 -2
  12. package/dist/chatgpt/index.js +4 -8
  13. package/dist/chatgpt/index.js.map +1 -1
  14. package/dist/claude/index.cjs +1 -1
  15. package/dist/claude/index.js +1 -1
  16. package/dist/discovery-Cgoegt62.js +114 -0
  17. package/dist/discovery-Cgoegt62.js.map +1 -0
  18. package/dist/discovery-Clu4uHp1.cjs +161 -0
  19. package/dist/discovery-Clu4uHp1.cjs.map +1 -0
  20. package/dist/index.cjs +1 -4
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.js +2 -3
  23. package/dist/index.js.map +1 -1
  24. package/dist/lib/discovery-cli.cjs +1 -1
  25. package/dist/lib/discovery-cli.js +1 -1
  26. package/dist/lib/discovery.d.ts +7 -67
  27. package/dist/lib/index.d.ts +0 -1
  28. package/dist/mcp/index.cjs +34 -23
  29. package/dist/mcp/index.cjs.map +1 -1
  30. package/dist/mcp/index.js +34 -23
  31. package/dist/mcp/index.js.map +1 -1
  32. package/dist/mcp/types.d.ts +5 -0
  33. package/dist/simulator/index.cjs +5 -11
  34. package/dist/simulator/index.cjs.map +1 -1
  35. package/dist/simulator/index.d.ts +4 -2
  36. package/dist/simulator/index.js +5 -8
  37. package/dist/simulator/index.js.map +1 -1
  38. package/dist/simulator/simple-sidebar.d.ts +7 -4
  39. package/dist/simulator/simulator-url.d.ts +8 -0
  40. package/dist/simulator/simulator.d.ts +15 -2
  41. package/dist/simulator/use-mcp-connection.d.ts +19 -0
  42. package/dist/{simulator-DIVvI69i.cjs → simulator-CH9hs0N6.cjs} +129 -21
  43. package/dist/simulator-CH9hs0N6.cjs.map +1 -0
  44. package/dist/{simulator-C7mkK7Sz.js → simulator-Dl8B-Ljb.js} +124 -22
  45. package/dist/simulator-Dl8B-Ljb.js.map +1 -0
  46. package/dist/{simulator-url-BDGD4vZD.cjs → simulator-url-CozKF1jf.cjs} +3 -1
  47. package/dist/simulator-url-CozKF1jf.cjs.map +1 -0
  48. package/dist/{simulator-url-Bkxj43yT.js → simulator-url-KoS_ToP6.js} +3 -1
  49. package/dist/simulator-url-KoS_ToP6.js.map +1 -0
  50. package/dist/style.css +8 -0
  51. package/package.json +11 -3
  52. package/template/dist/albums/albums.html +105 -0
  53. package/template/dist/albums/albums.json +16 -0
  54. package/template/dist/carousel/carousel.html +105 -0
  55. package/template/dist/carousel/carousel.json +16 -0
  56. package/template/dist/map/map.html +3060 -0
  57. package/template/dist/map/map.json +22 -0
  58. package/template/dist/review/review.html +105 -0
  59. package/template/dist/review/review.json +16 -0
  60. package/template/dist/server.js +15 -0
  61. package/template/dist/tools/review-diff.js +50 -0
  62. package/template/dist/tools/review-post.js +50 -0
  63. package/template/dist/tools/review-purchase.js +61 -0
  64. package/template/dist/tools/review.js +31 -0
  65. package/template/dist/tools/show-albums.js +56 -0
  66. package/template/dist/tools/show-carousel.js +41 -0
  67. package/template/dist/tools/show-map.js +47 -0
  68. package/template/node_modules/.vite/deps/_metadata.json +8 -0
  69. package/template/node_modules/.vite/deps/package.json +3 -0
  70. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps.js +500 -0
  71. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps.js.map +1 -0
  72. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_app-bridge.js +563 -0
  73. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_app-bridge.js.map +1 -0
  74. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_react.js +575 -0
  75. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_react.js.map +1 -0
  76. package/template/node_modules/.vite-mcp/deps/@testing-library_react.js +11363 -0
  77. package/template/node_modules/.vite-mcp/deps/@testing-library_react.js.map +1 -0
  78. package/template/node_modules/.vite-mcp/deps/_metadata.json +130 -0
  79. package/template/node_modules/.vite-mcp/deps/chunk-BoAXSpZd.js +33 -0
  80. package/template/node_modules/.vite-mcp/deps/client-CU1wWud4.js +14385 -0
  81. package/template/node_modules/.vite-mcp/deps/client-CU1wWud4.js.map +1 -0
  82. package/template/node_modules/.vite-mcp/deps/clsx.js +18 -0
  83. package/template/node_modules/.vite-mcp/deps/clsx.js.map +1 -0
  84. package/template/node_modules/.vite-mcp/deps/dist-uWX8WbjY.js +505 -0
  85. package/template/node_modules/.vite-mcp/deps/dist-uWX8WbjY.js.map +1 -0
  86. package/template/node_modules/.vite-mcp/deps/embla-carousel-react.js +1461 -0
  87. package/template/node_modules/.vite-mcp/deps/embla-carousel-react.js.map +1 -0
  88. package/template/node_modules/.vite-mcp/deps/embla-carousel-wheel-gestures.js +536 -0
  89. package/template/node_modules/.vite-mcp/deps/embla-carousel-wheel-gestures.js.map +1 -0
  90. package/template/node_modules/.vite-mcp/deps/magic-string.es-Cklsmr-5.js +1013 -0
  91. package/template/node_modules/.vite-mcp/deps/magic-string.es-Cklsmr-5.js.map +1 -0
  92. package/template/node_modules/.vite-mcp/deps/mapbox-gl.js +46311 -0
  93. package/template/node_modules/.vite-mcp/deps/mapbox-gl.js.map +1 -0
  94. package/template/node_modules/.vite-mcp/deps/package.json +3 -0
  95. package/template/node_modules/.vite-mcp/deps/protocol-CTflwIfG.js +2090 -0
  96. package/template/node_modules/.vite-mcp/deps/protocol-CTflwIfG.js.map +1 -0
  97. package/template/node_modules/.vite-mcp/deps/react-dom.js +186 -0
  98. package/template/node_modules/.vite-mcp/deps/react-dom.js.map +1 -0
  99. package/template/node_modules/.vite-mcp/deps/react-dom_client.js +2 -0
  100. package/template/node_modules/.vite-mcp/deps/react.js +769 -0
  101. package/template/node_modules/.vite-mcp/deps/react.js.map +1 -0
  102. package/template/node_modules/.vite-mcp/deps/react_jsx-dev-runtime.js +205 -0
  103. package/template/node_modules/.vite-mcp/deps/react_jsx-dev-runtime.js.map +1 -0
  104. package/template/node_modules/.vite-mcp/deps/react_jsx-runtime.js +209 -0
  105. package/template/node_modules/.vite-mcp/deps/react_jsx-runtime.js.map +1 -0
  106. package/template/node_modules/.vite-mcp/deps/schemas-NsgmY9QV.js +12157 -0
  107. package/template/node_modules/.vite-mcp/deps/schemas-NsgmY9QV.js.map +1 -0
  108. package/template/node_modules/.vite-mcp/deps/tailwind-merge.js +2025 -0
  109. package/template/node_modules/.vite-mcp/deps/tailwind-merge.js.map +1 -0
  110. package/template/node_modules/.vite-mcp/deps/vitest.js +14021 -0
  111. package/template/node_modules/.vite-mcp/deps/vitest.js.map +1 -0
  112. package/template/node_modules/.vite-mcp/deps/zod.js +624 -0
  113. package/template/node_modules/.vite-mcp/deps/zod.js.map +1 -0
  114. package/template/src/tools/review-diff.test.ts +5 -1
  115. package/template/src/tools/review-diff.ts +1 -1
  116. package/template/src/tools/review-post.test.ts +5 -1
  117. package/template/src/tools/review-post.ts +1 -1
  118. package/template/src/tools/review-purchase.test.ts +5 -1
  119. package/template/src/tools/review-purchase.ts +1 -1
  120. package/template/src/tools/review.test.ts +5 -1
  121. package/template/src/tools/review.ts +1 -1
  122. package/template/src/tools/show-albums.test.ts +5 -1
  123. package/template/src/tools/show-albums.ts +1 -1
  124. package/template/src/tools/show-carousel.test.ts +5 -1
  125. package/template/src/tools/show-carousel.ts +1 -1
  126. package/template/src/tools/show-map.test.ts +5 -1
  127. package/template/src/tools/show-map.ts +1 -1
  128. package/dist/discovery-BxKCIgG5.cjs +0 -332
  129. package/dist/discovery-BxKCIgG5.cjs.map +0 -1
  130. package/dist/discovery-Du4LHrih.js +0 -261
  131. package/dist/discovery-Du4LHrih.js.map +0 -1
  132. package/dist/simulator-C7mkK7Sz.js.map +0 -1
  133. package/dist/simulator-DIVvI69i.cjs.map +0 -1
  134. package/dist/simulator-url-BDGD4vZD.cjs.map +0 -1
  135. package/dist/simulator-url-Bkxj43yT.js.map +0 -1
  136. package/template/.sunpeak/dev.tsx +0 -79
  137. package/template/.sunpeak/resource-loader.html +0 -20
  138. package/template/.sunpeak/resource-loader.tsx +0 -57
  139. package/template/index.html +0 -14
  140. 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
+ ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;', "'": '&#39;' })[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
+ }