sunpeak 0.20.29 → 0.20.30

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.
@@ -19,6 +19,8 @@ const { existsSync, readdirSync, readFileSync } = fs;
19
19
  const { join, resolve, dirname, sep } = path;
20
20
  import { fileURLToPath, pathToFileURL } from 'url';
21
21
  import { createServer as createHttpServer } from 'http';
22
+ import { LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/sdk/types.js';
23
+ import { OAuthProtectedResourceMetadataSchema } from '@modelcontextprotocol/sdk/shared/auth.js';
22
24
  import { getPort } from '../lib/get-port.mjs';
23
25
  import { startSandboxServer } from '../lib/sandbox-server.mjs';
24
26
  import { getDevOverlayScript } from '../lib/dev-overlay.mjs';
@@ -176,6 +178,82 @@ function createInMemoryOAuthProvider(redirectUrl, opts = {}) {
176
178
  };
177
179
  }
178
180
 
181
+ /**
182
+ * @param {URL} serverUrl
183
+ * @returns {{ pathMetadataUrl: URL, rootMetadataUrl: URL } | undefined}
184
+ */
185
+ function getMcpResourceMetadataUrls(serverUrl) {
186
+ if (serverUrl.protocol !== 'http:' && serverUrl.protocol !== 'https:') return undefined;
187
+ if (serverUrl.pathname === '/' || serverUrl.pathname === '') return undefined;
188
+
189
+ const pathname = serverUrl.pathname.endsWith('/')
190
+ ? serverUrl.pathname.slice(0, -1)
191
+ : serverUrl.pathname;
192
+
193
+ const pathMetadataUrl = new URL(`/.well-known/oauth-protected-resource${pathname}`, serverUrl);
194
+ pathMetadataUrl.search = serverUrl.search;
195
+
196
+ const rootMetadataUrl = new URL('/.well-known/oauth-protected-resource', serverUrl.origin);
197
+ return { pathMetadataUrl, rootMetadataUrl };
198
+ }
199
+
200
+ /**
201
+ * MCP auth discovery supports both endpoint-path and root protected-resource
202
+ * metadata. When no WWW-Authenticate resource_metadata URL is available, the
203
+ * current MCP authorization draft says clients must try the endpoint path
204
+ * first, then the root well-known URI.
205
+ *
206
+ * The SDK already falls back from endpoint-path to root on 4xx responses. This
207
+ * helper detects the remaining common invalid-endpoint case before OAuth starts:
208
+ * the endpoint-path URL returns 200 but serves a text/html or text/plain landing
209
+ * page instead of protected-resource metadata JSON. In that case, start OAuth
210
+ * directly with the root metadata URL so no partial provider state is carried
211
+ * across a failed SDK auth attempt.
212
+ *
213
+ * @param {string | URL} serverUrl
214
+ * @param {typeof fetch} [fetchFn]
215
+ * @returns {Promise<string | undefined>}
216
+ */
217
+ export async function resolveMcpResourceMetadataUrl(serverUrl, fetchFn = fetch) {
218
+ let parsed;
219
+ try {
220
+ parsed = serverUrl instanceof URL ? serverUrl : new URL(serverUrl);
221
+ } catch {
222
+ return undefined;
223
+ }
224
+
225
+ const urls = getMcpResourceMetadataUrls(parsed);
226
+ if (!urls) return undefined;
227
+
228
+ let response;
229
+ try {
230
+ response = await fetchFn(urls.pathMetadataUrl, {
231
+ headers: { 'MCP-Protocol-Version': LATEST_PROTOCOL_VERSION },
232
+ });
233
+ } catch {
234
+ return undefined;
235
+ }
236
+
237
+ if (!response.ok) {
238
+ await response.body?.cancel();
239
+ return undefined;
240
+ }
241
+
242
+ const contentType = response.headers.get('content-type') ?? '';
243
+ if (!contentType.toLowerCase().includes('application/json')) {
244
+ await response.body?.cancel();
245
+ return urls.rootMetadataUrl.toString();
246
+ }
247
+
248
+ try {
249
+ OAuthProtectedResourceMetadataSchema.parse(await response.json());
250
+ } catch {
251
+ return urls.rootMetadataUrl.toString();
252
+ }
253
+
254
+ return undefined;
255
+ }
256
+
179
257
  /**
180
258
  * Negotiate OAuth with an MCP server and return an authenticated provider.
181
259
  *
@@ -197,10 +275,14 @@ async function negotiateOAuth(serverUrl) {
197
275
 
198
276
  const oauthState = createInMemoryOAuthProvider(callbackUrl);
199
277
  const { provider } = oauthState;
278
+ const resourceMetadataUrl = await resolveMcpResourceMetadataUrl(serverUrl);
200
279
 
201
280
  // First call to auth() — discovers metadata, registers client, and either
202
281
  // returns AUTHORIZED (client_credentials) or REDIRECT (authorization_code).
203
- const result = await auth(provider, { serverUrl: new URL(serverUrl) });
282
+ const result = await auth(provider, {
283
+ serverUrl: new URL(serverUrl),
284
+ ...(resourceMetadataUrl ? { resourceMetadataUrl: new URL(resourceMetadataUrl) } : {}),
285
+ });
204
286
 
205
287
  if (result === 'AUTHORIZED') {
206
288
  return provider;
@@ -1054,6 +1136,7 @@ function sunpeakInspectEndpointsPlugin(getClient, setClient, pluginOpts = {}) {
1054
1136
  // Always create a fresh provider for an explicit Authorize click.
1055
1137
  // This ensures the user's current credentials (or lack thereof) are
1056
1138
  // used, not stale ones from a previous attempt.
1139
+ const resourceMetadataUrl = await resolveMcpResourceMetadataUrl(serverUrl);
1057
1140
  const oauthState = createInMemoryOAuthProvider(callbackUrl, { clientId, clientSecret });
1058
1141
  oauthProviders.set(serverUrl, oauthState);
1059
1142
 
@@ -1062,6 +1145,7 @@ function sunpeakInspectEndpointsPlugin(getClient, setClient, pluginOpts = {}) {
1062
1145
  const result = await auth(oauthState.provider, {
1063
1146
  serverUrl,
1064
1147
  scope,
1148
+ ...(resourceMetadataUrl ? { resourceMetadataUrl: new URL(resourceMetadataUrl) } : {}),
1065
1149
  });
1066
1150
 
1067
1151
  if (result === 'REDIRECT') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sunpeak",
3
- "version": "0.20.29",
3
+ "version": "0.20.30",
4
4
  "description": "App framework, testing framework, and inspector for MCP Apps.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -12,5 +12,5 @@
12
12
  }
13
13
  },
14
14
  "name": "albums",
15
- "uri": "ui://albums-mphmde38"
15
+ "uri": "ui://albums-mphodtcz"
16
16
  }
@@ -12,5 +12,5 @@
12
12
  }
13
13
  },
14
14
  "name": "carousel",
15
- "uri": "ui://carousel-mphmde38"
15
+ "uri": "ui://carousel-mphodtcz"
16
16
  }
@@ -18,5 +18,5 @@
18
18
  }
19
19
  },
20
20
  "name": "map",
21
- "uri": "ui://map-mphmde38"
21
+ "uri": "ui://map-mphodtcz"
22
22
  }
@@ -12,5 +12,5 @@
12
12
  }
13
13
  },
14
14
  "name": "review",
15
- "uri": "ui://review-mphmde38"
15
+ "uri": "ui://review-mphodtcz"
16
16
  }