@mcp-z/client 1.0.4 → 1.0.5

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.
@@ -11,6 +11,7 @@ Object.defineProperty(exports, "probeAuthCapabilities", {
11
11
  return probeAuthCapabilities;
12
12
  }
13
13
  });
14
+ var _urlutilsts = require("../lib/url-utils.js");
14
15
  var _rfc9728discoveryts = require("./rfc9728-discovery.js");
15
16
  function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
16
17
  try {
@@ -222,7 +223,7 @@ function resolveCapabilitiesFromAuthorizationServer(authServerUrl, scopes) {
222
223
  }
223
224
  function probeAuthCapabilities(baseUrl) {
224
225
  return _async_to_generator(function() {
225
- var resourceMetadata, authServerUrl, capabilities, issuer, issuerCapabilities, issuer1, issuerCapabilities1, origin, originCapabilities, _error;
226
+ var normalizedBaseUrl, resourceMetadata, authServerUrl, capabilities, issuer, issuerCapabilities, issuer1, issuerCapabilities1, origin, originCapabilities, _error;
226
227
  return _ts_generator(this, function(_state) {
227
228
  switch(_state.label){
228
229
  case 0:
@@ -232,9 +233,10 @@ function probeAuthCapabilities(baseUrl) {
232
233
  ,
233
234
  11
234
235
  ]);
236
+ normalizedBaseUrl = (0, _urlutilsts.normalizeUrl)(baseUrl);
235
237
  return [
236
238
  4,
237
- (0, _rfc9728discoveryts.discoverProtectedResourceMetadata)(baseUrl)
239
+ (0, _rfc9728discoveryts.discoverProtectedResourceMetadata)(normalizedBaseUrl)
238
240
  ];
239
241
  case 1:
240
242
  resourceMetadata = _state.sent();
@@ -290,7 +292,7 @@ function probeAuthCapabilities(baseUrl) {
290
292
  case 5:
291
293
  return [
292
294
  4,
293
- (0, _rfc9728discoveryts.discoverAuthorizationServerIssuer)(baseUrl)
295
+ (0, _rfc9728discoveryts.discoverAuthorizationServerIssuer)(normalizedBaseUrl)
294
296
  ];
295
297
  case 6:
296
298
  issuer1 = _state.sent();
@@ -312,7 +314,7 @@ function probeAuthCapabilities(baseUrl) {
312
314
  case 8:
313
315
  // Strategy 2: Fall back to direct RFC 8414 discovery at resource origin
314
316
  // This handles same-domain OAuth (traditional setup)
315
- origin = getOrigin(baseUrl);
317
+ origin = getOrigin(normalizedBaseUrl);
316
318
  return [
317
319
  4,
318
320
  resolveCapabilitiesFromAuthorizationServer(origin)
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/Projects/mcp-z/client/src/auth/capability-discovery.ts"],"sourcesContent":["/**\n * OAuth Server Capability Discovery\n * Probes RFC 9728 (Protected Resource) and RFC 8414 (Authorization Server) metadata\n */\n\nimport { discoverAuthorizationServerIssuer, discoverAuthorizationServerMetadata, discoverProtectedResourceMetadata } from './rfc9728-discovery.ts';\nimport type { AuthCapabilities, AuthorizationServerMetadata } from './types.ts';\n\n/**\n * Extract origin (protocol + host) from a URL\n * @param url - Full URL that may include a path\n * @returns Origin (e.g., \"https://example.com\") or original string if invalid URL\n *\n * @example\n * getOrigin('https://example.com/mcp') // → 'https://example.com'\n * getOrigin('http://localhost:9999/api/v1/mcp') // → 'http://localhost:9999'\n */\nfunction getOrigin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n // Invalid URL - return as-is for graceful degradation\n return url;\n }\n}\n\n/**\n * Probe OAuth server capabilities using RFC 9728 → RFC 8414 discovery chain\n * Returns capabilities including DCR support detection\n *\n * Discovery Strategy:\n * 1. Try RFC 9728 Protected Resource Metadata (supports cross-domain OAuth)\n * 2. If found, use first authorization_server to discover RFC 8414 Authorization Server Metadata\n * 3. Fall back to direct RFC 8414 discovery at resource origin\n *\n * @param baseUrl - Base URL of the protected resource (e.g., https://ai.todoist.net/mcp)\n * @returns AuthCapabilities object with discovered endpoints and features\n *\n * @example\n * // Todoist case: MCP at ai.todoist.net/mcp, OAuth at todoist.com\n * const caps = await probeAuthCapabilities('https://ai.todoist.net/mcp');\n * if (caps.supportsDcr) {\n * console.log('Registration endpoint:', caps.registrationEndpoint);\n * }\n */\nfunction buildCapabilities(metadata: AuthorizationServerMetadata, scopes?: string[]): AuthCapabilities {\n const supportsDcr = !!metadata.registration_endpoint;\n const capabilities: AuthCapabilities = { supportsDcr };\n\n if (metadata.registration_endpoint) {\n capabilities.registrationEndpoint = metadata.registration_endpoint;\n }\n if (metadata.authorization_endpoint) {\n capabilities.authorizationEndpoint = metadata.authorization_endpoint;\n }\n if (metadata.token_endpoint) capabilities.tokenEndpoint = metadata.token_endpoint;\n if (metadata.introspection_endpoint) {\n capabilities.introspectionEndpoint = metadata.introspection_endpoint;\n }\n\n if (scopes && scopes.length > 0) {\n capabilities.scopes = scopes;\n } else if (metadata.scopes_supported) {\n capabilities.scopes = metadata.scopes_supported;\n }\n\n return capabilities;\n}\n\nasync function resolveCapabilitiesFromAuthorizationServer(authServerUrl: string, scopes?: string[]): Promise<AuthCapabilities | null> {\n const metadata = await discoverAuthorizationServerMetadata(authServerUrl);\n if (!metadata) return null;\n return buildCapabilities(metadata, scopes);\n}\n\nexport async function probeAuthCapabilities(baseUrl: string): Promise<AuthCapabilities> {\n try {\n // Strategy 1: Try RFC 9728 Protected Resource Metadata discovery\n // This handles cross-domain OAuth (e.g., Todoist: ai.todoist.net/mcp → todoist.com)\n const resourceMetadata = await discoverProtectedResourceMetadata(baseUrl);\n\n if (resourceMetadata && resourceMetadata.authorization_servers.length > 0) {\n // Found protected resource metadata with authorization servers\n // Discover the authorization server's metadata (RFC 8414)\n const authServerUrl = resourceMetadata.authorization_servers[0];\n if (!authServerUrl) {\n // Array has length > 0 but first element is undefined/null - skip this path\n return { supportsDcr: false };\n }\n const capabilities = await resolveCapabilitiesFromAuthorizationServer(authServerUrl, resourceMetadata.scopes_supported);\n if (capabilities) {\n return capabilities;\n }\n\n const issuer = await discoverAuthorizationServerIssuer(baseUrl);\n if (issuer) {\n const issuerCapabilities = await resolveCapabilitiesFromAuthorizationServer(issuer, resourceMetadata.scopes_supported);\n if (issuerCapabilities) return issuerCapabilities;\n }\n }\n\n const issuer = await discoverAuthorizationServerIssuer(baseUrl);\n if (issuer) {\n const issuerCapabilities = await resolveCapabilitiesFromAuthorizationServer(issuer);\n if (issuerCapabilities) return issuerCapabilities;\n }\n\n // Strategy 2: Fall back to direct RFC 8414 discovery at resource origin\n // This handles same-domain OAuth (traditional setup)\n const origin = getOrigin(baseUrl);\n const originCapabilities = await resolveCapabilitiesFromAuthorizationServer(origin);\n if (originCapabilities) return originCapabilities;\n\n // No OAuth metadata found\n return { supportsDcr: false };\n } catch (_error) {\n // Network error, invalid JSON, or other fetch failure\n // Gracefully degrade - assume no DCR support\n return { supportsDcr: false };\n }\n}\n"],"names":["probeAuthCapabilities","getOrigin","url","URL","origin","buildCapabilities","metadata","scopes","supportsDcr","registration_endpoint","capabilities","registrationEndpoint","authorization_endpoint","authorizationEndpoint","token_endpoint","tokenEndpoint","introspection_endpoint","introspectionEndpoint","length","scopes_supported","resolveCapabilitiesFromAuthorizationServer","authServerUrl","discoverAuthorizationServerMetadata","baseUrl","resourceMetadata","issuer","issuerCapabilities","originCapabilities","_error","discoverProtectedResourceMetadata","authorization_servers","discoverAuthorizationServerIssuer"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/Projects/mcp-z/client/src/auth/capability-discovery.ts"],"sourcesContent":["/**\n * OAuth Server Capability Discovery\n * Probes RFC 9728 (Protected Resource) and RFC 8414 (Authorization Server) metadata\n */\n\nimport { normalizeUrl } from '../lib/url-utils.ts';\nimport { discoverAuthorizationServerIssuer, discoverAuthorizationServerMetadata, discoverProtectedResourceMetadata } from './rfc9728-discovery.ts';\nimport type { AuthCapabilities, AuthorizationServerMetadata } from './types.ts';\n\n/**\n * Extract origin (protocol + host) from a URL\n * @param url - Full URL that may include a path\n * @returns Origin (e.g., \"https://example.com\") or original string if invalid URL\n *\n * @example\n * getOrigin('https://example.com/mcp') // → 'https://example.com'\n * getOrigin('http://localhost:9999/api/v1/mcp') // → 'http://localhost:9999'\n */\nfunction getOrigin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n // Invalid URL - return as-is for graceful degradation\n return url;\n }\n}\n\n/**\n * Probe OAuth server capabilities using RFC 9728 → RFC 8414 discovery chain\n * Returns capabilities including DCR support detection\n *\n * Discovery Strategy:\n * 1. Try RFC 9728 Protected Resource Metadata (supports cross-domain OAuth)\n * 2. If found, use first authorization_server to discover RFC 8414 Authorization Server Metadata\n * 3. Fall back to direct RFC 8414 discovery at resource origin\n *\n * @param baseUrl - Base URL of the protected resource (e.g., https://ai.todoist.net/mcp)\n * @returns AuthCapabilities object with discovered endpoints and features\n *\n * @example\n * // Todoist case: MCP at ai.todoist.net/mcp, OAuth at todoist.com\n * const caps = await probeAuthCapabilities('https://ai.todoist.net/mcp');\n * if (caps.supportsDcr) {\n * console.log('Registration endpoint:', caps.registrationEndpoint);\n * }\n */\nfunction buildCapabilities(metadata: AuthorizationServerMetadata, scopes?: string[]): AuthCapabilities {\n const supportsDcr = !!metadata.registration_endpoint;\n const capabilities: AuthCapabilities = { supportsDcr };\n\n if (metadata.registration_endpoint) {\n capabilities.registrationEndpoint = metadata.registration_endpoint;\n }\n if (metadata.authorization_endpoint) {\n capabilities.authorizationEndpoint = metadata.authorization_endpoint;\n }\n if (metadata.token_endpoint) capabilities.tokenEndpoint = metadata.token_endpoint;\n if (metadata.introspection_endpoint) {\n capabilities.introspectionEndpoint = metadata.introspection_endpoint;\n }\n\n if (scopes && scopes.length > 0) {\n capabilities.scopes = scopes;\n } else if (metadata.scopes_supported) {\n capabilities.scopes = metadata.scopes_supported;\n }\n\n return capabilities;\n}\n\nasync function resolveCapabilitiesFromAuthorizationServer(authServerUrl: string, scopes?: string[]): Promise<AuthCapabilities | null> {\n const metadata = await discoverAuthorizationServerMetadata(authServerUrl);\n if (!metadata) return null;\n return buildCapabilities(metadata, scopes);\n}\n\nexport async function probeAuthCapabilities(baseUrl: string): Promise<AuthCapabilities> {\n try {\n const normalizedBaseUrl = normalizeUrl(baseUrl);\n // Strategy 1: Try RFC 9728 Protected Resource Metadata discovery\n // This handles cross-domain OAuth (e.g., Todoist: ai.todoist.net/mcp → todoist.com)\n const resourceMetadata = await discoverProtectedResourceMetadata(normalizedBaseUrl);\n\n if (resourceMetadata && resourceMetadata.authorization_servers.length > 0) {\n // Found protected resource metadata with authorization servers\n // Discover the authorization server's metadata (RFC 8414)\n const authServerUrl = resourceMetadata.authorization_servers[0];\n if (!authServerUrl) {\n // Array has length > 0 but first element is undefined/null - skip this path\n return { supportsDcr: false };\n }\n const capabilities = await resolveCapabilitiesFromAuthorizationServer(authServerUrl, resourceMetadata.scopes_supported);\n if (capabilities) {\n return capabilities;\n }\n\n const issuer = await discoverAuthorizationServerIssuer(baseUrl);\n if (issuer) {\n const issuerCapabilities = await resolveCapabilitiesFromAuthorizationServer(issuer, resourceMetadata.scopes_supported);\n if (issuerCapabilities) return issuerCapabilities;\n }\n }\n\n const issuer = await discoverAuthorizationServerIssuer(normalizedBaseUrl);\n if (issuer) {\n const issuerCapabilities = await resolveCapabilitiesFromAuthorizationServer(issuer);\n if (issuerCapabilities) return issuerCapabilities;\n }\n\n // Strategy 2: Fall back to direct RFC 8414 discovery at resource origin\n // This handles same-domain OAuth (traditional setup)\n const origin = getOrigin(normalizedBaseUrl);\n const originCapabilities = await resolveCapabilitiesFromAuthorizationServer(origin);\n if (originCapabilities) return originCapabilities;\n\n // No OAuth metadata found\n return { supportsDcr: false };\n } catch (_error) {\n // Network error, invalid JSON, or other fetch failure\n // Gracefully degrade - assume no DCR support\n return { supportsDcr: false };\n }\n}\n"],"names":["probeAuthCapabilities","getOrigin","url","URL","origin","buildCapabilities","metadata","scopes","supportsDcr","registration_endpoint","capabilities","registrationEndpoint","authorization_endpoint","authorizationEndpoint","token_endpoint","tokenEndpoint","introspection_endpoint","introspectionEndpoint","length","scopes_supported","resolveCapabilitiesFromAuthorizationServer","authServerUrl","discoverAuthorizationServerMetadata","baseUrl","normalizedBaseUrl","resourceMetadata","issuer","issuerCapabilities","originCapabilities","_error","normalizeUrl","discoverProtectedResourceMetadata","authorization_servers","discoverAuthorizationServerIssuer"],"mappings":"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"}
@@ -3,6 +3,9 @@
3
3
  * Probes .well-known/oauth-protected-resource endpoint
4
4
  */
5
5
  import type { AuthorizationServerMetadata, ProtectedResourceMetadata } from './types.js';
6
+ /**
7
+ * Normalize a resource URL by stripping query/hash and trailing slashes.
8
+ */
6
9
  /**
7
10
  * Discover OAuth 2.0 Protected Resource Metadata (RFC 9728)
8
11
  * Probes .well-known/oauth-protected-resource endpoint
@@ -3,6 +3,9 @@
3
3
  * Probes .well-known/oauth-protected-resource endpoint
4
4
  */
5
5
  import type { AuthorizationServerMetadata, ProtectedResourceMetadata } from './types.js';
6
+ /**
7
+ * Normalize a resource URL by stripping query/hash and trailing slashes.
8
+ */
6
9
  /**
7
10
  * Discover OAuth 2.0 Protected Resource Metadata (RFC 9728)
8
11
  * Probes .well-known/oauth-protected-resource endpoint
@@ -22,6 +22,7 @@ _export(exports, {
22
22
  return discoverProtectedResourceMetadata;
23
23
  }
24
24
  });
25
+ var _urlutilsts = require("../lib/url-utils.js");
25
26
  function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
26
27
  try {
27
28
  var info = gen[key](arg);
@@ -181,19 +182,20 @@ function _ts_generator(thisArg, body) {
181
182
  }
182
183
  function discoverProtectedResourceMetadata(resourceUrl) {
183
184
  return _async_to_generator(function() {
184
- var headerMetadata, origin, path, rootUrl, response, metadata, rootMetadata, subPathUrl, subPathResponse, unused, unused1, subPathUrl1, response1, unused2, _error;
185
+ var normalizedResourceUrl, headerMetadata, localWellKnownUrl, response, unused, origin, path, rootUrl, response1, metadata, rootMetadata, subPathUrl, subPathResponse, unused1, unused2, subPathUrl1, response2, unused3, _error;
185
186
  return _ts_generator(this, function(_state) {
186
187
  switch(_state.label){
187
188
  case 0:
188
189
  _state.trys.push([
189
190
  0,
190
- 20,
191
+ 26,
191
192
  ,
192
- 21
193
+ 27
193
194
  ]);
195
+ normalizedResourceUrl = (0, _urlutilsts.normalizeUrl)(resourceUrl);
194
196
  return [
195
197
  4,
196
- discoverProtectedResourceMetadataFromHeader(resourceUrl)
198
+ discoverProtectedResourceMetadataFromHeader(normalizedResourceUrl)
197
199
  ];
198
200
  case 1:
199
201
  headerMetadata = _state.sent();
@@ -201,21 +203,19 @@ function discoverProtectedResourceMetadata(resourceUrl) {
201
203
  2,
202
204
  headerMetadata
203
205
  ];
204
- origin = getOrigin(resourceUrl);
205
- path = getPath(resourceUrl);
206
- // Strategy 1: Try root location (REQUIRED by RFC 9728)
207
- rootUrl = "".concat(origin, "/.well-known/oauth-protected-resource");
206
+ // Strategy 0: Try path-local well-known (supports path-prefixed deployments like /outlook)
207
+ localWellKnownUrl = (0, _urlutilsts.joinWellKnown)(normalizedResourceUrl, '/.well-known/oauth-protected-resource');
208
208
  _state.label = 2;
209
209
  case 2:
210
210
  _state.trys.push([
211
211
  2,
212
- 12,
212
+ 6,
213
213
  ,
214
- 13
214
+ 7
215
215
  ]);
216
216
  return [
217
217
  4,
218
- fetch(rootUrl, {
218
+ fetch(localWellKnownUrl, {
219
219
  method: 'GET',
220
220
  headers: {
221
221
  Accept: 'application/json',
@@ -227,16 +227,65 @@ function discoverProtectedResourceMetadata(resourceUrl) {
227
227
  response = _state.sent();
228
228
  if (!response.ok) return [
229
229
  3,
230
- 11
230
+ 5
231
231
  ];
232
232
  return [
233
233
  4,
234
234
  response.json()
235
235
  ];
236
236
  case 4:
237
+ return [
238
+ 2,
239
+ _state.sent()
240
+ ];
241
+ case 5:
242
+ return [
243
+ 3,
244
+ 7
245
+ ];
246
+ case 6:
247
+ unused = _state.sent();
248
+ return [
249
+ 3,
250
+ 7
251
+ ];
252
+ case 7:
253
+ origin = getOrigin(normalizedResourceUrl);
254
+ path = getPath(normalizedResourceUrl);
255
+ // Strategy 1: Try root location (REQUIRED by RFC 9728)
256
+ rootUrl = "".concat(origin, "/.well-known/oauth-protected-resource");
257
+ _state.label = 8;
258
+ case 8:
259
+ _state.trys.push([
260
+ 8,
261
+ 18,
262
+ ,
263
+ 19
264
+ ]);
265
+ return [
266
+ 4,
267
+ fetch(rootUrl, {
268
+ method: 'GET',
269
+ headers: {
270
+ Accept: 'application/json',
271
+ Connection: 'close'
272
+ }
273
+ })
274
+ ];
275
+ case 9:
276
+ response1 = _state.sent();
277
+ if (!response1.ok) return [
278
+ 3,
279
+ 17
280
+ ];
281
+ return [
282
+ 4,
283
+ response1.json()
284
+ ];
285
+ case 10:
237
286
  metadata = _state.sent();
238
287
  // Check if the discovered resource matches what we're looking for
239
- if (metadata.resource === resourceUrl) {
288
+ if (metadata.resource === normalizedResourceUrl) {
240
289
  return [
241
290
  2,
242
291
  metadata
@@ -250,22 +299,22 @@ function discoverProtectedResourceMetadata(resourceUrl) {
250
299
  metadata
251
300
  ];
252
301
  }
253
- if (!resourceUrl.startsWith(metadata.resource)) return [
302
+ if (!normalizedResourceUrl.startsWith(metadata.resource)) return [
254
303
  3,
255
- 11
304
+ 17
256
305
  ];
257
306
  // Still try sub-path location to see if there's more specific metadata
258
307
  // But save root metadata as fallback
259
308
  rootMetadata = metadata;
260
309
  // Try sub-path location for more specific metadata
261
310
  subPathUrl = "".concat(origin, "/.well-known/oauth-protected-resource").concat(path);
262
- _state.label = 5;
263
- case 5:
311
+ _state.label = 11;
312
+ case 11:
264
313
  _state.trys.push([
265
- 5,
266
- 9,
314
+ 11,
315
+ 15,
267
316
  ,
268
- 10
317
+ 16
269
318
  ]);
270
319
  return [
271
320
  4,
@@ -277,62 +326,62 @@ function discoverProtectedResourceMetadata(resourceUrl) {
277
326
  }
278
327
  })
279
328
  ];
280
- case 6:
329
+ case 12:
281
330
  subPathResponse = _state.sent();
282
331
  if (!subPathResponse.ok) return [
283
332
  3,
284
- 8
333
+ 14
285
334
  ];
286
335
  return [
287
336
  4,
288
337
  subPathResponse.json()
289
338
  ];
290
- case 7:
339
+ case 13:
291
340
  return [
292
341
  2,
293
342
  _state.sent()
294
343
  ];
295
- case 8:
344
+ case 14:
296
345
  return [
297
346
  3,
298
- 10
347
+ 16
299
348
  ];
300
- case 9:
301
- unused = _state.sent();
349
+ case 15:
350
+ unused1 = _state.sent();
302
351
  return [
303
352
  3,
304
- 10
353
+ 16
305
354
  ];
306
- case 10:
355
+ case 16:
307
356
  // Return root metadata as it applies to this resource
308
357
  return [
309
358
  2,
310
359
  rootMetadata
311
360
  ];
312
- case 11:
361
+ case 17:
313
362
  return [
314
363
  3,
315
- 13
364
+ 19
316
365
  ];
317
- case 12:
318
- unused1 = _state.sent();
366
+ case 18:
367
+ unused2 = _state.sent();
319
368
  return [
320
369
  3,
321
- 13
370
+ 19
322
371
  ];
323
- case 13:
372
+ case 19:
324
373
  if (!path) return [
325
374
  3,
326
- 19
375
+ 25
327
376
  ];
328
377
  subPathUrl1 = "".concat(origin, "/.well-known/oauth-protected-resource").concat(path);
329
- _state.label = 14;
330
- case 14:
378
+ _state.label = 20;
379
+ case 20:
331
380
  _state.trys.push([
332
- 14,
333
- 18,
381
+ 20,
382
+ 24,
334
383
  ,
335
- 19
384
+ 25
336
385
  ]);
337
386
  return [
338
387
  4,
@@ -344,46 +393,46 @@ function discoverProtectedResourceMetadata(resourceUrl) {
344
393
  }
345
394
  })
346
395
  ];
347
- case 15:
348
- response1 = _state.sent();
349
- if (!response1.ok) return [
396
+ case 21:
397
+ response2 = _state.sent();
398
+ if (!response2.ok) return [
350
399
  3,
351
- 17
400
+ 23
352
401
  ];
353
402
  return [
354
403
  4,
355
- response1.json()
404
+ response2.json()
356
405
  ];
357
- case 16:
406
+ case 22:
358
407
  return [
359
408
  2,
360
409
  _state.sent()
361
410
  ];
362
- case 17:
411
+ case 23:
363
412
  return [
364
413
  3,
365
- 19
414
+ 25
366
415
  ];
367
- case 18:
368
- unused2 = _state.sent();
416
+ case 24:
417
+ unused3 = _state.sent();
369
418
  return [
370
419
  3,
371
- 19
420
+ 25
372
421
  ];
373
- case 19:
422
+ case 25:
374
423
  // Neither location found or resource didn't match
375
424
  return [
376
425
  2,
377
426
  null
378
427
  ];
379
- case 20:
428
+ case 26:
380
429
  _error = _state.sent();
381
430
  // Network error, invalid URL, or other failure
382
431
  return [
383
432
  2,
384
433
  null
385
434
  ];
386
- case 21:
435
+ case 27:
387
436
  return [
388
437
  2
389
438
  ];
@@ -490,17 +539,45 @@ function discoverProtectedResourceMetadataFromHeader(resourceUrl) {
490
539
  }
491
540
  function discoverAuthorizationServerMetadata(authServerUrl) {
492
541
  return _async_to_generator(function() {
493
- var origin, wellKnownUrl, response, _error;
542
+ var normalizedAuthServerUrl, localWellKnownUrl, localResponse, origin, wellKnownUrl, response, _error;
494
543
  return _ts_generator(this, function(_state) {
495
544
  switch(_state.label){
496
545
  case 0:
497
546
  _state.trys.push([
498
547
  0,
499
- 3,
548
+ 6,
500
549
  ,
501
- 4
550
+ 7
502
551
  ]);
503
- origin = getOrigin(authServerUrl);
552
+ normalizedAuthServerUrl = (0, _urlutilsts.normalizeUrl)(authServerUrl);
553
+ localWellKnownUrl = (0, _urlutilsts.joinWellKnown)(normalizedAuthServerUrl, '/.well-known/oauth-authorization-server');
554
+ return [
555
+ 4,
556
+ fetch(localWellKnownUrl, {
557
+ method: 'GET',
558
+ headers: {
559
+ Accept: 'application/json',
560
+ Connection: 'close'
561
+ }
562
+ })
563
+ ];
564
+ case 1:
565
+ localResponse = _state.sent();
566
+ if (!localResponse.ok) return [
567
+ 3,
568
+ 3
569
+ ];
570
+ return [
571
+ 4,
572
+ localResponse.json()
573
+ ];
574
+ case 2:
575
+ return [
576
+ 2,
577
+ _state.sent()
578
+ ];
579
+ case 3:
580
+ origin = getOrigin(normalizedAuthServerUrl);
504
581
  wellKnownUrl = "".concat(origin, "/.well-known/oauth-authorization-server");
505
582
  return [
506
583
  4,
@@ -512,7 +589,7 @@ function discoverAuthorizationServerMetadata(authServerUrl) {
512
589
  }
513
590
  })
514
591
  ];
515
- case 1:
592
+ case 4:
516
593
  response = _state.sent();
517
594
  if (!response.ok) {
518
595
  return [
@@ -524,18 +601,18 @@ function discoverAuthorizationServerMetadata(authServerUrl) {
524
601
  4,
525
602
  response.json()
526
603
  ];
527
- case 2:
604
+ case 5:
528
605
  return [
529
606
  2,
530
607
  _state.sent()
531
608
  ];
532
- case 3:
609
+ case 6:
533
610
  _error = _state.sent();
534
611
  return [
535
612
  2,
536
613
  null
537
614
  ];
538
- case 4:
615
+ case 7:
539
616
  return [
540
617
  2
541
618
  ];
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/Projects/mcp-z/client/src/auth/rfc9728-discovery.ts"],"sourcesContent":["/**\n * RFC 9728 Protected Resource Metadata Discovery\n * Probes .well-known/oauth-protected-resource endpoint\n */\n\nimport type { AuthorizationServerMetadata, ProtectedResourceMetadata } from './types.ts';\n\n/**\n * Extract origin (protocol + host) from a URL\n * @param url - Full URL that may include a path\n * @returns Origin (e.g., \"https://example.com\") or original string if invalid URL\n *\n * @example\n * getOrigin('https://example.com/mcp') // → 'https://example.com'\n * getOrigin('http://localhost:9999/api/v1/mcp') // → 'http://localhost:9999'\n */\nfunction getOrigin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n // Invalid URL - return as-is for graceful degradation\n return url;\n }\n}\n\n/**\n * Extract path from a URL (without origin)\n * @param url - Full URL\n * @returns Path component (e.g., \"/mcp\", \"/api/v1/mcp\") or empty string if no path\n */\nfunction getPath(url: string): string {\n try {\n const parsed = new URL(url);\n // pathname includes leading slash, e.g., \"/mcp\"\n return parsed.pathname === '/' ? '' : parsed.pathname;\n } catch {\n return '';\n }\n}\n\n/**\n * Discover OAuth 2.0 Protected Resource Metadata (RFC 9728)\n * Probes .well-known/oauth-protected-resource endpoint\n *\n * Discovery Strategy:\n * 1. Try origin root: {origin}/.well-known/oauth-protected-resource\n * 2. If 404, try sub-path: {origin}/.well-known/oauth-protected-resource{path}\n *\n * @param resourceUrl - URL of the protected resource (e.g., https://ai.todoist.net/mcp)\n * @returns ProtectedResourceMetadata if discovered, null otherwise\n *\n * @example\n * // Todoist case: MCP at ai.todoist.net/mcp, OAuth at todoist.com\n * const metadata = await discoverProtectedResourceMetadata('https://ai.todoist.net/mcp');\n * // Returns: { resource: \"https://ai.todoist.net/mcp\", authorization_servers: [\"https://todoist.com\"] }\n */\nexport async function discoverProtectedResourceMetadata(resourceUrl: string): Promise<ProtectedResourceMetadata | null> {\n try {\n const headerMetadata = await discoverProtectedResourceMetadataFromHeader(resourceUrl);\n if (headerMetadata) return headerMetadata;\n\n const origin = getOrigin(resourceUrl);\n const path = getPath(resourceUrl);\n\n // Strategy 1: Try root location (REQUIRED by RFC 9728)\n const rootUrl = `${origin}/.well-known/oauth-protected-resource`;\n\n try {\n const response = await fetch(rootUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (response.ok) {\n const metadata = (await response.json()) as ProtectedResourceMetadata;\n // Check if the discovered resource matches what we're looking for\n if (metadata.resource === resourceUrl) {\n return metadata;\n }\n // If there's no path component, return root metadata\n // (e.g., looking for http://example.com and found it)\n if (!path) {\n return metadata;\n }\n // If requested URL starts with metadata.resource, the root metadata applies to sub-paths\n // (e.g., looking for http://example.com/api/v1/mcp, found http://example.com)\n if (resourceUrl.startsWith(metadata.resource)) {\n // Still try sub-path location to see if there's more specific metadata\n // But save root metadata as fallback\n const rootMetadata = metadata;\n\n // Try sub-path location for more specific metadata\n const subPathUrl = `${origin}/.well-known/oauth-protected-resource${path}`;\n try {\n const subPathResponse = await fetch(subPathUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n if (subPathResponse.ok) {\n return (await subPathResponse.json()) as ProtectedResourceMetadata;\n }\n } catch {\n // Sub-path failed, use root metadata\n }\n\n // Return root metadata as it applies to this resource\n return rootMetadata;\n }\n // Otherwise, try sub-path location before giving up\n }\n } catch {\n // Continue to sub-path location\n }\n\n // Strategy 2: Try sub-path location (MCP spec extension)\n // Only try if there's a path component\n if (path) {\n const subPathUrl = `${origin}/.well-known/oauth-protected-resource${path}`;\n\n try {\n const response = await fetch(subPathUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (response.ok) {\n return (await response.json()) as ProtectedResourceMetadata;\n }\n } catch {\n // Fall through to return null\n }\n }\n\n // Neither location found or resource didn't match\n return null;\n } catch (_error) {\n // Network error, invalid URL, or other failure\n return null;\n }\n}\n\nasync function discoverProtectedResourceMetadataFromHeader(resourceUrl: string): Promise<ProtectedResourceMetadata | null> {\n try {\n const response = await fetch(resourceUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n let header = response.headers.get('www-authenticate');\n if (!header) {\n const postResponse = await fetch(resourceUrl, {\n method: 'POST',\n headers: { Accept: 'application/json', Connection: 'close', 'Content-Type': 'application/json' },\n body: '{}',\n });\n header = postResponse.headers.get('www-authenticate');\n }\n\n if (!header) return null;\n\n const match = header.match(/resource_metadata=\"([^\"]+)\"/i);\n if (!match || !match[1]) return null;\n\n const metadataUrl = match[1];\n const metadataResponse = await fetch(metadataUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (!metadataResponse.ok) {\n return null;\n }\n\n return (await metadataResponse.json()) as ProtectedResourceMetadata;\n } catch (_error) {\n return null;\n }\n}\n\n/**\n * Discover OAuth 2.0 Authorization Server Metadata (RFC 8414)\n * Probes .well-known/oauth-authorization-server endpoint\n *\n * @param authServerUrl - URL of the authorization server (typically from RFC 9728 discovery)\n * @returns AuthorizationServerMetadata if discovered, null otherwise\n *\n * @example\n * const metadata = await discoverAuthorizationServerMetadata('https://todoist.com');\n * // Returns: { issuer: \"https://todoist.com\", authorization_endpoint: \"...\", ... }\n */\nexport async function discoverAuthorizationServerMetadata(authServerUrl: string): Promise<AuthorizationServerMetadata | null> {\n try {\n const origin = getOrigin(authServerUrl);\n const wellKnownUrl = `${origin}/.well-known/oauth-authorization-server`;\n\n const response = await fetch(wellKnownUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (!response.ok) {\n return null;\n }\n\n return (await response.json()) as AuthorizationServerMetadata;\n } catch (_error) {\n return null;\n }\n}\n\n/**\n * Discover OAuth Authorization Server Issuer from resource response (RFC 9207)\n *\n * @param resourceUrl - URL of the protected resource\n * @returns Issuer URL if present in WWW-Authenticate header, null otherwise\n */\nexport async function discoverAuthorizationServerIssuer(resourceUrl: string): Promise<string | null> {\n try {\n const response = await fetch(resourceUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n const header = response.headers.get('www-authenticate');\n if (!header) return null;\n\n const match = header.match(/(?:authorization_server|issuer)=\"([^\"]+)\"/i);\n if (!match) return null;\n\n return match[1] ?? null;\n } catch (_error) {\n return null;\n }\n}\n"],"names":["discoverAuthorizationServerIssuer","discoverAuthorizationServerMetadata","discoverProtectedResourceMetadata","getOrigin","url","URL","origin","getPath","parsed","pathname","resourceUrl","headerMetadata","path","rootUrl","response","metadata","rootMetadata","subPathUrl","subPathResponse","_error","discoverProtectedResourceMetadataFromHeader","fetch","method","headers","Accept","Connection","ok","json","resource","startsWith","header","postResponse","match","metadataUrl","metadataResponse","get","body","authServerUrl","wellKnownUrl"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/Projects/mcp-z/client/src/auth/rfc9728-discovery.ts"],"sourcesContent":["/**\n * RFC 9728 Protected Resource Metadata Discovery\n * Probes .well-known/oauth-protected-resource endpoint\n */\n\nimport { joinWellKnown, normalizeUrl } from '../lib/url-utils.ts';\nimport type { AuthorizationServerMetadata, ProtectedResourceMetadata } from './types.ts';\n\n/**\n * Extract origin (protocol + host) from a URL\n * @param url - Full URL that may include a path\n * @returns Origin (e.g., \"https://example.com\") or original string if invalid URL\n *\n * @example\n * getOrigin('https://example.com/mcp') // → 'https://example.com'\n * getOrigin('http://localhost:9999/api/v1/mcp') // → 'http://localhost:9999'\n */\nfunction getOrigin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n // Invalid URL - return as-is for graceful degradation\n return url;\n }\n}\n\n/**\n * Extract path from a URL (without origin)\n * @param url - Full URL\n * @returns Path component (e.g., \"/mcp\", \"/api/v1/mcp\") or empty string if no path\n */\nfunction getPath(url: string): string {\n try {\n const parsed = new URL(url);\n // pathname includes leading slash, e.g., \"/mcp\"\n return parsed.pathname === '/' ? '' : parsed.pathname;\n } catch {\n return '';\n }\n}\n\n/**\n * Normalize a resource URL by stripping query/hash and trailing slashes.\n */\n/**\n * Discover OAuth 2.0 Protected Resource Metadata (RFC 9728)\n * Probes .well-known/oauth-protected-resource endpoint\n *\n * Discovery Strategy:\n * 1. Try origin root: {origin}/.well-known/oauth-protected-resource\n * 2. If 404, try sub-path: {origin}/.well-known/oauth-protected-resource{path}\n *\n * @param resourceUrl - URL of the protected resource (e.g., https://ai.todoist.net/mcp)\n * @returns ProtectedResourceMetadata if discovered, null otherwise\n *\n * @example\n * // Todoist case: MCP at ai.todoist.net/mcp, OAuth at todoist.com\n * const metadata = await discoverProtectedResourceMetadata('https://ai.todoist.net/mcp');\n * // Returns: { resource: \"https://ai.todoist.net/mcp\", authorization_servers: [\"https://todoist.com\"] }\n */\nexport async function discoverProtectedResourceMetadata(resourceUrl: string): Promise<ProtectedResourceMetadata | null> {\n try {\n const normalizedResourceUrl = normalizeUrl(resourceUrl);\n const headerMetadata = await discoverProtectedResourceMetadataFromHeader(normalizedResourceUrl);\n if (headerMetadata) return headerMetadata;\n\n // Strategy 0: Try path-local well-known (supports path-prefixed deployments like /outlook)\n const localWellKnownUrl = joinWellKnown(normalizedResourceUrl, '/.well-known/oauth-protected-resource');\n try {\n const response = await fetch(localWellKnownUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n if (response.ok) {\n return (await response.json()) as ProtectedResourceMetadata;\n }\n } catch {\n // Continue to origin-based discovery\n }\n\n const origin = getOrigin(normalizedResourceUrl);\n const path = getPath(normalizedResourceUrl);\n\n // Strategy 1: Try root location (REQUIRED by RFC 9728)\n const rootUrl = `${origin}/.well-known/oauth-protected-resource`;\n\n try {\n const response = await fetch(rootUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (response.ok) {\n const metadata = (await response.json()) as ProtectedResourceMetadata;\n // Check if the discovered resource matches what we're looking for\n if (metadata.resource === normalizedResourceUrl) {\n return metadata;\n }\n // If there's no path component, return root metadata\n // (e.g., looking for http://example.com and found it)\n if (!path) {\n return metadata;\n }\n // If requested URL starts with metadata.resource, the root metadata applies to sub-paths\n // (e.g., looking for http://example.com/api/v1/mcp, found http://example.com)\n if (normalizedResourceUrl.startsWith(metadata.resource)) {\n // Still try sub-path location to see if there's more specific metadata\n // But save root metadata as fallback\n const rootMetadata = metadata;\n\n // Try sub-path location for more specific metadata\n const subPathUrl = `${origin}/.well-known/oauth-protected-resource${path}`;\n try {\n const subPathResponse = await fetch(subPathUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n if (subPathResponse.ok) {\n return (await subPathResponse.json()) as ProtectedResourceMetadata;\n }\n } catch {\n // Sub-path failed, use root metadata\n }\n\n // Return root metadata as it applies to this resource\n return rootMetadata;\n }\n // Otherwise, try sub-path location before giving up\n }\n } catch {\n // Continue to sub-path location\n }\n\n // Strategy 2: Try sub-path location (MCP spec extension)\n // Only try if there's a path component\n if (path) {\n const subPathUrl = `${origin}/.well-known/oauth-protected-resource${path}`;\n\n try {\n const response = await fetch(subPathUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (response.ok) {\n return (await response.json()) as ProtectedResourceMetadata;\n }\n } catch {\n // Fall through to return null\n }\n }\n\n // Neither location found or resource didn't match\n return null;\n } catch (_error) {\n // Network error, invalid URL, or other failure\n return null;\n }\n}\n\nasync function discoverProtectedResourceMetadataFromHeader(resourceUrl: string): Promise<ProtectedResourceMetadata | null> {\n try {\n const response = await fetch(resourceUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n let header = response.headers.get('www-authenticate');\n if (!header) {\n const postResponse = await fetch(resourceUrl, {\n method: 'POST',\n headers: { Accept: 'application/json', Connection: 'close', 'Content-Type': 'application/json' },\n body: '{}',\n });\n header = postResponse.headers.get('www-authenticate');\n }\n\n if (!header) return null;\n\n const match = header.match(/resource_metadata=\"([^\"]+)\"/i);\n if (!match || !match[1]) return null;\n\n const metadataUrl = match[1];\n const metadataResponse = await fetch(metadataUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (!metadataResponse.ok) {\n return null;\n }\n\n return (await metadataResponse.json()) as ProtectedResourceMetadata;\n } catch (_error) {\n return null;\n }\n}\n\n/**\n * Discover OAuth 2.0 Authorization Server Metadata (RFC 8414)\n * Probes .well-known/oauth-authorization-server endpoint\n *\n * @param authServerUrl - URL of the authorization server (typically from RFC 9728 discovery)\n * @returns AuthorizationServerMetadata if discovered, null otherwise\n *\n * @example\n * const metadata = await discoverAuthorizationServerMetadata('https://todoist.com');\n * // Returns: { issuer: \"https://todoist.com\", authorization_endpoint: \"...\", ... }\n */\nexport async function discoverAuthorizationServerMetadata(authServerUrl: string): Promise<AuthorizationServerMetadata | null> {\n try {\n const normalizedAuthServerUrl = normalizeUrl(authServerUrl);\n const localWellKnownUrl = joinWellKnown(normalizedAuthServerUrl, '/.well-known/oauth-authorization-server');\n const localResponse = await fetch(localWellKnownUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (localResponse.ok) {\n return (await localResponse.json()) as AuthorizationServerMetadata;\n }\n\n const origin = getOrigin(normalizedAuthServerUrl);\n const wellKnownUrl = `${origin}/.well-known/oauth-authorization-server`;\n\n const response = await fetch(wellKnownUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (!response.ok) {\n return null;\n }\n\n return (await response.json()) as AuthorizationServerMetadata;\n } catch (_error) {\n return null;\n }\n}\n\n/**\n * Discover OAuth Authorization Server Issuer from resource response (RFC 9207)\n *\n * @param resourceUrl - URL of the protected resource\n * @returns Issuer URL if present in WWW-Authenticate header, null otherwise\n */\nexport async function discoverAuthorizationServerIssuer(resourceUrl: string): Promise<string | null> {\n try {\n const response = await fetch(resourceUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n const header = response.headers.get('www-authenticate');\n if (!header) return null;\n\n const match = header.match(/(?:authorization_server|issuer)=\"([^\"]+)\"/i);\n if (!match) return null;\n\n return match[1] ?? null;\n } catch (_error) {\n return null;\n }\n}\n"],"names":["discoverAuthorizationServerIssuer","discoverAuthorizationServerMetadata","discoverProtectedResourceMetadata","getOrigin","url","URL","origin","getPath","parsed","pathname","resourceUrl","normalizedResourceUrl","headerMetadata","localWellKnownUrl","response","path","rootUrl","metadata","rootMetadata","subPathUrl","subPathResponse","_error","normalizeUrl","discoverProtectedResourceMetadataFromHeader","joinWellKnown","fetch","method","headers","Accept","Connection","ok","json","resource","startsWith","header","postResponse","match","metadataUrl","metadataResponse","get","body","authServerUrl","normalizedAuthServerUrl","localResponse","wellKnownUrl"],"mappings":"AAAA;;;CAGC;;;;;;;;;;;QAmPqBA;eAAAA;;QArCAC;eAAAA;;QArJAC;eAAAA;;;0BAvDsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAG5C;;;;;;;;CAQC,GACD,SAASC,UAAUC,GAAW;IAC5B,IAAI;QACF,OAAO,IAAIC,IAAID,KAAKE,MAAM;IAC5B,EAAE,eAAM;QACN,sDAAsD;QACtD,OAAOF;IACT;AACF;AAEA;;;;CAIC,GACD,SAASG,QAAQH,GAAW;IAC1B,IAAI;QACF,IAAMI,SAAS,IAAIH,IAAID;QACvB,gDAAgD;QAChD,OAAOI,OAAOC,QAAQ,KAAK,MAAM,KAAKD,OAAOC,QAAQ;IACvD,EAAE,eAAM;QACN,OAAO;IACT;AACF;AAqBO,SAAeP,kCAAkCQ,WAAmB;;YAEjEC,uBACAC,gBAIAC,mBAEEC,kBAWFR,QACAS,MAGAC,SAGEF,WAMEG,UAeEC,cAGAC,YAEEC,mCAuBND,aAGEL,oBAeHO;;;;;;;;;;oBA5FDV,wBAAwBW,IAAAA,wBAAY,EAACZ;oBACpB;;wBAAMa,4CAA4CZ;;;oBAAnEC,iBAAiB;oBACvB,IAAIA,gBAAgB;;wBAAOA;;oBAE3B,2FAA2F;oBACrFC,oBAAoBW,IAAAA,yBAAa,EAACb,uBAAuB;;;;;;;;;oBAE5C;;wBAAMc,MAAMZ,mBAAmB;4BAC9Ca,QAAQ;4BACRC,SAAS;gCAAEC,QAAQ;gCAAoBC,YAAY;4BAAQ;wBAC7D;;;oBAHMf,WAAW;yBAIbA,SAASgB,EAAE,EAAXhB;;;;oBACM;;wBAAMA,SAASiB,IAAI;;;oBAA3B;;wBAAQ;;;;;;;;;;;;;;oBAMNzB,SAASH,UAAUQ;oBACnBI,OAAOR,QAAQI;oBAErB,uDAAuD;oBACjDK,UAAU,AAAC,GAAS,OAAPV,QAAO;;;;;;;;;oBAGP;;wBAAMmB,MAAMT,SAAS;4BACpCU,QAAQ;4BACRC,SAAS;gCAAEC,QAAQ;gCAAoBC,YAAY;4BAAQ;wBAC7D;;;oBAHMf,YAAW;yBAKbA,UAASgB,EAAE,EAAXhB;;;;oBACgB;;wBAAMA,UAASiB,IAAI;;;oBAA/Bd,WAAY;oBAClB,kEAAkE;oBAClE,IAAIA,SAASe,QAAQ,KAAKrB,uBAAuB;wBAC/C;;4BAAOM;;oBACT;oBACA,qDAAqD;oBACrD,sDAAsD;oBACtD,IAAI,CAACF,MAAM;wBACT;;4BAAOE;;oBACT;yBAGIN,sBAAsBsB,UAAU,CAAChB,SAASe,QAAQ,GAAlDrB;;;;oBACF,uEAAuE;oBACvE,qCAAqC;oBAC/BO,eAAeD;oBAErB,mDAAmD;oBAC7CE,aAAa,AAAC,GAAgDJ,OAA9CT,QAAO,yCAA4C,OAALS;;;;;;;;;oBAE1C;;wBAAMU,MAAMN,YAAY;4BAC9CO,QAAQ;4BACRC,SAAS;gCAAEC,QAAQ;gCAAoBC,YAAY;4BAAQ;wBAC7D;;;oBAHMT,kBAAkB;yBAIpBA,gBAAgBU,EAAE,EAAlBV;;;;oBACM;;wBAAMA,gBAAgBW,IAAI;;;oBAAlC;;wBAAQ;;;;;;;;;;;;;;oBAMZ,sDAAsD;oBACtD;;wBAAOb;;;;;;;;;;;;;;yBAUTH,MAAAA;;;;oBACII,cAAa,AAAC,GAAgDJ,OAA9CT,QAAO,yCAA4C,OAALS;;;;;;;;;oBAGjD;;wBAAMU,MAAMN,aAAY;4BACvCO,QAAQ;4BACRC,SAAS;gCAAEC,QAAQ;gCAAoBC,YAAY;4BAAQ;wBAC7D;;;oBAHMf,YAAW;yBAKbA,UAASgB,EAAE,EAAXhB;;;;oBACM;;wBAAMA,UAASiB,IAAI;;;oBAA3B;;wBAAQ;;;;;;;;;;;;;;oBAOd,kDAAkD;oBAClD;;wBAAO;;;oBACAV;oBACP,+CAA+C;oBAC/C;;wBAAO;;;;;;;;IAEX;;AAEA,SAAeE,4CAA4Cb,WAAmB;;YAEpEI,UAKFoB,QAEIC,cAUFC,OAGAC,aACAC,kBAUCjB;;;;;;;;;;oBA/BU;;wBAAMI,MAAMf,aAAa;4BACxCgB,QAAQ;4BACRC,SAAS;gCAAEC,QAAQ;gCAAoBC,YAAY;4BAAQ;wBAC7D;;;oBAHMf,WAAW;oBAKboB,SAASpB,SAASa,OAAO,CAACY,GAAG,CAAC;yBAC9B,CAACL,QAAD;;;;oBACmB;;wBAAMT,MAAMf,aAAa;4BAC5CgB,QAAQ;4BACRC,SAAS;gCAAEC,QAAQ;gCAAoBC,YAAY;gCAAS,gBAAgB;4BAAmB;4BAC/FW,MAAM;wBACR;;;oBAJML,eAAe;oBAKrBD,SAASC,aAAaR,OAAO,CAACY,GAAG,CAAC;;;oBAGpC,IAAI,CAACL,QAAQ;;wBAAO;;oBAEdE,QAAQF,OAAOE,KAAK,CAAC;oBAC3B,IAAI,CAACA,SAAS,CAACA,KAAK,CAAC,EAAE,EAAE;;wBAAO;;oBAE1BC,cAAcD,KAAK,CAAC,EAAE;oBACH;;wBAAMX,MAAMY,aAAa;4BAChDX,QAAQ;4BACRC,SAAS;gCAAEC,QAAQ;gCAAoBC,YAAY;4BAAQ;wBAC7D;;;oBAHMS,mBAAmB;oBAKzB,IAAI,CAACA,iBAAiBR,EAAE,EAAE;wBACxB;;4BAAO;;oBACT;oBAEQ;;wBAAMQ,iBAAiBP,IAAI;;;oBAAnC;;wBAAQ;;;oBACDV;oBACP;;wBAAO;;;;;;;;IAEX;;AAaO,SAAepB,oCAAoCwC,aAAqB;;YAErEC,yBACA7B,mBACA8B,eASArC,QACAsC,cAEA9B,UAUCO;;;;;;;;;;oBAxBDqB,0BAA0BpB,IAAAA,wBAAY,EAACmB;oBACvC5B,oBAAoBW,IAAAA,yBAAa,EAACkB,yBAAyB;oBAC3C;;wBAAMjB,MAAMZ,mBAAmB;4BACnDa,QAAQ;4BACRC,SAAS;gCAAEC,QAAQ;gCAAoBC,YAAY;4BAAQ;wBAC7D;;;oBAHMc,gBAAgB;yBAKlBA,cAAcb,EAAE,EAAhBa;;;;oBACM;;wBAAMA,cAAcZ,IAAI;;;oBAAhC;;wBAAQ;;;oBAGJzB,SAASH,UAAUuC;oBACnBE,eAAe,AAAC,GAAS,OAAPtC,QAAO;oBAEd;;wBAAMmB,MAAMmB,cAAc;4BACzClB,QAAQ;4BACRC,SAAS;gCAAEC,QAAQ;gCAAoBC,YAAY;4BAAQ;wBAC7D;;;oBAHMf,WAAW;oBAKjB,IAAI,CAACA,SAASgB,EAAE,EAAE;wBAChB;;4BAAO;;oBACT;oBAEQ;;wBAAMhB,SAASiB,IAAI;;;oBAA3B;;wBAAQ;;;oBACDV;oBACP;;wBAAO;;;;;;;;IAEX;;AAQO,SAAerB,kCAAkCU,WAAmB;;YAahE0B,SAXDtB,UAKAoB,QAGAE,OAICf;;;;;;;;;;oBAZU;;wBAAMI,MAAMf,aAAa;4BACxCgB,QAAQ;4BACRC,SAAS;gCAAEC,QAAQ;gCAAoBC,YAAY;4BAAQ;wBAC7D;;;oBAHMf,WAAW;oBAKXoB,SAASpB,SAASa,OAAO,CAACY,GAAG,CAAC;oBACpC,IAAI,CAACL,QAAQ;;wBAAO;;oBAEdE,QAAQF,OAAOE,KAAK,CAAC;oBAC3B,IAAI,CAACA,OAAO;;wBAAO;;oBAEnB;;yBAAOA,UAAAA,KAAK,CAAC,EAAE,cAARA,qBAAAA,UAAY;;;oBACZf;oBACP;;wBAAO;;;;;;;;IAEX"}
@@ -0,0 +1,2 @@
1
+ export declare function normalizeUrl(input: string): string;
2
+ export declare function joinWellKnown(baseUrl: string, suffix: string): string;
@@ -0,0 +1,2 @@
1
+ export declare function normalizeUrl(input: string): string;
2
+ export declare function joinWellKnown(baseUrl: string, suffix: string): string;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ function _export(target, all) {
6
+ for(var name in all)Object.defineProperty(target, name, {
7
+ enumerable: true,
8
+ get: Object.getOwnPropertyDescriptor(all, name).get
9
+ });
10
+ }
11
+ _export(exports, {
12
+ get joinWellKnown () {
13
+ return joinWellKnown;
14
+ },
15
+ get normalizeUrl () {
16
+ return normalizeUrl;
17
+ }
18
+ });
19
+ function normalizeUrl(input) {
20
+ try {
21
+ var url = new URL(input);
22
+ url.search = '';
23
+ url.hash = '';
24
+ url.pathname = url.pathname.replace(/\/+$/, '');
25
+ return url.origin + url.pathname;
26
+ } catch (unused) {
27
+ return input.replace(/\/+$/, '');
28
+ }
29
+ }
30
+ function joinWellKnown(baseUrl, suffix) {
31
+ return "".concat(normalizeUrl(baseUrl)).concat(suffix);
32
+ }
33
+ /* CJS INTEROP */ if (exports.__esModule && exports.default) { try { Object.defineProperty(exports.default, '__esModule', { value: true }); for (var key in exports) { exports.default[key] = exports[key]; } } catch (_) {}; module.exports = exports.default; }
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/kevin/Dev/Projects/mcp-z/client/src/lib/url-utils.ts"],"sourcesContent":["export function normalizeUrl(input: string): string {\n try {\n const url = new URL(input);\n url.search = '';\n url.hash = '';\n url.pathname = url.pathname.replace(/\\/+$/, '');\n return url.origin + url.pathname;\n } catch {\n return input.replace(/\\/+$/, '');\n }\n}\n\nexport function joinWellKnown(baseUrl: string, suffix: string): string {\n return `${normalizeUrl(baseUrl)}${suffix}`;\n}\n"],"names":["joinWellKnown","normalizeUrl","input","url","URL","search","hash","pathname","replace","origin","baseUrl","suffix"],"mappings":";;;;;;;;;;;QAYgBA;eAAAA;;QAZAC;eAAAA;;;AAAT,SAASA,aAAaC,KAAa;IACxC,IAAI;QACF,IAAMC,MAAM,IAAIC,IAAIF;QACpBC,IAAIE,MAAM,GAAG;QACbF,IAAIG,IAAI,GAAG;QACXH,IAAII,QAAQ,GAAGJ,IAAII,QAAQ,CAACC,OAAO,CAAC,QAAQ;QAC5C,OAAOL,IAAIM,MAAM,GAAGN,IAAII,QAAQ;IAClC,EAAE,eAAM;QACN,OAAOL,MAAMM,OAAO,CAAC,QAAQ;IAC/B;AACF;AAEO,SAASR,cAAcU,OAAe,EAAEC,MAAc;IAC3D,OAAO,AAAC,GAA0BA,OAAxBV,aAAaS,UAAkB,OAAPC;AACpC"}
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * OAuth Server Capability Discovery
3
3
  * Probes RFC 9728 (Protected Resource) and RFC 8414 (Authorization Server) metadata
4
- */ import { discoverAuthorizationServerIssuer, discoverAuthorizationServerMetadata, discoverProtectedResourceMetadata } from './rfc9728-discovery.js';
4
+ */ import { normalizeUrl } from '../lib/url-utils.js';
5
+ import { discoverAuthorizationServerIssuer, discoverAuthorizationServerMetadata, discoverProtectedResourceMetadata } from './rfc9728-discovery.js';
5
6
  /**
6
7
  * Extract origin (protocol + host) from a URL
7
8
  * @param url - Full URL that may include a path
@@ -65,9 +66,10 @@ async function resolveCapabilitiesFromAuthorizationServer(authServerUrl, scopes)
65
66
  }
66
67
  export async function probeAuthCapabilities(baseUrl) {
67
68
  try {
69
+ const normalizedBaseUrl = normalizeUrl(baseUrl);
68
70
  // Strategy 1: Try RFC 9728 Protected Resource Metadata discovery
69
71
  // This handles cross-domain OAuth (e.g., Todoist: ai.todoist.net/mcp → todoist.com)
70
- const resourceMetadata = await discoverProtectedResourceMetadata(baseUrl);
72
+ const resourceMetadata = await discoverProtectedResourceMetadata(normalizedBaseUrl);
71
73
  if (resourceMetadata && resourceMetadata.authorization_servers.length > 0) {
72
74
  // Found protected resource metadata with authorization servers
73
75
  // Discover the authorization server's metadata (RFC 8414)
@@ -88,14 +90,14 @@ export async function probeAuthCapabilities(baseUrl) {
88
90
  if (issuerCapabilities) return issuerCapabilities;
89
91
  }
90
92
  }
91
- const issuer = await discoverAuthorizationServerIssuer(baseUrl);
93
+ const issuer = await discoverAuthorizationServerIssuer(normalizedBaseUrl);
92
94
  if (issuer) {
93
95
  const issuerCapabilities = await resolveCapabilitiesFromAuthorizationServer(issuer);
94
96
  if (issuerCapabilities) return issuerCapabilities;
95
97
  }
96
98
  // Strategy 2: Fall back to direct RFC 8414 discovery at resource origin
97
99
  // This handles same-domain OAuth (traditional setup)
98
- const origin = getOrigin(baseUrl);
100
+ const origin = getOrigin(normalizedBaseUrl);
99
101
  const originCapabilities = await resolveCapabilitiesFromAuthorizationServer(origin);
100
102
  if (originCapabilities) return originCapabilities;
101
103
  // No OAuth metadata found
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/Projects/mcp-z/client/src/auth/capability-discovery.ts"],"sourcesContent":["/**\n * OAuth Server Capability Discovery\n * Probes RFC 9728 (Protected Resource) and RFC 8414 (Authorization Server) metadata\n */\n\nimport { discoverAuthorizationServerIssuer, discoverAuthorizationServerMetadata, discoverProtectedResourceMetadata } from './rfc9728-discovery.ts';\nimport type { AuthCapabilities, AuthorizationServerMetadata } from './types.ts';\n\n/**\n * Extract origin (protocol + host) from a URL\n * @param url - Full URL that may include a path\n * @returns Origin (e.g., \"https://example.com\") or original string if invalid URL\n *\n * @example\n * getOrigin('https://example.com/mcp') // → 'https://example.com'\n * getOrigin('http://localhost:9999/api/v1/mcp') // → 'http://localhost:9999'\n */\nfunction getOrigin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n // Invalid URL - return as-is for graceful degradation\n return url;\n }\n}\n\n/**\n * Probe OAuth server capabilities using RFC 9728 → RFC 8414 discovery chain\n * Returns capabilities including DCR support detection\n *\n * Discovery Strategy:\n * 1. Try RFC 9728 Protected Resource Metadata (supports cross-domain OAuth)\n * 2. If found, use first authorization_server to discover RFC 8414 Authorization Server Metadata\n * 3. Fall back to direct RFC 8414 discovery at resource origin\n *\n * @param baseUrl - Base URL of the protected resource (e.g., https://ai.todoist.net/mcp)\n * @returns AuthCapabilities object with discovered endpoints and features\n *\n * @example\n * // Todoist case: MCP at ai.todoist.net/mcp, OAuth at todoist.com\n * const caps = await probeAuthCapabilities('https://ai.todoist.net/mcp');\n * if (caps.supportsDcr) {\n * console.log('Registration endpoint:', caps.registrationEndpoint);\n * }\n */\nfunction buildCapabilities(metadata: AuthorizationServerMetadata, scopes?: string[]): AuthCapabilities {\n const supportsDcr = !!metadata.registration_endpoint;\n const capabilities: AuthCapabilities = { supportsDcr };\n\n if (metadata.registration_endpoint) {\n capabilities.registrationEndpoint = metadata.registration_endpoint;\n }\n if (metadata.authorization_endpoint) {\n capabilities.authorizationEndpoint = metadata.authorization_endpoint;\n }\n if (metadata.token_endpoint) capabilities.tokenEndpoint = metadata.token_endpoint;\n if (metadata.introspection_endpoint) {\n capabilities.introspectionEndpoint = metadata.introspection_endpoint;\n }\n\n if (scopes && scopes.length > 0) {\n capabilities.scopes = scopes;\n } else if (metadata.scopes_supported) {\n capabilities.scopes = metadata.scopes_supported;\n }\n\n return capabilities;\n}\n\nasync function resolveCapabilitiesFromAuthorizationServer(authServerUrl: string, scopes?: string[]): Promise<AuthCapabilities | null> {\n const metadata = await discoverAuthorizationServerMetadata(authServerUrl);\n if (!metadata) return null;\n return buildCapabilities(metadata, scopes);\n}\n\nexport async function probeAuthCapabilities(baseUrl: string): Promise<AuthCapabilities> {\n try {\n // Strategy 1: Try RFC 9728 Protected Resource Metadata discovery\n // This handles cross-domain OAuth (e.g., Todoist: ai.todoist.net/mcp → todoist.com)\n const resourceMetadata = await discoverProtectedResourceMetadata(baseUrl);\n\n if (resourceMetadata && resourceMetadata.authorization_servers.length > 0) {\n // Found protected resource metadata with authorization servers\n // Discover the authorization server's metadata (RFC 8414)\n const authServerUrl = resourceMetadata.authorization_servers[0];\n if (!authServerUrl) {\n // Array has length > 0 but first element is undefined/null - skip this path\n return { supportsDcr: false };\n }\n const capabilities = await resolveCapabilitiesFromAuthorizationServer(authServerUrl, resourceMetadata.scopes_supported);\n if (capabilities) {\n return capabilities;\n }\n\n const issuer = await discoverAuthorizationServerIssuer(baseUrl);\n if (issuer) {\n const issuerCapabilities = await resolveCapabilitiesFromAuthorizationServer(issuer, resourceMetadata.scopes_supported);\n if (issuerCapabilities) return issuerCapabilities;\n }\n }\n\n const issuer = await discoverAuthorizationServerIssuer(baseUrl);\n if (issuer) {\n const issuerCapabilities = await resolveCapabilitiesFromAuthorizationServer(issuer);\n if (issuerCapabilities) return issuerCapabilities;\n }\n\n // Strategy 2: Fall back to direct RFC 8414 discovery at resource origin\n // This handles same-domain OAuth (traditional setup)\n const origin = getOrigin(baseUrl);\n const originCapabilities = await resolveCapabilitiesFromAuthorizationServer(origin);\n if (originCapabilities) return originCapabilities;\n\n // No OAuth metadata found\n return { supportsDcr: false };\n } catch (_error) {\n // Network error, invalid JSON, or other fetch failure\n // Gracefully degrade - assume no DCR support\n return { supportsDcr: false };\n }\n}\n"],"names":["discoverAuthorizationServerIssuer","discoverAuthorizationServerMetadata","discoverProtectedResourceMetadata","getOrigin","url","URL","origin","buildCapabilities","metadata","scopes","supportsDcr","registration_endpoint","capabilities","registrationEndpoint","authorization_endpoint","authorizationEndpoint","token_endpoint","tokenEndpoint","introspection_endpoint","introspectionEndpoint","length","scopes_supported","resolveCapabilitiesFromAuthorizationServer","authServerUrl","probeAuthCapabilities","baseUrl","resourceMetadata","authorization_servers","issuer","issuerCapabilities","originCapabilities","_error"],"mappings":"AAAA;;;CAGC,GAED,SAASA,iCAAiC,EAAEC,mCAAmC,EAAEC,iCAAiC,QAAQ,yBAAyB;AAGnJ;;;;;;;;CAQC,GACD,SAASC,UAAUC,GAAW;IAC5B,IAAI;QACF,OAAO,IAAIC,IAAID,KAAKE,MAAM;IAC5B,EAAE,OAAM;QACN,sDAAsD;QACtD,OAAOF;IACT;AACF;AAEA;;;;;;;;;;;;;;;;;;CAkBC,GACD,SAASG,kBAAkBC,QAAqC,EAAEC,MAAiB;IACjF,MAAMC,cAAc,CAAC,CAACF,SAASG,qBAAqB;IACpD,MAAMC,eAAiC;QAAEF;IAAY;IAErD,IAAIF,SAASG,qBAAqB,EAAE;QAClCC,aAAaC,oBAAoB,GAAGL,SAASG,qBAAqB;IACpE;IACA,IAAIH,SAASM,sBAAsB,EAAE;QACnCF,aAAaG,qBAAqB,GAAGP,SAASM,sBAAsB;IACtE;IACA,IAAIN,SAASQ,cAAc,EAAEJ,aAAaK,aAAa,GAAGT,SAASQ,cAAc;IACjF,IAAIR,SAASU,sBAAsB,EAAE;QACnCN,aAAaO,qBAAqB,GAAGX,SAASU,sBAAsB;IACtE;IAEA,IAAIT,UAAUA,OAAOW,MAAM,GAAG,GAAG;QAC/BR,aAAaH,MAAM,GAAGA;IACxB,OAAO,IAAID,SAASa,gBAAgB,EAAE;QACpCT,aAAaH,MAAM,GAAGD,SAASa,gBAAgB;IACjD;IAEA,OAAOT;AACT;AAEA,eAAeU,2CAA2CC,aAAqB,EAAEd,MAAiB;IAChG,MAAMD,WAAW,MAAMP,oCAAoCsB;IAC3D,IAAI,CAACf,UAAU,OAAO;IACtB,OAAOD,kBAAkBC,UAAUC;AACrC;AAEA,OAAO,eAAee,sBAAsBC,OAAe;IACzD,IAAI;QACF,iEAAiE;QACjE,oFAAoF;QACpF,MAAMC,mBAAmB,MAAMxB,kCAAkCuB;QAEjE,IAAIC,oBAAoBA,iBAAiBC,qBAAqB,CAACP,MAAM,GAAG,GAAG;YACzE,+DAA+D;YAC/D,0DAA0D;YAC1D,MAAMG,gBAAgBG,iBAAiBC,qBAAqB,CAAC,EAAE;YAC/D,IAAI,CAACJ,eAAe;gBAClB,4EAA4E;gBAC5E,OAAO;oBAAEb,aAAa;gBAAM;YAC9B;YACA,MAAME,eAAe,MAAMU,2CAA2CC,eAAeG,iBAAiBL,gBAAgB;YACtH,IAAIT,cAAc;gBAChB,OAAOA;YACT;YAEA,MAAMgB,SAAS,MAAM5B,kCAAkCyB;YACvD,IAAIG,QAAQ;gBACV,MAAMC,qBAAqB,MAAMP,2CAA2CM,QAAQF,iBAAiBL,gBAAgB;gBACrH,IAAIQ,oBAAoB,OAAOA;YACjC;QACF;QAEA,MAAMD,SAAS,MAAM5B,kCAAkCyB;QACvD,IAAIG,QAAQ;YACV,MAAMC,qBAAqB,MAAMP,2CAA2CM;YAC5E,IAAIC,oBAAoB,OAAOA;QACjC;QAEA,wEAAwE;QACxE,qDAAqD;QACrD,MAAMvB,SAASH,UAAUsB;QACzB,MAAMK,qBAAqB,MAAMR,2CAA2ChB;QAC5E,IAAIwB,oBAAoB,OAAOA;QAE/B,0BAA0B;QAC1B,OAAO;YAAEpB,aAAa;QAAM;IAC9B,EAAE,OAAOqB,QAAQ;QACf,sDAAsD;QACtD,6CAA6C;QAC7C,OAAO;YAAErB,aAAa;QAAM;IAC9B;AACF"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/Projects/mcp-z/client/src/auth/capability-discovery.ts"],"sourcesContent":["/**\n * OAuth Server Capability Discovery\n * Probes RFC 9728 (Protected Resource) and RFC 8414 (Authorization Server) metadata\n */\n\nimport { normalizeUrl } from '../lib/url-utils.ts';\nimport { discoverAuthorizationServerIssuer, discoverAuthorizationServerMetadata, discoverProtectedResourceMetadata } from './rfc9728-discovery.ts';\nimport type { AuthCapabilities, AuthorizationServerMetadata } from './types.ts';\n\n/**\n * Extract origin (protocol + host) from a URL\n * @param url - Full URL that may include a path\n * @returns Origin (e.g., \"https://example.com\") or original string if invalid URL\n *\n * @example\n * getOrigin('https://example.com/mcp') // → 'https://example.com'\n * getOrigin('http://localhost:9999/api/v1/mcp') // → 'http://localhost:9999'\n */\nfunction getOrigin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n // Invalid URL - return as-is for graceful degradation\n return url;\n }\n}\n\n/**\n * Probe OAuth server capabilities using RFC 9728 → RFC 8414 discovery chain\n * Returns capabilities including DCR support detection\n *\n * Discovery Strategy:\n * 1. Try RFC 9728 Protected Resource Metadata (supports cross-domain OAuth)\n * 2. If found, use first authorization_server to discover RFC 8414 Authorization Server Metadata\n * 3. Fall back to direct RFC 8414 discovery at resource origin\n *\n * @param baseUrl - Base URL of the protected resource (e.g., https://ai.todoist.net/mcp)\n * @returns AuthCapabilities object with discovered endpoints and features\n *\n * @example\n * // Todoist case: MCP at ai.todoist.net/mcp, OAuth at todoist.com\n * const caps = await probeAuthCapabilities('https://ai.todoist.net/mcp');\n * if (caps.supportsDcr) {\n * console.log('Registration endpoint:', caps.registrationEndpoint);\n * }\n */\nfunction buildCapabilities(metadata: AuthorizationServerMetadata, scopes?: string[]): AuthCapabilities {\n const supportsDcr = !!metadata.registration_endpoint;\n const capabilities: AuthCapabilities = { supportsDcr };\n\n if (metadata.registration_endpoint) {\n capabilities.registrationEndpoint = metadata.registration_endpoint;\n }\n if (metadata.authorization_endpoint) {\n capabilities.authorizationEndpoint = metadata.authorization_endpoint;\n }\n if (metadata.token_endpoint) capabilities.tokenEndpoint = metadata.token_endpoint;\n if (metadata.introspection_endpoint) {\n capabilities.introspectionEndpoint = metadata.introspection_endpoint;\n }\n\n if (scopes && scopes.length > 0) {\n capabilities.scopes = scopes;\n } else if (metadata.scopes_supported) {\n capabilities.scopes = metadata.scopes_supported;\n }\n\n return capabilities;\n}\n\nasync function resolveCapabilitiesFromAuthorizationServer(authServerUrl: string, scopes?: string[]): Promise<AuthCapabilities | null> {\n const metadata = await discoverAuthorizationServerMetadata(authServerUrl);\n if (!metadata) return null;\n return buildCapabilities(metadata, scopes);\n}\n\nexport async function probeAuthCapabilities(baseUrl: string): Promise<AuthCapabilities> {\n try {\n const normalizedBaseUrl = normalizeUrl(baseUrl);\n // Strategy 1: Try RFC 9728 Protected Resource Metadata discovery\n // This handles cross-domain OAuth (e.g., Todoist: ai.todoist.net/mcp → todoist.com)\n const resourceMetadata = await discoverProtectedResourceMetadata(normalizedBaseUrl);\n\n if (resourceMetadata && resourceMetadata.authorization_servers.length > 0) {\n // Found protected resource metadata with authorization servers\n // Discover the authorization server's metadata (RFC 8414)\n const authServerUrl = resourceMetadata.authorization_servers[0];\n if (!authServerUrl) {\n // Array has length > 0 but first element is undefined/null - skip this path\n return { supportsDcr: false };\n }\n const capabilities = await resolveCapabilitiesFromAuthorizationServer(authServerUrl, resourceMetadata.scopes_supported);\n if (capabilities) {\n return capabilities;\n }\n\n const issuer = await discoverAuthorizationServerIssuer(baseUrl);\n if (issuer) {\n const issuerCapabilities = await resolveCapabilitiesFromAuthorizationServer(issuer, resourceMetadata.scopes_supported);\n if (issuerCapabilities) return issuerCapabilities;\n }\n }\n\n const issuer = await discoverAuthorizationServerIssuer(normalizedBaseUrl);\n if (issuer) {\n const issuerCapabilities = await resolveCapabilitiesFromAuthorizationServer(issuer);\n if (issuerCapabilities) return issuerCapabilities;\n }\n\n // Strategy 2: Fall back to direct RFC 8414 discovery at resource origin\n // This handles same-domain OAuth (traditional setup)\n const origin = getOrigin(normalizedBaseUrl);\n const originCapabilities = await resolveCapabilitiesFromAuthorizationServer(origin);\n if (originCapabilities) return originCapabilities;\n\n // No OAuth metadata found\n return { supportsDcr: false };\n } catch (_error) {\n // Network error, invalid JSON, or other fetch failure\n // Gracefully degrade - assume no DCR support\n return { supportsDcr: false };\n }\n}\n"],"names":["normalizeUrl","discoverAuthorizationServerIssuer","discoverAuthorizationServerMetadata","discoverProtectedResourceMetadata","getOrigin","url","URL","origin","buildCapabilities","metadata","scopes","supportsDcr","registration_endpoint","capabilities","registrationEndpoint","authorization_endpoint","authorizationEndpoint","token_endpoint","tokenEndpoint","introspection_endpoint","introspectionEndpoint","length","scopes_supported","resolveCapabilitiesFromAuthorizationServer","authServerUrl","probeAuthCapabilities","baseUrl","normalizedBaseUrl","resourceMetadata","authorization_servers","issuer","issuerCapabilities","originCapabilities","_error"],"mappings":"AAAA;;;CAGC,GAED,SAASA,YAAY,QAAQ,sBAAsB;AACnD,SAASC,iCAAiC,EAAEC,mCAAmC,EAAEC,iCAAiC,QAAQ,yBAAyB;AAGnJ;;;;;;;;CAQC,GACD,SAASC,UAAUC,GAAW;IAC5B,IAAI;QACF,OAAO,IAAIC,IAAID,KAAKE,MAAM;IAC5B,EAAE,OAAM;QACN,sDAAsD;QACtD,OAAOF;IACT;AACF;AAEA;;;;;;;;;;;;;;;;;;CAkBC,GACD,SAASG,kBAAkBC,QAAqC,EAAEC,MAAiB;IACjF,MAAMC,cAAc,CAAC,CAACF,SAASG,qBAAqB;IACpD,MAAMC,eAAiC;QAAEF;IAAY;IAErD,IAAIF,SAASG,qBAAqB,EAAE;QAClCC,aAAaC,oBAAoB,GAAGL,SAASG,qBAAqB;IACpE;IACA,IAAIH,SAASM,sBAAsB,EAAE;QACnCF,aAAaG,qBAAqB,GAAGP,SAASM,sBAAsB;IACtE;IACA,IAAIN,SAASQ,cAAc,EAAEJ,aAAaK,aAAa,GAAGT,SAASQ,cAAc;IACjF,IAAIR,SAASU,sBAAsB,EAAE;QACnCN,aAAaO,qBAAqB,GAAGX,SAASU,sBAAsB;IACtE;IAEA,IAAIT,UAAUA,OAAOW,MAAM,GAAG,GAAG;QAC/BR,aAAaH,MAAM,GAAGA;IACxB,OAAO,IAAID,SAASa,gBAAgB,EAAE;QACpCT,aAAaH,MAAM,GAAGD,SAASa,gBAAgB;IACjD;IAEA,OAAOT;AACT;AAEA,eAAeU,2CAA2CC,aAAqB,EAAEd,MAAiB;IAChG,MAAMD,WAAW,MAAMP,oCAAoCsB;IAC3D,IAAI,CAACf,UAAU,OAAO;IACtB,OAAOD,kBAAkBC,UAAUC;AACrC;AAEA,OAAO,eAAee,sBAAsBC,OAAe;IACzD,IAAI;QACF,MAAMC,oBAAoB3B,aAAa0B;QACvC,iEAAiE;QACjE,oFAAoF;QACpF,MAAME,mBAAmB,MAAMzB,kCAAkCwB;QAEjE,IAAIC,oBAAoBA,iBAAiBC,qBAAqB,CAACR,MAAM,GAAG,GAAG;YACzE,+DAA+D;YAC/D,0DAA0D;YAC1D,MAAMG,gBAAgBI,iBAAiBC,qBAAqB,CAAC,EAAE;YAC/D,IAAI,CAACL,eAAe;gBAClB,4EAA4E;gBAC5E,OAAO;oBAAEb,aAAa;gBAAM;YAC9B;YACA,MAAME,eAAe,MAAMU,2CAA2CC,eAAeI,iBAAiBN,gBAAgB;YACtH,IAAIT,cAAc;gBAChB,OAAOA;YACT;YAEA,MAAMiB,SAAS,MAAM7B,kCAAkCyB;YACvD,IAAII,QAAQ;gBACV,MAAMC,qBAAqB,MAAMR,2CAA2CO,QAAQF,iBAAiBN,gBAAgB;gBACrH,IAAIS,oBAAoB,OAAOA;YACjC;QACF;QAEA,MAAMD,SAAS,MAAM7B,kCAAkC0B;QACvD,IAAIG,QAAQ;YACV,MAAMC,qBAAqB,MAAMR,2CAA2CO;YAC5E,IAAIC,oBAAoB,OAAOA;QACjC;QAEA,wEAAwE;QACxE,qDAAqD;QACrD,MAAMxB,SAASH,UAAUuB;QACzB,MAAMK,qBAAqB,MAAMT,2CAA2ChB;QAC5E,IAAIyB,oBAAoB,OAAOA;QAE/B,0BAA0B;QAC1B,OAAO;YAAErB,aAAa;QAAM;IAC9B,EAAE,OAAOsB,QAAQ;QACf,sDAAsD;QACtD,6CAA6C;QAC7C,OAAO;YAAEtB,aAAa;QAAM;IAC9B;AACF"}
@@ -3,6 +3,9 @@
3
3
  * Probes .well-known/oauth-protected-resource endpoint
4
4
  */
5
5
  import type { AuthorizationServerMetadata, ProtectedResourceMetadata } from './types.js';
6
+ /**
7
+ * Normalize a resource URL by stripping query/hash and trailing slashes.
8
+ */
6
9
  /**
7
10
  * Discover OAuth 2.0 Protected Resource Metadata (RFC 9728)
8
11
  * Probes .well-known/oauth-protected-resource endpoint
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * RFC 9728 Protected Resource Metadata Discovery
3
3
  * Probes .well-known/oauth-protected-resource endpoint
4
- */ /**
4
+ */ import { joinWellKnown, normalizeUrl } from '../lib/url-utils.js';
5
+ /**
5
6
  * Extract origin (protocol + host) from a URL
6
7
  * @param url - Full URL that may include a path
7
8
  * @returns Origin (e.g., "https://example.com") or original string if invalid URL
@@ -31,6 +32,8 @@
31
32
  }
32
33
  }
33
34
  /**
35
+ * Normalize a resource URL by stripping query/hash and trailing slashes.
36
+ */ /**
34
37
  * Discover OAuth 2.0 Protected Resource Metadata (RFC 9728)
35
38
  * Probes .well-known/oauth-protected-resource endpoint
36
39
  *
@@ -47,10 +50,27 @@
47
50
  * // Returns: { resource: "https://ai.todoist.net/mcp", authorization_servers: ["https://todoist.com"] }
48
51
  */ export async function discoverProtectedResourceMetadata(resourceUrl) {
49
52
  try {
50
- const headerMetadata = await discoverProtectedResourceMetadataFromHeader(resourceUrl);
53
+ const normalizedResourceUrl = normalizeUrl(resourceUrl);
54
+ const headerMetadata = await discoverProtectedResourceMetadataFromHeader(normalizedResourceUrl);
51
55
  if (headerMetadata) return headerMetadata;
52
- const origin = getOrigin(resourceUrl);
53
- const path = getPath(resourceUrl);
56
+ // Strategy 0: Try path-local well-known (supports path-prefixed deployments like /outlook)
57
+ const localWellKnownUrl = joinWellKnown(normalizedResourceUrl, '/.well-known/oauth-protected-resource');
58
+ try {
59
+ const response = await fetch(localWellKnownUrl, {
60
+ method: 'GET',
61
+ headers: {
62
+ Accept: 'application/json',
63
+ Connection: 'close'
64
+ }
65
+ });
66
+ if (response.ok) {
67
+ return await response.json();
68
+ }
69
+ } catch {
70
+ // Continue to origin-based discovery
71
+ }
72
+ const origin = getOrigin(normalizedResourceUrl);
73
+ const path = getPath(normalizedResourceUrl);
54
74
  // Strategy 1: Try root location (REQUIRED by RFC 9728)
55
75
  const rootUrl = `${origin}/.well-known/oauth-protected-resource`;
56
76
  try {
@@ -64,7 +84,7 @@
64
84
  if (response.ok) {
65
85
  const metadata = await response.json();
66
86
  // Check if the discovered resource matches what we're looking for
67
- if (metadata.resource === resourceUrl) {
87
+ if (metadata.resource === normalizedResourceUrl) {
68
88
  return metadata;
69
89
  }
70
90
  // If there's no path component, return root metadata
@@ -74,7 +94,7 @@
74
94
  }
75
95
  // If requested URL starts with metadata.resource, the root metadata applies to sub-paths
76
96
  // (e.g., looking for http://example.com/api/v1/mcp, found http://example.com)
77
- if (resourceUrl.startsWith(metadata.resource)) {
97
+ if (normalizedResourceUrl.startsWith(metadata.resource)) {
78
98
  // Still try sub-path location to see if there's more specific metadata
79
99
  // But save root metadata as fallback
80
100
  const rootMetadata = metadata;
@@ -181,7 +201,19 @@ async function discoverProtectedResourceMetadataFromHeader(resourceUrl) {
181
201
  * // Returns: { issuer: "https://todoist.com", authorization_endpoint: "...", ... }
182
202
  */ export async function discoverAuthorizationServerMetadata(authServerUrl) {
183
203
  try {
184
- const origin = getOrigin(authServerUrl);
204
+ const normalizedAuthServerUrl = normalizeUrl(authServerUrl);
205
+ const localWellKnownUrl = joinWellKnown(normalizedAuthServerUrl, '/.well-known/oauth-authorization-server');
206
+ const localResponse = await fetch(localWellKnownUrl, {
207
+ method: 'GET',
208
+ headers: {
209
+ Accept: 'application/json',
210
+ Connection: 'close'
211
+ }
212
+ });
213
+ if (localResponse.ok) {
214
+ return await localResponse.json();
215
+ }
216
+ const origin = getOrigin(normalizedAuthServerUrl);
185
217
  const wellKnownUrl = `${origin}/.well-known/oauth-authorization-server`;
186
218
  const response = await fetch(wellKnownUrl, {
187
219
  method: 'GET',
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/Projects/mcp-z/client/src/auth/rfc9728-discovery.ts"],"sourcesContent":["/**\n * RFC 9728 Protected Resource Metadata Discovery\n * Probes .well-known/oauth-protected-resource endpoint\n */\n\nimport type { AuthorizationServerMetadata, ProtectedResourceMetadata } from './types.ts';\n\n/**\n * Extract origin (protocol + host) from a URL\n * @param url - Full URL that may include a path\n * @returns Origin (e.g., \"https://example.com\") or original string if invalid URL\n *\n * @example\n * getOrigin('https://example.com/mcp') // → 'https://example.com'\n * getOrigin('http://localhost:9999/api/v1/mcp') // → 'http://localhost:9999'\n */\nfunction getOrigin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n // Invalid URL - return as-is for graceful degradation\n return url;\n }\n}\n\n/**\n * Extract path from a URL (without origin)\n * @param url - Full URL\n * @returns Path component (e.g., \"/mcp\", \"/api/v1/mcp\") or empty string if no path\n */\nfunction getPath(url: string): string {\n try {\n const parsed = new URL(url);\n // pathname includes leading slash, e.g., \"/mcp\"\n return parsed.pathname === '/' ? '' : parsed.pathname;\n } catch {\n return '';\n }\n}\n\n/**\n * Discover OAuth 2.0 Protected Resource Metadata (RFC 9728)\n * Probes .well-known/oauth-protected-resource endpoint\n *\n * Discovery Strategy:\n * 1. Try origin root: {origin}/.well-known/oauth-protected-resource\n * 2. If 404, try sub-path: {origin}/.well-known/oauth-protected-resource{path}\n *\n * @param resourceUrl - URL of the protected resource (e.g., https://ai.todoist.net/mcp)\n * @returns ProtectedResourceMetadata if discovered, null otherwise\n *\n * @example\n * // Todoist case: MCP at ai.todoist.net/mcp, OAuth at todoist.com\n * const metadata = await discoverProtectedResourceMetadata('https://ai.todoist.net/mcp');\n * // Returns: { resource: \"https://ai.todoist.net/mcp\", authorization_servers: [\"https://todoist.com\"] }\n */\nexport async function discoverProtectedResourceMetadata(resourceUrl: string): Promise<ProtectedResourceMetadata | null> {\n try {\n const headerMetadata = await discoverProtectedResourceMetadataFromHeader(resourceUrl);\n if (headerMetadata) return headerMetadata;\n\n const origin = getOrigin(resourceUrl);\n const path = getPath(resourceUrl);\n\n // Strategy 1: Try root location (REQUIRED by RFC 9728)\n const rootUrl = `${origin}/.well-known/oauth-protected-resource`;\n\n try {\n const response = await fetch(rootUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (response.ok) {\n const metadata = (await response.json()) as ProtectedResourceMetadata;\n // Check if the discovered resource matches what we're looking for\n if (metadata.resource === resourceUrl) {\n return metadata;\n }\n // If there's no path component, return root metadata\n // (e.g., looking for http://example.com and found it)\n if (!path) {\n return metadata;\n }\n // If requested URL starts with metadata.resource, the root metadata applies to sub-paths\n // (e.g., looking for http://example.com/api/v1/mcp, found http://example.com)\n if (resourceUrl.startsWith(metadata.resource)) {\n // Still try sub-path location to see if there's more specific metadata\n // But save root metadata as fallback\n const rootMetadata = metadata;\n\n // Try sub-path location for more specific metadata\n const subPathUrl = `${origin}/.well-known/oauth-protected-resource${path}`;\n try {\n const subPathResponse = await fetch(subPathUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n if (subPathResponse.ok) {\n return (await subPathResponse.json()) as ProtectedResourceMetadata;\n }\n } catch {\n // Sub-path failed, use root metadata\n }\n\n // Return root metadata as it applies to this resource\n return rootMetadata;\n }\n // Otherwise, try sub-path location before giving up\n }\n } catch {\n // Continue to sub-path location\n }\n\n // Strategy 2: Try sub-path location (MCP spec extension)\n // Only try if there's a path component\n if (path) {\n const subPathUrl = `${origin}/.well-known/oauth-protected-resource${path}`;\n\n try {\n const response = await fetch(subPathUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (response.ok) {\n return (await response.json()) as ProtectedResourceMetadata;\n }\n } catch {\n // Fall through to return null\n }\n }\n\n // Neither location found or resource didn't match\n return null;\n } catch (_error) {\n // Network error, invalid URL, or other failure\n return null;\n }\n}\n\nasync function discoverProtectedResourceMetadataFromHeader(resourceUrl: string): Promise<ProtectedResourceMetadata | null> {\n try {\n const response = await fetch(resourceUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n let header = response.headers.get('www-authenticate');\n if (!header) {\n const postResponse = await fetch(resourceUrl, {\n method: 'POST',\n headers: { Accept: 'application/json', Connection: 'close', 'Content-Type': 'application/json' },\n body: '{}',\n });\n header = postResponse.headers.get('www-authenticate');\n }\n\n if (!header) return null;\n\n const match = header.match(/resource_metadata=\"([^\"]+)\"/i);\n if (!match || !match[1]) return null;\n\n const metadataUrl = match[1];\n const metadataResponse = await fetch(metadataUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (!metadataResponse.ok) {\n return null;\n }\n\n return (await metadataResponse.json()) as ProtectedResourceMetadata;\n } catch (_error) {\n return null;\n }\n}\n\n/**\n * Discover OAuth 2.0 Authorization Server Metadata (RFC 8414)\n * Probes .well-known/oauth-authorization-server endpoint\n *\n * @param authServerUrl - URL of the authorization server (typically from RFC 9728 discovery)\n * @returns AuthorizationServerMetadata if discovered, null otherwise\n *\n * @example\n * const metadata = await discoverAuthorizationServerMetadata('https://todoist.com');\n * // Returns: { issuer: \"https://todoist.com\", authorization_endpoint: \"...\", ... }\n */\nexport async function discoverAuthorizationServerMetadata(authServerUrl: string): Promise<AuthorizationServerMetadata | null> {\n try {\n const origin = getOrigin(authServerUrl);\n const wellKnownUrl = `${origin}/.well-known/oauth-authorization-server`;\n\n const response = await fetch(wellKnownUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (!response.ok) {\n return null;\n }\n\n return (await response.json()) as AuthorizationServerMetadata;\n } catch (_error) {\n return null;\n }\n}\n\n/**\n * Discover OAuth Authorization Server Issuer from resource response (RFC 9207)\n *\n * @param resourceUrl - URL of the protected resource\n * @returns Issuer URL if present in WWW-Authenticate header, null otherwise\n */\nexport async function discoverAuthorizationServerIssuer(resourceUrl: string): Promise<string | null> {\n try {\n const response = await fetch(resourceUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n const header = response.headers.get('www-authenticate');\n if (!header) return null;\n\n const match = header.match(/(?:authorization_server|issuer)=\"([^\"]+)\"/i);\n if (!match) return null;\n\n return match[1] ?? null;\n } catch (_error) {\n return null;\n }\n}\n"],"names":["getOrigin","url","URL","origin","getPath","parsed","pathname","discoverProtectedResourceMetadata","resourceUrl","headerMetadata","discoverProtectedResourceMetadataFromHeader","path","rootUrl","response","fetch","method","headers","Accept","Connection","ok","metadata","json","resource","startsWith","rootMetadata","subPathUrl","subPathResponse","_error","header","get","postResponse","body","match","metadataUrl","metadataResponse","discoverAuthorizationServerMetadata","authServerUrl","wellKnownUrl","discoverAuthorizationServerIssuer"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/Projects/mcp-z/client/src/auth/rfc9728-discovery.ts"],"sourcesContent":["/**\n * RFC 9728 Protected Resource Metadata Discovery\n * Probes .well-known/oauth-protected-resource endpoint\n */\n\nimport { joinWellKnown, normalizeUrl } from '../lib/url-utils.ts';\nimport type { AuthorizationServerMetadata, ProtectedResourceMetadata } from './types.ts';\n\n/**\n * Extract origin (protocol + host) from a URL\n * @param url - Full URL that may include a path\n * @returns Origin (e.g., \"https://example.com\") or original string if invalid URL\n *\n * @example\n * getOrigin('https://example.com/mcp') // → 'https://example.com'\n * getOrigin('http://localhost:9999/api/v1/mcp') // → 'http://localhost:9999'\n */\nfunction getOrigin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n // Invalid URL - return as-is for graceful degradation\n return url;\n }\n}\n\n/**\n * Extract path from a URL (without origin)\n * @param url - Full URL\n * @returns Path component (e.g., \"/mcp\", \"/api/v1/mcp\") or empty string if no path\n */\nfunction getPath(url: string): string {\n try {\n const parsed = new URL(url);\n // pathname includes leading slash, e.g., \"/mcp\"\n return parsed.pathname === '/' ? '' : parsed.pathname;\n } catch {\n return '';\n }\n}\n\n/**\n * Normalize a resource URL by stripping query/hash and trailing slashes.\n */\n/**\n * Discover OAuth 2.0 Protected Resource Metadata (RFC 9728)\n * Probes .well-known/oauth-protected-resource endpoint\n *\n * Discovery Strategy:\n * 1. Try origin root: {origin}/.well-known/oauth-protected-resource\n * 2. If 404, try sub-path: {origin}/.well-known/oauth-protected-resource{path}\n *\n * @param resourceUrl - URL of the protected resource (e.g., https://ai.todoist.net/mcp)\n * @returns ProtectedResourceMetadata if discovered, null otherwise\n *\n * @example\n * // Todoist case: MCP at ai.todoist.net/mcp, OAuth at todoist.com\n * const metadata = await discoverProtectedResourceMetadata('https://ai.todoist.net/mcp');\n * // Returns: { resource: \"https://ai.todoist.net/mcp\", authorization_servers: [\"https://todoist.com\"] }\n */\nexport async function discoverProtectedResourceMetadata(resourceUrl: string): Promise<ProtectedResourceMetadata | null> {\n try {\n const normalizedResourceUrl = normalizeUrl(resourceUrl);\n const headerMetadata = await discoverProtectedResourceMetadataFromHeader(normalizedResourceUrl);\n if (headerMetadata) return headerMetadata;\n\n // Strategy 0: Try path-local well-known (supports path-prefixed deployments like /outlook)\n const localWellKnownUrl = joinWellKnown(normalizedResourceUrl, '/.well-known/oauth-protected-resource');\n try {\n const response = await fetch(localWellKnownUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n if (response.ok) {\n return (await response.json()) as ProtectedResourceMetadata;\n }\n } catch {\n // Continue to origin-based discovery\n }\n\n const origin = getOrigin(normalizedResourceUrl);\n const path = getPath(normalizedResourceUrl);\n\n // Strategy 1: Try root location (REQUIRED by RFC 9728)\n const rootUrl = `${origin}/.well-known/oauth-protected-resource`;\n\n try {\n const response = await fetch(rootUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (response.ok) {\n const metadata = (await response.json()) as ProtectedResourceMetadata;\n // Check if the discovered resource matches what we're looking for\n if (metadata.resource === normalizedResourceUrl) {\n return metadata;\n }\n // If there's no path component, return root metadata\n // (e.g., looking for http://example.com and found it)\n if (!path) {\n return metadata;\n }\n // If requested URL starts with metadata.resource, the root metadata applies to sub-paths\n // (e.g., looking for http://example.com/api/v1/mcp, found http://example.com)\n if (normalizedResourceUrl.startsWith(metadata.resource)) {\n // Still try sub-path location to see if there's more specific metadata\n // But save root metadata as fallback\n const rootMetadata = metadata;\n\n // Try sub-path location for more specific metadata\n const subPathUrl = `${origin}/.well-known/oauth-protected-resource${path}`;\n try {\n const subPathResponse = await fetch(subPathUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n if (subPathResponse.ok) {\n return (await subPathResponse.json()) as ProtectedResourceMetadata;\n }\n } catch {\n // Sub-path failed, use root metadata\n }\n\n // Return root metadata as it applies to this resource\n return rootMetadata;\n }\n // Otherwise, try sub-path location before giving up\n }\n } catch {\n // Continue to sub-path location\n }\n\n // Strategy 2: Try sub-path location (MCP spec extension)\n // Only try if there's a path component\n if (path) {\n const subPathUrl = `${origin}/.well-known/oauth-protected-resource${path}`;\n\n try {\n const response = await fetch(subPathUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (response.ok) {\n return (await response.json()) as ProtectedResourceMetadata;\n }\n } catch {\n // Fall through to return null\n }\n }\n\n // Neither location found or resource didn't match\n return null;\n } catch (_error) {\n // Network error, invalid URL, or other failure\n return null;\n }\n}\n\nasync function discoverProtectedResourceMetadataFromHeader(resourceUrl: string): Promise<ProtectedResourceMetadata | null> {\n try {\n const response = await fetch(resourceUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n let header = response.headers.get('www-authenticate');\n if (!header) {\n const postResponse = await fetch(resourceUrl, {\n method: 'POST',\n headers: { Accept: 'application/json', Connection: 'close', 'Content-Type': 'application/json' },\n body: '{}',\n });\n header = postResponse.headers.get('www-authenticate');\n }\n\n if (!header) return null;\n\n const match = header.match(/resource_metadata=\"([^\"]+)\"/i);\n if (!match || !match[1]) return null;\n\n const metadataUrl = match[1];\n const metadataResponse = await fetch(metadataUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (!metadataResponse.ok) {\n return null;\n }\n\n return (await metadataResponse.json()) as ProtectedResourceMetadata;\n } catch (_error) {\n return null;\n }\n}\n\n/**\n * Discover OAuth 2.0 Authorization Server Metadata (RFC 8414)\n * Probes .well-known/oauth-authorization-server endpoint\n *\n * @param authServerUrl - URL of the authorization server (typically from RFC 9728 discovery)\n * @returns AuthorizationServerMetadata if discovered, null otherwise\n *\n * @example\n * const metadata = await discoverAuthorizationServerMetadata('https://todoist.com');\n * // Returns: { issuer: \"https://todoist.com\", authorization_endpoint: \"...\", ... }\n */\nexport async function discoverAuthorizationServerMetadata(authServerUrl: string): Promise<AuthorizationServerMetadata | null> {\n try {\n const normalizedAuthServerUrl = normalizeUrl(authServerUrl);\n const localWellKnownUrl = joinWellKnown(normalizedAuthServerUrl, '/.well-known/oauth-authorization-server');\n const localResponse = await fetch(localWellKnownUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (localResponse.ok) {\n return (await localResponse.json()) as AuthorizationServerMetadata;\n }\n\n const origin = getOrigin(normalizedAuthServerUrl);\n const wellKnownUrl = `${origin}/.well-known/oauth-authorization-server`;\n\n const response = await fetch(wellKnownUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (!response.ok) {\n return null;\n }\n\n return (await response.json()) as AuthorizationServerMetadata;\n } catch (_error) {\n return null;\n }\n}\n\n/**\n * Discover OAuth Authorization Server Issuer from resource response (RFC 9207)\n *\n * @param resourceUrl - URL of the protected resource\n * @returns Issuer URL if present in WWW-Authenticate header, null otherwise\n */\nexport async function discoverAuthorizationServerIssuer(resourceUrl: string): Promise<string | null> {\n try {\n const response = await fetch(resourceUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n const header = response.headers.get('www-authenticate');\n if (!header) return null;\n\n const match = header.match(/(?:authorization_server|issuer)=\"([^\"]+)\"/i);\n if (!match) return null;\n\n return match[1] ?? null;\n } catch (_error) {\n return null;\n }\n}\n"],"names":["joinWellKnown","normalizeUrl","getOrigin","url","URL","origin","getPath","parsed","pathname","discoverProtectedResourceMetadata","resourceUrl","normalizedResourceUrl","headerMetadata","discoverProtectedResourceMetadataFromHeader","localWellKnownUrl","response","fetch","method","headers","Accept","Connection","ok","json","path","rootUrl","metadata","resource","startsWith","rootMetadata","subPathUrl","subPathResponse","_error","header","get","postResponse","body","match","metadataUrl","metadataResponse","discoverAuthorizationServerMetadata","authServerUrl","normalizedAuthServerUrl","localResponse","wellKnownUrl","discoverAuthorizationServerIssuer"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare function normalizeUrl(input: string): string;
2
+ export declare function joinWellKnown(baseUrl: string, suffix: string): string;
@@ -0,0 +1,14 @@
1
+ export function normalizeUrl(input) {
2
+ try {
3
+ const url = new URL(input);
4
+ url.search = '';
5
+ url.hash = '';
6
+ url.pathname = url.pathname.replace(/\/+$/, '');
7
+ return url.origin + url.pathname;
8
+ } catch {
9
+ return input.replace(/\/+$/, '');
10
+ }
11
+ }
12
+ export function joinWellKnown(baseUrl, suffix) {
13
+ return `${normalizeUrl(baseUrl)}${suffix}`;
14
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/kevin/Dev/Projects/mcp-z/client/src/lib/url-utils.ts"],"sourcesContent":["export function normalizeUrl(input: string): string {\n try {\n const url = new URL(input);\n url.search = '';\n url.hash = '';\n url.pathname = url.pathname.replace(/\\/+$/, '');\n return url.origin + url.pathname;\n } catch {\n return input.replace(/\\/+$/, '');\n }\n}\n\nexport function joinWellKnown(baseUrl: string, suffix: string): string {\n return `${normalizeUrl(baseUrl)}${suffix}`;\n}\n"],"names":["normalizeUrl","input","url","URL","search","hash","pathname","replace","origin","joinWellKnown","baseUrl","suffix"],"mappings":"AAAA,OAAO,SAASA,aAAaC,KAAa;IACxC,IAAI;QACF,MAAMC,MAAM,IAAIC,IAAIF;QACpBC,IAAIE,MAAM,GAAG;QACbF,IAAIG,IAAI,GAAG;QACXH,IAAII,QAAQ,GAAGJ,IAAII,QAAQ,CAACC,OAAO,CAAC,QAAQ;QAC5C,OAAOL,IAAIM,MAAM,GAAGN,IAAII,QAAQ;IAClC,EAAE,OAAM;QACN,OAAOL,MAAMM,OAAO,CAAC,QAAQ;IAC/B;AACF;AAEA,OAAO,SAASE,cAAcC,OAAe,EAAEC,MAAc;IAC3D,OAAO,GAAGX,aAAaU,WAAWC,QAAQ;AAC5C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-z/client",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Programmatic MCP client library for Node.js - connect, discover, and call tools on Model Context Protocol servers.",
5
5
  "keywords": [
6
6
  "mcp",