sunpeak 0.20.62 → 0.20.64
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 +3 -0
- package/bin/commands/inspect.mjs +8 -6
- package/bin/lib/live/chatgpt-page.mjs +40 -25
- package/bin/lib/live/global-setup.mjs +2 -2
- package/bin/lib/live/host-page.mjs +30 -24
- package/bin/lib/test/test-fixtures.mjs +12 -3
- package/package.json +1 -1
- package/template/README.md +12 -4
- package/template/dist/albums/albums.json +1 -1
- package/template/dist/carousel/carousel.json +1 -1
- package/template/dist/map/map.json +1 -1
- package/template/dist/review/review.json +1 -1
package/README.md
CHANGED
|
@@ -20,6 +20,8 @@ Server-agnostic MCP testing framework and full-stack MCP App framework.
|
|
|
20
20
|
|
|
21
21
|
MCP Apps are cross-platform, meaning sunpeak is a ChatGPT App framework, Claude Connector framework, and more.
|
|
22
22
|
|
|
23
|
+
ChatGPT apps are now submitted and published as plugins. The app remains an MCP-backed app, so sunpeak's architecture and runtime do not change. The plugin is the package used for local installation, review, and public distribution.
|
|
24
|
+
|
|
23
25
|
```bash
|
|
24
26
|
npx sunpeak new
|
|
25
27
|
```
|
|
@@ -149,4 +151,5 @@ Toggle between hosts, themes, display modes, and device types from the sidebar.
|
|
|
149
151
|
- [MCP Overview](https://sunpeak.ai/docs/mcp-apps/mcp/overview) · [Tools](https://sunpeak.ai/docs/mcp-apps/mcp/tools) · [Resources](https://sunpeak.ai/docs/mcp-apps/mcp/resources)
|
|
150
152
|
- [MCP Apps SDK](https://github.com/modelcontextprotocol/ext-apps)
|
|
151
153
|
- [ChatGPT Apps SDK Design Guidelines](https://developers.openai.com/apps-sdk/concepts/design-guidelines)
|
|
154
|
+
- [Build ChatGPT Plugins](https://learn.chatgpt.com/docs/build-plugins) · [Submit ChatGPT Plugins](https://learn.chatgpt.com/docs/submit-plugins)
|
|
152
155
|
- [Troubleshooting](https://sunpeak.ai/docs/app-framework/guides/troubleshooting)
|
package/bin/commands/inspect.mjs
CHANGED
|
@@ -1032,6 +1032,12 @@ function appendInspectorRequestToken(resourceUrl, requestToken) {
|
|
|
1032
1032
|
}
|
|
1033
1033
|
}
|
|
1034
1034
|
|
|
1035
|
+
function isInspectorRequestTokenValid(req, requestToken) {
|
|
1036
|
+
if (!requestToken) return true;
|
|
1037
|
+
const url = new URL(req.url, 'http://localhost');
|
|
1038
|
+
return url.searchParams.get('__sunpeak_token') === requestToken;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1035
1041
|
function addInspectorRequestTokenToSimulations(simulations, requestToken) {
|
|
1036
1042
|
if (!requestToken || !simulations || typeof simulations !== 'object') return simulations;
|
|
1037
1043
|
return Object.fromEntries(
|
|
@@ -2073,12 +2079,7 @@ function sunpeakInspectEndpointsPlugin(getClient, setClient, pluginOpts = {}) {
|
|
|
2073
2079
|
}
|
|
2074
2080
|
|
|
2075
2081
|
function requireInspectorRequestToken(req, res) {
|
|
2076
|
-
if (
|
|
2077
|
-
const fetchSiteHeader = req.headers['sec-fetch-site'];
|
|
2078
|
-
const fetchSite = Array.isArray(fetchSiteHeader) ? fetchSiteHeader[0] : fetchSiteHeader;
|
|
2079
|
-
if (fetchSite !== 'cross-site') return true;
|
|
2080
|
-
const url = new URL(req.url, 'http://localhost');
|
|
2081
|
-
if (url.searchParams.get('__sunpeak_token') === pluginOpts.requestToken) return true;
|
|
2082
|
+
if (isInspectorRequestTokenValid(req, pluginOpts.requestToken)) return true;
|
|
2082
2083
|
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
2083
2084
|
res.end('Forbidden: missing or invalid inspector request token');
|
|
2084
2085
|
return false;
|
|
@@ -3090,6 +3091,7 @@ export const _securityTestExports = {
|
|
|
3090
3091
|
formatSharedAppContextForModel,
|
|
3091
3092
|
addInspectorRequestTokenToSimulations,
|
|
3092
3093
|
appendInspectorRequestToken,
|
|
3094
|
+
isInspectorRequestTokenValid,
|
|
3093
3095
|
normalizeApiKey,
|
|
3094
3096
|
normalizeModelChatMessages,
|
|
3095
3097
|
normalizeModelAppContext,
|
|
@@ -11,7 +11,7 @@ import { HostPage } from './host-page.mjs';
|
|
|
11
11
|
/**
|
|
12
12
|
* All ChatGPT DOM selectors in one place for easy maintenance.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
14
|
+
* Updated: 2026-07-10 for the documented ChatGPT Plugins flow.
|
|
15
15
|
*/
|
|
16
16
|
const SELECTORS = {
|
|
17
17
|
// Chat interface
|
|
@@ -23,8 +23,9 @@ const SELECTORS = {
|
|
|
23
23
|
loggedInIndicator: '[data-testid="accounts-profile-button"]',
|
|
24
24
|
loginPage: 'button:has-text("Log in")',
|
|
25
25
|
|
|
26
|
-
//
|
|
27
|
-
|
|
26
|
+
// Plugin directory navigation
|
|
27
|
+
createdByYou:
|
|
28
|
+
'button:has-text("Created by you"), [role="tab"]:has-text("Created by you"), a:has-text("Created by you")',
|
|
28
29
|
refreshButton: 'button:has-text("Refresh")',
|
|
29
30
|
reconnectButton: 'button:has-text("Reconnect")',
|
|
30
31
|
|
|
@@ -40,40 +41,51 @@ const SELECTORS = {
|
|
|
40
41
|
|
|
41
42
|
const URLS = {
|
|
42
43
|
base: 'https://chatgpt.com',
|
|
43
|
-
|
|
44
|
+
plugins: 'https://chatgpt.com/plugins',
|
|
44
45
|
};
|
|
45
46
|
|
|
47
|
+
const PLUGIN_SETUP_HINT =
|
|
48
|
+
'Enable Developer mode from the bottom-left user menu under Settings > Security and login > Developer mode, then add the app from Plugins > +.';
|
|
49
|
+
|
|
46
50
|
export { SELECTORS as CHATGPT_SELECTORS, URLS as CHATGPT_URLS };
|
|
47
51
|
|
|
48
52
|
export class ChatGPTPage extends HostPage {
|
|
49
|
-
get hostId() {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
get
|
|
53
|
+
get hostId() {
|
|
54
|
+
return 'chatgpt';
|
|
55
|
+
}
|
|
56
|
+
get hostName() {
|
|
57
|
+
return 'ChatGPT';
|
|
58
|
+
}
|
|
59
|
+
get selectors() {
|
|
60
|
+
return SELECTORS;
|
|
61
|
+
}
|
|
62
|
+
get urls() {
|
|
63
|
+
return URLS;
|
|
64
|
+
}
|
|
53
65
|
|
|
54
66
|
/**
|
|
55
|
-
* Refresh the MCP server connection
|
|
56
|
-
*
|
|
67
|
+
* Refresh the MCP server connection from the ChatGPT Plugins directory.
|
|
68
|
+
* Opens Plugins, finds the developer-mode app, clicks Refresh,
|
|
57
69
|
* and waits for the success/error toast.
|
|
58
70
|
*/
|
|
59
71
|
async refreshMcpServer({ tunnelUrl, appName } = {}) {
|
|
60
|
-
await this.page.goto(URLS.
|
|
72
|
+
await this.page.goto(URLS.plugins, { waitUntil: 'domcontentloaded' });
|
|
61
73
|
await this.page.waitForTimeout(3_000);
|
|
62
74
|
|
|
63
75
|
const found = await this._findAndClickRefresh(appName);
|
|
64
76
|
|
|
65
77
|
if (!found) {
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
if (
|
|
69
|
-
await
|
|
78
|
+
const createdByYou = this.page.locator(SELECTORS.createdByYou).first();
|
|
79
|
+
const hasCreatedByYou = await createdByYou.isVisible().catch(() => false);
|
|
80
|
+
if (hasCreatedByYou) {
|
|
81
|
+
await createdByYou.click();
|
|
70
82
|
await this.page.waitForTimeout(2_000);
|
|
71
83
|
const retryFound = await this._findAndClickRefresh(appName);
|
|
72
84
|
if (!retryFound) {
|
|
73
|
-
await this._screenshotAndThrow('refresh-mcp-
|
|
85
|
+
await this._screenshotAndThrow('refresh-mcp-plugin', tunnelUrl, PLUGIN_SETUP_HINT);
|
|
74
86
|
}
|
|
75
87
|
} else {
|
|
76
|
-
await this._screenshotAndThrow('
|
|
88
|
+
await this._screenshotAndThrow('developer-plugin-not-found', tunnelUrl, PLUGIN_SETUP_HINT);
|
|
77
89
|
}
|
|
78
90
|
}
|
|
79
91
|
|
|
@@ -82,7 +94,7 @@ export class ChatGPTPage extends HostPage {
|
|
|
82
94
|
if (hasError) {
|
|
83
95
|
throw new Error(
|
|
84
96
|
`MCP server refresh failed in ChatGPT:\n${errorText.trim()}\n\n` +
|
|
85
|
-
|
|
97
|
+
`Make sure your MCP dev server is running (pnpm dev) and your tunnel is active.`
|
|
86
98
|
);
|
|
87
99
|
}
|
|
88
100
|
|
|
@@ -107,13 +119,18 @@ export class ChatGPTPage extends HostPage {
|
|
|
107
119
|
}
|
|
108
120
|
|
|
109
121
|
// Wait for the outer sandbox iframe
|
|
110
|
-
await this.page
|
|
122
|
+
await this.page
|
|
123
|
+
.locator(SELECTORS.mcpAppOuterIframe)
|
|
124
|
+
.first()
|
|
125
|
+
.waitFor({ state: 'attached', timeout: 30_000 });
|
|
111
126
|
|
|
112
127
|
// Wait for the inner frame to appear inside the sandboxed outer iframe.
|
|
113
128
|
// waitForFunction can't cross the sandbox boundary, so use Playwright's frameLocator instead.
|
|
114
129
|
const outerFrame = this.page.frameLocator(SELECTORS.mcpAppOuterIframe).first();
|
|
115
130
|
await outerFrame
|
|
116
|
-
.locator(
|
|
131
|
+
.locator(
|
|
132
|
+
`iframe[name="${SELECTORS.mcpAppInnerFrameName}"], iframe#${SELECTORS.mcpAppInnerFrameName}`
|
|
133
|
+
)
|
|
117
134
|
.waitFor({ state: 'attached', timeout: 15_000 });
|
|
118
135
|
|
|
119
136
|
const appFrame = this.getAppIframe();
|
|
@@ -182,13 +199,11 @@ export class ChatGPTPage extends HostPage {
|
|
|
182
199
|
return false;
|
|
183
200
|
};
|
|
184
201
|
|
|
185
|
-
if (await tryClickRefresh()) return true;
|
|
186
|
-
|
|
187
202
|
if (appName) {
|
|
188
203
|
const strategies = [
|
|
189
204
|
() => this.page.getByText(appName, { exact: true }).first(),
|
|
190
|
-
() => this.page.
|
|
191
|
-
() => this.page.
|
|
205
|
+
() => this.page.getByRole('link', { name: appName, exact: true }).first(),
|
|
206
|
+
() => this.page.getByRole('button', { name: appName, exact: true }).first(),
|
|
192
207
|
];
|
|
193
208
|
|
|
194
209
|
for (const getLocator of strategies) {
|
|
@@ -205,6 +220,6 @@ export class ChatGPTPage extends HostPage {
|
|
|
205
220
|
}
|
|
206
221
|
}
|
|
207
222
|
|
|
208
|
-
return false;
|
|
223
|
+
return appName ? false : tryClickRefresh();
|
|
209
224
|
}
|
|
210
225
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Runs exactly once before all workers. Two responsibilities:
|
|
5
5
|
* 1. Authenticate — launch a browser, verify login, wait for user if needed.
|
|
6
|
-
* 2. Refresh MCP server
|
|
6
|
+
* 2. Refresh MCP server: open the host's app management UI and click Refresh so
|
|
7
7
|
* all workers start with pre-loaded resources.
|
|
8
8
|
*
|
|
9
9
|
* Auth approach:
|
|
@@ -150,7 +150,7 @@ export default async function globalSetup() {
|
|
|
150
150
|
// Refresh MCP server in the SAME browser session.
|
|
151
151
|
// This is critical — Cloudflare's cf_clearance cookie is HttpOnly and
|
|
152
152
|
// won't be in the saved storageState. By refreshing here, the cookie
|
|
153
|
-
// is still valid for navigating to
|
|
153
|
+
// is still valid for navigating to the plugin directory.
|
|
154
154
|
//
|
|
155
155
|
// This MUST succeed — if the MCP server isn't reachable or the refresh
|
|
156
156
|
// fails, tests will fail with confusing iframe/timeout errors.
|
|
@@ -23,8 +23,7 @@
|
|
|
23
23
|
/**
|
|
24
24
|
* @typedef {Object} HostUrls
|
|
25
25
|
* @property {string} base - Host base URL (e.g., 'https://chatgpt.com')
|
|
26
|
-
* @property {string}
|
|
27
|
-
* @property {string} loginTestId - Test ID for login detection (if using getByTestId)
|
|
26
|
+
* @property {string} [plugins] - Host plugin or app management URL
|
|
28
27
|
*/
|
|
29
28
|
|
|
30
29
|
export class HostPage {
|
|
@@ -80,7 +79,8 @@ export class HostPage {
|
|
|
80
79
|
if (warnings.length > 0) {
|
|
81
80
|
console.warn(
|
|
82
81
|
`\n⚠️ ${this.hostName} DOM may have changed — update selectors in ${this.hostId}-page.mjs:\n` +
|
|
83
|
-
|
|
82
|
+
warnings.join('\n') +
|
|
83
|
+
'\n'
|
|
84
84
|
);
|
|
85
85
|
}
|
|
86
86
|
|
|
@@ -135,14 +135,14 @@ export class HostPage {
|
|
|
135
135
|
// Not logged in. Wait for the user to authenticate in this browser window.
|
|
136
136
|
console.log(
|
|
137
137
|
`\n` +
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
138
|
+
`╔══════════════════════════════════════════════════════════════╗\n` +
|
|
139
|
+
`║ Not logged into ${this.hostName.padEnd(42)}║\n` +
|
|
140
|
+
`║ ║\n` +
|
|
141
|
+
`║ Please log in at: ${this.urls.base.padEnd(39)}║\n` +
|
|
142
|
+
`║ in the browser window that just opened. ║\n` +
|
|
143
|
+
`║ ║\n` +
|
|
144
|
+
`║ Waiting up to 3 minutes... ║\n` +
|
|
145
|
+
`╚══════════════════════════════════════════════════════════════╝\n`
|
|
146
146
|
);
|
|
147
147
|
|
|
148
148
|
// Poll for login — the user may need to pass Cloudflare + enter credentials
|
|
@@ -160,14 +160,14 @@ export class HostPage {
|
|
|
160
160
|
|
|
161
161
|
throw new Error(
|
|
162
162
|
`Login to ${this.hostName} timed out after 3 minutes.\n` +
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
`Please log in at ${this.urls.base} in the browser window and try again.\n` +
|
|
164
|
+
'If the session expired, delete the .auth/ directory and try again.'
|
|
165
165
|
);
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
/**
|
|
169
|
-
* Refresh the MCP server connection in host
|
|
170
|
-
* Subclasses must implement this
|
|
169
|
+
* Refresh the MCP server connection in the host's app management UI.
|
|
170
|
+
* Subclasses must implement this because each host has a different flow.
|
|
171
171
|
*
|
|
172
172
|
* @param {Object} [options]
|
|
173
173
|
* @param {string} [options.tunnelUrl] - Tunnel URL for error messages
|
|
@@ -228,9 +228,10 @@ export class HostPage {
|
|
|
228
228
|
* Capture a debug screenshot and throw with helpful message.
|
|
229
229
|
* @param {string} context - Context label for the screenshot filename
|
|
230
230
|
* @param {string} [tunnelUrl] - Tunnel URL for the error message
|
|
231
|
+
* @param {string} [setupHint] - Host-specific setup steps
|
|
231
232
|
* @protected
|
|
232
233
|
*/
|
|
233
|
-
async _screenshotAndThrow(context, tunnelUrl) {
|
|
234
|
+
async _screenshotAndThrow(context, tunnelUrl, setupHint) {
|
|
234
235
|
const screenshotPath = `/tmp/sunpeak-live-debug-${this.hostId}-${context}.png`;
|
|
235
236
|
try {
|
|
236
237
|
await this.page.screenshot({ path: screenshotPath, fullPage: true });
|
|
@@ -241,29 +242,34 @@ export class HostPage {
|
|
|
241
242
|
|
|
242
243
|
try {
|
|
243
244
|
const buttons = await this.page.locator('button').allTextContents();
|
|
244
|
-
console.error('Visible buttons on page:', buttons.filter(t => t.trim()).join(', '));
|
|
245
|
+
console.error('Visible buttons on page:', buttons.filter((t) => t.trim()).join(', '));
|
|
245
246
|
} catch {
|
|
246
247
|
// Best effort
|
|
247
248
|
}
|
|
248
249
|
|
|
249
250
|
throw new Error(
|
|
250
|
-
`Could not find Refresh/Reconnect
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
251
|
+
`Could not find Refresh/Reconnect in ${this.hostName} app management.\n` +
|
|
252
|
+
`Make sure your MCP server is added to ${this.hostName}` +
|
|
253
|
+
(tunnelUrl ? ` with URL: ${tunnelUrl}/mcp` : '') +
|
|
254
|
+
(setupHint ? `\n${setupHint}` : '') +
|
|
255
|
+
`\n\nDebug screenshot: ${screenshotPath}`
|
|
254
256
|
);
|
|
255
257
|
}
|
|
256
258
|
|
|
257
259
|
/**
|
|
258
260
|
* Wait for a toast/alert banner and check for errors.
|
|
259
|
-
* Many hosts show success/error toasts after
|
|
261
|
+
* Many hosts show success/error toasts after app management actions.
|
|
260
262
|
* @param {Object} [options]
|
|
261
263
|
* @param {string} [options.alertSelector='[role="alert"]'] - Selector for toast elements
|
|
262
264
|
* @param {number} [options.timeout=30000] - Max time to wait
|
|
263
265
|
* @param {number} [options.minTextLength=5] - Minimum text length to consider as a real toast
|
|
264
266
|
* @protected
|
|
265
267
|
*/
|
|
266
|
-
async _waitForToast({
|
|
268
|
+
async _waitForToast({
|
|
269
|
+
alertSelector = '[role="alert"]',
|
|
270
|
+
timeout = 30_000,
|
|
271
|
+
minTextLength = 5,
|
|
272
|
+
} = {}) {
|
|
267
273
|
try {
|
|
268
274
|
await this.page.waitForFunction(
|
|
269
275
|
({ selector, minLen }) => {
|
|
@@ -275,7 +281,7 @@ export class HostPage {
|
|
|
275
281
|
return false;
|
|
276
282
|
},
|
|
277
283
|
{ selector: alertSelector, minLen: minTextLength },
|
|
278
|
-
{ timeout }
|
|
284
|
+
{ timeout }
|
|
279
285
|
);
|
|
280
286
|
} catch {
|
|
281
287
|
console.warn('No toast detected — assuming success.');
|
|
@@ -67,6 +67,14 @@ async function fetchJson(page, path) {
|
|
|
67
67
|
return response.json();
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
async function getInspectorRequestToken(page) {
|
|
71
|
+
const baseURL = page.context()._options?.baseURL || '';
|
|
72
|
+
const response = await page.request.get(`${baseURL}/@id/__x00__virtual:sunpeak-inspect-entry`);
|
|
73
|
+
if (!response.ok()) return null;
|
|
74
|
+
const source = await response.text();
|
|
75
|
+
return source.match(/[?&]__sunpeak_token=([^"'&\\]+)/)?.[1] ?? null;
|
|
76
|
+
}
|
|
77
|
+
|
|
70
78
|
/**
|
|
71
79
|
* Read the tool result from the inspector's <script id="__tool-result"> element.
|
|
72
80
|
*/
|
|
@@ -199,9 +207,10 @@ const test = base.extend({
|
|
|
199
207
|
|
|
200
208
|
async readResource(uri) {
|
|
201
209
|
const baseURL = page.context()._options?.baseURL || '';
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
);
|
|
210
|
+
const params = new URLSearchParams({ uri });
|
|
211
|
+
const requestToken = await getInspectorRequestToken(page);
|
|
212
|
+
if (requestToken) params.set('__sunpeak_token', requestToken);
|
|
213
|
+
const response = await page.request.get(`${baseURL}/__sunpeak/read-resource?${params}`);
|
|
205
214
|
if (!response.ok()) {
|
|
206
215
|
throw new Error(
|
|
207
216
|
`readResource(${uri}) returned ${response.status()}: ${await response.text()}`
|
package/package.json
CHANGED
package/template/README.md
CHANGED
|
@@ -76,7 +76,11 @@ pnpm dev
|
|
|
76
76
|
ngrok http 8000
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
If Developer mode is not already enabled, click your user menu in the bottom-left corner and select `Settings > Security and login > Developer mode`.
|
|
80
|
+
|
|
81
|
+
Then return to the ChatGPT homepage and click `Plugins` in the sidebar. Open your app if it is already there, or select the `+` button and connect the tunnel forwarding URL at the `/mcp` path.
|
|
82
|
+
|
|
83
|
+
ChatGPT creates a developer-mode app backed by your MCP server. It appears under `Plugins`, but its MCP App UI and runtime do not change.
|
|
80
84
|
|
|
81
85
|
Once your app is connected, send the name of the app and a tool, like `/sunpeak show review`, to ChatGPT.
|
|
82
86
|
|
|
@@ -86,8 +90,9 @@ Run automated tests against real ChatGPT with `pnpm test:live`. This opens your
|
|
|
86
90
|
|
|
87
91
|
**One-time setup:**
|
|
88
92
|
|
|
89
|
-
1.
|
|
90
|
-
2.
|
|
93
|
+
1. If Developer mode is not already enabled, click your user menu in the bottom-left corner and select `Settings > Security and login > Developer mode`
|
|
94
|
+
2. Return to the ChatGPT homepage and click `Plugins` in the sidebar
|
|
95
|
+
3. Open your app if it is already there, or select `+` and add your tunnel URL at `/mcp`
|
|
91
96
|
|
|
92
97
|
**Run live tests:**
|
|
93
98
|
|
|
@@ -133,6 +138,8 @@ pnpm start -- --json-logs # Structured JSON logging for production
|
|
|
133
138
|
|
|
134
139
|
The server includes a `/health` endpoint for load balancer probes and monitoring. See the [Deployment Guide](https://sunpeak.ai/docs/app-framework/guides/deployment) for production operations details (reverse proxy, process management, Docker).
|
|
135
140
|
|
|
141
|
+
ChatGPT apps are submitted and published as plugins. Deploy this MCP server first, then create a `With MCP` submission in the [OpenAI Platform plugin portal](https://platform.openai.com/plugins). Submit the MCP server URL directly, not an existing ChatGPT app ID. See the [sunpeak publishing guide](https://sunpeak.ai/docs/app-framework/guides/deployment#publish-to-chatgpt) and OpenAI's [plugin submission guide](https://learn.chatgpt.com/docs/submit-plugins).
|
|
142
|
+
|
|
136
143
|
## Add a new UI (Resource)
|
|
137
144
|
|
|
138
145
|
To add a new UI ([MCP Resource](https://sunpeak.ai/docs/mcp-apps/mcp/resources)), create a new directory under `src/resources/` with the following files:
|
|
@@ -162,7 +169,7 @@ If your app doesn't render in ChatGPT or Claude:
|
|
|
162
169
|
|
|
163
170
|
1. **Check your tunnel** is running and pointing to the correct port
|
|
164
171
|
2. **Restart `pnpm dev`** to clear stale connections
|
|
165
|
-
3. **Refresh or re-add the MCP server**
|
|
172
|
+
3. **Refresh or re-add the MCP server** from the host's app or plugin management page
|
|
166
173
|
4. **Hard refresh** the host page (`Cmd+Shift+R` / `Ctrl+Shift+R`)
|
|
167
174
|
5. **Open a new chat** in the host (cached iframes persist per-conversation)
|
|
168
175
|
|
|
@@ -175,3 +182,4 @@ Full guide: [sunpeak.ai/docs/app-framework/guides/troubleshooting](https://sunpe
|
|
|
175
182
|
- [MCP Overview](https://sunpeak.ai/docs/mcp-apps/mcp/overview) · [Tools](https://sunpeak.ai/docs/mcp-apps/mcp/tools) · [Resources](https://sunpeak.ai/docs/mcp-apps/mcp/resources)
|
|
176
183
|
- [MCP Apps SDK](https://github.com/modelcontextprotocol/ext-apps)
|
|
177
184
|
- [ChatGPT Apps SDK Design Guidelines](https://developers.openai.com/apps-sdk/concepts/design-guidelines)
|
|
185
|
+
- [Build ChatGPT Plugins](https://learn.chatgpt.com/docs/build-plugins) · [Submit ChatGPT Plugins](https://learn.chatgpt.com/docs/submit-plugins)
|