sunpeak 0.18.14 → 0.19.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -134
- package/bin/commands/new.mjs +3 -1
- package/bin/commands/test-init.mjs +305 -0
- package/bin/commands/test.mjs +144 -0
- package/bin/lib/inspect/inspect-config.d.mts +4 -0
- package/bin/lib/inspect/inspect-config.mjs +18 -24
- package/bin/lib/test/base-config.mjs +75 -0
- package/bin/lib/test/matchers.mjs +99 -0
- package/bin/lib/test/test-config.d.mts +66 -0
- package/bin/lib/test/test-config.mjs +125 -0
- package/bin/lib/test/test-fixtures.d.mts +129 -0
- package/bin/lib/test/test-fixtures.mjs +232 -0
- package/bin/sunpeak.js +18 -5
- package/package.json +22 -10
- package/template/README.md +18 -8
- package/template/dist/albums/albums.json +1 -1
- package/template/dist/carousel/carousel.json +1 -1
- package/template/dist/map/map.html +468 -280
- package/template/dist/map/map.json +1 -1
- package/template/dist/review/review.json +1 -1
- package/template/node_modules/.bin/playwright +2 -2
- package/template/node_modules/.bin/vite +2 -2
- package/template/node_modules/.bin/vitest +2 -2
- package/template/node_modules/.vite/deps/_metadata.json +4 -4
- package/template/node_modules/.vite-mcp/deps/_metadata.json +22 -22
- package/template/node_modules/.vite-mcp/deps/mapbox-gl.js +15924 -14588
- package/template/node_modules/.vite-mcp/deps/mapbox-gl.js.map +1 -1
- package/template/node_modules/.vite-mcp/deps/vitest.js +8 -8
- package/template/node_modules/.vite-mcp/deps/vitest.js.map +1 -1
- package/template/package.json +9 -7
- package/template/playwright.config.ts +2 -40
- package/template/test-results/.last-run.json +4 -0
- package/template/tests/e2e/albums.spec.ts +114 -245
- package/template/tests/e2e/carousel.spec.ts +189 -313
- package/template/tests/e2e/map.spec.ts +177 -300
- package/template/tests/e2e/review.spec.ts +232 -423
- package/template/tests/e2e/visual.spec.ts +36 -0
- package/template/tests/e2e/visual.spec.ts-snapshots/albums-dark-chatgpt-linux.png +0 -0
- package/template/tests/e2e/visual.spec.ts-snapshots/albums-dark-claude-linux.png +0 -0
- package/template/tests/e2e/visual.spec.ts-snapshots/albums-fullscreen-chatgpt-linux.png +0 -0
- package/template/tests/e2e/visual.spec.ts-snapshots/albums-fullscreen-claude-linux.png +0 -0
- package/template/tests/e2e/visual.spec.ts-snapshots/albums-light-chatgpt-linux.png +0 -0
- package/template/tests/e2e/visual.spec.ts-snapshots/albums-light-claude-linux.png +0 -0
- package/template/tests/e2e/visual.spec.ts-snapshots/albums-page-light-chatgpt-linux.png +0 -0
- package/template/tests/e2e/visual.spec.ts-snapshots/albums-page-light-claude-linux.png +0 -0
- package/template/tests/live/albums.spec.ts +1 -1
- package/template/tests/live/carousel.spec.ts +1 -1
- package/template/tests/live/map.spec.ts +1 -1
- package/template/tests/live/playwright.config.ts +1 -1
- package/template/tests/live/review.spec.ts +1 -1
- package/template/vitest.config.ts +1 -1
- package/template/tests/e2e/global-setup.ts +0 -10
- package/template/tests/e2e/helpers.ts +0 -13
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP-first Playwright fixtures for testing MCP servers.
|
|
3
|
+
*
|
|
4
|
+
* Provides an `mcp` fixture that abstracts the inspector, double-iframe
|
|
5
|
+
* traversal, URL construction, and host selection. Tests read like MCP
|
|
6
|
+
* operations, not browser automation.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* import { test, expect } from 'sunpeak/test';
|
|
10
|
+
*
|
|
11
|
+
* test('weather tool', async ({ mcp }) => {
|
|
12
|
+
* const result = await mcp.callTool('get-weather', { city: 'SF' });
|
|
13
|
+
* expect(result).not.toBeError();
|
|
14
|
+
* expect(result).toHaveTextContent('temperature');
|
|
15
|
+
*
|
|
16
|
+
* const app = result.app();
|
|
17
|
+
* await expect(app.getByText('San Francisco')).toBeVisible();
|
|
18
|
+
* });
|
|
19
|
+
*/
|
|
20
|
+
import { resolvePlaywrightESM } from '../live/utils.mjs';
|
|
21
|
+
import { registerMatchers } from './matchers.mjs';
|
|
22
|
+
|
|
23
|
+
const projectRoot = process.env.SUNPEAK_PROJECT_ROOT || process.cwd();
|
|
24
|
+
const { test: base, expect } = await resolvePlaywrightESM(projectRoot);
|
|
25
|
+
|
|
26
|
+
// Register MCP-native matchers
|
|
27
|
+
registerMatchers(expect);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Build an inspector URL path with query parameters.
|
|
31
|
+
* Inlined to avoid importing from dist (which pulls in React).
|
|
32
|
+
*/
|
|
33
|
+
function buildInspectorUrl(params) {
|
|
34
|
+
const sp = new URLSearchParams();
|
|
35
|
+
for (const [key, value] of Object.entries(params)) {
|
|
36
|
+
if (value !== undefined && value !== null) {
|
|
37
|
+
sp.set(key, String(value));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Always disable dev overlay in tests
|
|
41
|
+
sp.set('devOverlay', 'false');
|
|
42
|
+
const qs = sp.toString();
|
|
43
|
+
return qs ? `/?${qs}` : '/';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the host ID from the Playwright project name.
|
|
48
|
+
*/
|
|
49
|
+
function resolveHostId(projectName) {
|
|
50
|
+
if (!projectName) return 'chatgpt';
|
|
51
|
+
if (projectName.startsWith('chatgpt')) return 'chatgpt';
|
|
52
|
+
if (projectName.startsWith('claude')) return 'claude';
|
|
53
|
+
return projectName;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create a ToolResult wrapper around the inspector's rendered state.
|
|
58
|
+
*/
|
|
59
|
+
function createToolResult(page, resultData) {
|
|
60
|
+
return {
|
|
61
|
+
content: resultData?.content || [],
|
|
62
|
+
structuredContent: resultData?.structuredContent,
|
|
63
|
+
isError: resultData?.isError || false,
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get a FrameLocator for the rendered resource UI.
|
|
67
|
+
* Handles the double-iframe traversal (outer sandbox proxy + inner app).
|
|
68
|
+
* Returns the locator regardless — Playwright will throw with a clear
|
|
69
|
+
* error if no iframe exists when you interact with it.
|
|
70
|
+
*/
|
|
71
|
+
app() {
|
|
72
|
+
return page.frameLocator('iframe').frameLocator('iframe');
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const test = base.extend({
|
|
78
|
+
mcp: async ({ page }, use, testInfo) => {
|
|
79
|
+
const host = resolveHostId(testInfo.project.name);
|
|
80
|
+
|
|
81
|
+
const fixture = {
|
|
82
|
+
page,
|
|
83
|
+
host,
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Call a tool and get the rendered result.
|
|
87
|
+
*
|
|
88
|
+
* For sunpeak projects: navigates to the matching simulation (simulation
|
|
89
|
+
* fixture data including toolInput is served by sunpeak dev).
|
|
90
|
+
* For external servers: navigates to the matching simulation created by
|
|
91
|
+
* inspectServer from discovered tools.
|
|
92
|
+
*
|
|
93
|
+
* Note: The `input` parameter is accepted for API consistency and future
|
|
94
|
+
* use but is not currently passed to the inspector. Simulation fixture
|
|
95
|
+
* data provides the tool input for rendering.
|
|
96
|
+
*
|
|
97
|
+
* @param {string} name - Tool/simulation name (e.g., 'show-albums')
|
|
98
|
+
* @param {Record<string, unknown>} [_input] - Reserved for future use
|
|
99
|
+
* @param {Object} [options] - Display options
|
|
100
|
+
* @returns {Promise<ToolResult>}
|
|
101
|
+
*/
|
|
102
|
+
async callTool(name, _input, options = {}) {
|
|
103
|
+
const { theme, displayMode, ...rest } = options;
|
|
104
|
+
|
|
105
|
+
const params = {
|
|
106
|
+
simulation: name,
|
|
107
|
+
host,
|
|
108
|
+
...(theme && { theme }),
|
|
109
|
+
...(displayMode && { displayMode }),
|
|
110
|
+
...rest,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
await page.goto(buildInspectorUrl(params));
|
|
114
|
+
|
|
115
|
+
// Wait for the resource iframe to have content
|
|
116
|
+
try {
|
|
117
|
+
const frame = page.frameLocator('iframe').frameLocator('iframe');
|
|
118
|
+
await frame.locator('body').waitFor({ state: 'attached', timeout: 15_000 });
|
|
119
|
+
} catch {
|
|
120
|
+
// Tool may not have a resource (no UI)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return createToolResult(page, {
|
|
124
|
+
content: [],
|
|
125
|
+
structuredContent: undefined,
|
|
126
|
+
isError: false,
|
|
127
|
+
});
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Navigate to a tool with no mock data ("Press Run" state).
|
|
132
|
+
* Use for testing the empty/loading state before a tool is executed.
|
|
133
|
+
*/
|
|
134
|
+
async openTool(name, options = {}) {
|
|
135
|
+
const { theme, ...rest } = options;
|
|
136
|
+
const params = {
|
|
137
|
+
tool: name,
|
|
138
|
+
host,
|
|
139
|
+
...(theme && { theme }),
|
|
140
|
+
...rest,
|
|
141
|
+
};
|
|
142
|
+
await page.goto(buildInspectorUrl(params));
|
|
143
|
+
await page.locator('#root').waitFor({ state: 'attached' });
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Click the Run button and wait for the resource to render.
|
|
148
|
+
* Use after openTool() in Prod Tools mode.
|
|
149
|
+
*/
|
|
150
|
+
async runTool() {
|
|
151
|
+
await page.locator('button:has-text("Run")').click();
|
|
152
|
+
await page.locator('iframe').waitFor({ state: 'attached', timeout: 30_000 });
|
|
153
|
+
return createToolResult(page, {
|
|
154
|
+
content: [],
|
|
155
|
+
structuredContent: undefined,
|
|
156
|
+
isError: false,
|
|
157
|
+
});
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Change the theme via the sidebar toggle.
|
|
162
|
+
*/
|
|
163
|
+
async setTheme(theme) {
|
|
164
|
+
const label = theme === 'light' ? 'Light' : 'Dark';
|
|
165
|
+
const button = page.locator(`button:has-text("${label}")`);
|
|
166
|
+
if (await button.isVisible().catch(() => false)) {
|
|
167
|
+
await button.click();
|
|
168
|
+
// Wait for theme to propagate to the iframe
|
|
169
|
+
await page.waitForTimeout(300);
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Change the display mode via the sidebar buttons.
|
|
175
|
+
*/
|
|
176
|
+
async setDisplayMode(mode) {
|
|
177
|
+
const labels = { inline: 'Inline', pip: 'PiP', fullscreen: 'Full' };
|
|
178
|
+
const label = labels[mode] || mode;
|
|
179
|
+
await page.locator(`button:has-text("${label}")`).click();
|
|
180
|
+
// Wait for display mode transition
|
|
181
|
+
await page.waitForTimeout(500);
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Take a screenshot and compare against a baseline.
|
|
186
|
+
* Only performs the comparison when visual testing is enabled
|
|
187
|
+
* (`sunpeak test --visual`). Silently skips otherwise, so tests
|
|
188
|
+
* that include screenshot() calls still pass during normal runs.
|
|
189
|
+
*
|
|
190
|
+
* Accepts all Playwright toHaveScreenshot() options (threshold,
|
|
191
|
+
* maxDiffPixelRatio, maxDiffPixels, mask, animations, caret,
|
|
192
|
+
* fullPage, clip, scale, stylePath, etc.) and passes them through.
|
|
193
|
+
*
|
|
194
|
+
* @param {string} [name] - Snapshot name (auto-generated from test title if omitted)
|
|
195
|
+
* @param {Object} [options] - Screenshot and comparison options
|
|
196
|
+
* @param {'app' | 'page'} [options.target='app'] - What to screenshot
|
|
197
|
+
* @param {import('@playwright/test').Locator} [options.element] - Specific locator to screenshot
|
|
198
|
+
*/
|
|
199
|
+
async screenshot(name, options = {}) {
|
|
200
|
+
if (process.env.SUNPEAK_VISUAL !== 'true') return;
|
|
201
|
+
|
|
202
|
+
// Support screenshot(options) without a name
|
|
203
|
+
if (typeof name === 'object' && name !== null) {
|
|
204
|
+
options = name;
|
|
205
|
+
name = undefined;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const { target = 'app', element, ...playwrightOptions } = options;
|
|
209
|
+
|
|
210
|
+
let locator;
|
|
211
|
+
if (element) {
|
|
212
|
+
locator = element;
|
|
213
|
+
} else if (target === 'page') {
|
|
214
|
+
locator = page.locator('#root');
|
|
215
|
+
} else {
|
|
216
|
+
locator = page.frameLocator('iframe').frameLocator('iframe').locator('body');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const fullName = name && !name.endsWith('.png') ? `${name}.png` : name;
|
|
220
|
+
const args = fullName
|
|
221
|
+
? [fullName, playwrightOptions]
|
|
222
|
+
: [playwrightOptions];
|
|
223
|
+
|
|
224
|
+
await expect(locator).toHaveScreenshot(...args);
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
await use(fixture);
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
export { test, expect };
|
package/bin/sunpeak.js
CHANGED
|
@@ -37,7 +37,7 @@ function getVersion() {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// Commands that don't require a package.json
|
|
40
|
-
const standaloneCommands = ['new', 'upgrade', 'inspect', 'help', undefined];
|
|
40
|
+
const standaloneCommands = ['new', 'upgrade', 'inspect', 'test', 'help', undefined];
|
|
41
41
|
|
|
42
42
|
if (command && !standaloneCommands.includes(command)) {
|
|
43
43
|
checkPackageJson();
|
|
@@ -79,6 +79,13 @@ function getVersion() {
|
|
|
79
79
|
}
|
|
80
80
|
break;
|
|
81
81
|
|
|
82
|
+
case 'test':
|
|
83
|
+
{
|
|
84
|
+
const { runTest } = await import(join(COMMANDS_DIR, 'test.mjs'));
|
|
85
|
+
await runTest(args);
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
|
|
82
89
|
case 'upgrade':
|
|
83
90
|
{
|
|
84
91
|
const { upgrade } = await import(join(COMMANDS_DIR, 'upgrade.mjs'));
|
|
@@ -100,16 +107,22 @@ function getVersion() {
|
|
|
100
107
|
Install:
|
|
101
108
|
pnpm add -g sunpeak
|
|
102
109
|
|
|
103
|
-
|
|
110
|
+
Testing (works with any MCP server):
|
|
111
|
+
sunpeak inspect Inspect any MCP server in the inspector
|
|
112
|
+
--server, -s <url|cmd> MCP server URL or stdio command (required)
|
|
113
|
+
--simulations <dir> Simulation JSON directory
|
|
114
|
+
sunpeak test Run e2e tests against the inspector
|
|
115
|
+
init Scaffold test infrastructure into a project
|
|
116
|
+
--unit Run unit tests (vitest)
|
|
117
|
+
--live Run live tests against real hosts
|
|
118
|
+
|
|
119
|
+
App framework (for sunpeak projects):
|
|
104
120
|
sunpeak new [name] [resources] Create a new project
|
|
105
121
|
sunpeak dev Start dev server + inspector + MCP endpoint
|
|
106
122
|
--no-begging Suppress GitHub star message
|
|
107
123
|
sunpeak build Build resources + tools for production
|
|
108
124
|
sunpeak start Start production MCP server
|
|
109
125
|
--port, -p Server port (default: 8000, or PORT env)
|
|
110
|
-
sunpeak inspect Inspect any MCP server in the inspector
|
|
111
|
-
--server, -s <url|cmd> MCP server URL or stdio command (required)
|
|
112
|
-
--simulations <dir> Simulation JSON directory
|
|
113
126
|
sunpeak upgrade Upgrade sunpeak to latest version
|
|
114
127
|
sunpeak --version Show version number
|
|
115
128
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sunpeak",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.2",
|
|
4
4
|
"description": "Inspector, testing framework, and app framework for MCP Apps.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -55,24 +55,36 @@
|
|
|
55
55
|
}
|
|
56
56
|
},
|
|
57
57
|
"./test": {
|
|
58
|
+
"import": {
|
|
59
|
+
"types": "./bin/lib/test/test-fixtures.d.mts",
|
|
60
|
+
"default": "./bin/lib/test/test-fixtures.mjs"
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"./test/config": {
|
|
64
|
+
"import": {
|
|
65
|
+
"types": "./bin/lib/test/test-config.d.mts",
|
|
66
|
+
"default": "./bin/lib/test/test-config.mjs"
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"./test/live": {
|
|
58
70
|
"import": {
|
|
59
71
|
"types": "./bin/lib/live/live-fixtures.d.mts",
|
|
60
72
|
"default": "./bin/lib/live/live-fixtures.mjs"
|
|
61
73
|
}
|
|
62
74
|
},
|
|
63
|
-
"./test/config": {
|
|
75
|
+
"./test/live/config": {
|
|
64
76
|
"import": {
|
|
65
77
|
"types": "./bin/lib/live/test-config.d.mts",
|
|
66
78
|
"default": "./bin/lib/live/test-config.mjs"
|
|
67
79
|
}
|
|
68
80
|
},
|
|
69
|
-
"./test/chatgpt": {
|
|
81
|
+
"./test/live/chatgpt": {
|
|
70
82
|
"import": {
|
|
71
83
|
"types": "./bin/lib/live/chatgpt-fixtures.d.mts",
|
|
72
84
|
"default": "./bin/lib/live/chatgpt-fixtures.mjs"
|
|
73
85
|
}
|
|
74
86
|
},
|
|
75
|
-
"./test/chatgpt/config": {
|
|
87
|
+
"./test/live/chatgpt/config": {
|
|
76
88
|
"import": {
|
|
77
89
|
"types": "./bin/lib/live/chatgpt-config.d.mts",
|
|
78
90
|
"default": "./bin/lib/live/chatgpt-config.mjs"
|
|
@@ -122,7 +134,7 @@
|
|
|
122
134
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
123
135
|
},
|
|
124
136
|
"dependencies": {
|
|
125
|
-
"@clack/prompts": "^1.
|
|
137
|
+
"@clack/prompts": "^1.2.0",
|
|
126
138
|
"@modelcontextprotocol/ext-apps": "^1.3.2",
|
|
127
139
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
128
140
|
"@vitejs/plugin-react": "^6.0.1",
|
|
@@ -137,16 +149,16 @@
|
|
|
137
149
|
"@testing-library/jest-dom": "^6.9.1",
|
|
138
150
|
"@testing-library/react": "^16.3.2",
|
|
139
151
|
"@testing-library/user-event": "^14.6.1",
|
|
140
|
-
"@types/node": "^25.5.
|
|
152
|
+
"@types/node": "^25.5.2",
|
|
141
153
|
"@types/react": "^19.2.14",
|
|
142
154
|
"@types/react-dom": "^19.2.3",
|
|
143
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
144
|
-
"@typescript-eslint/parser": "^8.
|
|
155
|
+
"@typescript-eslint/eslint-plugin": "^8.58.0",
|
|
156
|
+
"@typescript-eslint/parser": "^8.58.0",
|
|
145
157
|
"eslint": "^9.39.4",
|
|
146
158
|
"eslint-config-prettier": "^10.1.8",
|
|
147
159
|
"eslint-plugin-react": "^7.37.5",
|
|
148
160
|
"eslint-plugin-react-hooks": "^7.0.1",
|
|
149
|
-
"
|
|
161
|
+
"happy-dom": "^18.0.1",
|
|
150
162
|
"prettier": "^3.8.1",
|
|
151
163
|
"react": "^19.2.4",
|
|
152
164
|
"react-dom": "^19.2.4",
|
|
@@ -155,7 +167,7 @@
|
|
|
155
167
|
"tsx": "^4.21.0",
|
|
156
168
|
"typescript": "^5.9.3",
|
|
157
169
|
"vite-plugin-dts": "^4.5.4",
|
|
158
|
-
"@playwright/test": "^1.
|
|
170
|
+
"@playwright/test": "^1.59.1",
|
|
159
171
|
"vitest": "^4.1.2"
|
|
160
172
|
},
|
|
161
173
|
"repository": {
|
package/template/README.md
CHANGED
|
@@ -14,17 +14,27 @@ That's it! Edit the resource files in [./src/resources/](./src/resources/) to bu
|
|
|
14
14
|
|
|
15
15
|
## Commands
|
|
16
16
|
|
|
17
|
+
**Testing:**
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
sunpeak test # Run unit + e2e tests.
|
|
21
|
+
sunpeak test --unit # Run unit tests only (Vitest).
|
|
22
|
+
sunpeak test --e2e # Run e2e tests only (Playwright).
|
|
23
|
+
sunpeak test --visual # Run e2e tests with visual regression.
|
|
24
|
+
sunpeak test --visual --update # Update visual regression baselines.
|
|
25
|
+
sunpeak test --live # Run live tests against real ChatGPT.
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Development and production:**
|
|
29
|
+
|
|
17
30
|
```bash
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
sunpeak
|
|
22
|
-
sunpeak build # Build resources and compile tools for production.
|
|
23
|
-
sunpeak start # Start the production MCP server.
|
|
24
|
-
sunpeak upgrade # Upgrade sunpeak to latest version.
|
|
31
|
+
sunpeak dev # Start dev server + MCP endpoint.
|
|
32
|
+
sunpeak build # Build resources and compile tools for production.
|
|
33
|
+
sunpeak start # Start the production MCP server.
|
|
34
|
+
sunpeak upgrade # Upgrade sunpeak to latest version.
|
|
25
35
|
```
|
|
26
36
|
|
|
27
|
-
|
|
37
|
+
E2e tests use the `mcp` fixture from `sunpeak/test` to call tools and assert against rendered UI across ChatGPT and Claude hosts. Unit tests use Vitest with happy-dom. You can add additional tooling (linting, formatting, type-checking) as needed for your project.
|
|
28
38
|
|
|
29
39
|
## Project Structure
|
|
30
40
|
|