@mcp-z/client 2.2.4 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +12 -1
  2. package/dist/cjs/auth/capability-discovery.d.cts +3 -1
  3. package/dist/cjs/auth/capability-discovery.d.ts +3 -1
  4. package/dist/cjs/auth/capability-discovery.js +46 -32
  5. package/dist/cjs/auth/capability-discovery.js.map +1 -1
  6. package/dist/cjs/auth/discovery-fetch.d.cts +2 -0
  7. package/dist/cjs/auth/discovery-fetch.d.ts +2 -0
  8. package/dist/cjs/auth/discovery-fetch.js +85 -31
  9. package/dist/cjs/auth/discovery-fetch.js.map +1 -1
  10. package/dist/cjs/auth/interactive-oauth-flow.d.cts +1 -1
  11. package/dist/cjs/auth/interactive-oauth-flow.d.ts +1 -1
  12. package/dist/cjs/auth/interactive-oauth-flow.js +13 -6
  13. package/dist/cjs/auth/interactive-oauth-flow.js.map +1 -1
  14. package/dist/cjs/auth/oauth-callback-listener.d.cts +1 -1
  15. package/dist/cjs/auth/oauth-callback-listener.d.ts +1 -1
  16. package/dist/cjs/auth/oauth-callback-listener.js +38 -2
  17. package/dist/cjs/auth/oauth-callback-listener.js.map +1 -1
  18. package/dist/cjs/auth/rfc9728-discovery.d.cts +7 -2
  19. package/dist/cjs/auth/rfc9728-discovery.d.ts +7 -2
  20. package/dist/cjs/auth/rfc9728-discovery.js +120 -88
  21. package/dist/cjs/auth/rfc9728-discovery.js.map +1 -1
  22. package/dist/cjs/auth/types.d.cts +4 -0
  23. package/dist/cjs/auth/types.d.ts +4 -0
  24. package/dist/cjs/auth/types.js.map +1 -1
  25. package/dist/cjs/connection/connect-client.d.cts +9 -1
  26. package/dist/cjs/connection/connect-client.d.ts +9 -1
  27. package/dist/cjs/connection/connect-client.js +282 -75
  28. package/dist/cjs/connection/connect-client.js.map +1 -1
  29. package/dist/cjs/connection/existing-process-transport.d.cts +18 -18
  30. package/dist/cjs/connection/existing-process-transport.d.ts +18 -18
  31. package/dist/cjs/connection/existing-process-transport.js +187 -82
  32. package/dist/cjs/connection/existing-process-transport.js.map +1 -1
  33. package/dist/cjs/connection/wait-for-http-ready.d.cts +1 -1
  34. package/dist/cjs/connection/wait-for-http-ready.d.ts +1 -1
  35. package/dist/cjs/connection/wait-for-http-ready.js +30 -5
  36. package/dist/cjs/connection/wait-for-http-ready.js.map +1 -1
  37. package/dist/cjs/dcr/dcr-authenticator.d.cts +1 -1
  38. package/dist/cjs/dcr/dcr-authenticator.d.ts +1 -1
  39. package/dist/cjs/dcr/dcr-authenticator.js +29 -18
  40. package/dist/cjs/dcr/dcr-authenticator.js.map +1 -1
  41. package/dist/cjs/dcr/dynamic-client-registrar.js +2 -1
  42. package/dist/cjs/dcr/dynamic-client-registrar.js.map +1 -1
  43. package/dist/cjs/index.d.cts +1 -1
  44. package/dist/cjs/index.d.ts +1 -1
  45. package/dist/cjs/index.js.map +1 -1
  46. package/dist/cjs/spawn/owned-process-tree.d.cts +12 -0
  47. package/dist/cjs/spawn/owned-process-tree.d.ts +12 -0
  48. package/dist/cjs/spawn/owned-process-tree.js +93 -0
  49. package/dist/cjs/spawn/owned-process-tree.js.map +1 -0
  50. package/dist/cjs/spawn/spawn-server.d.cts +8 -32
  51. package/dist/cjs/spawn/spawn-server.d.ts +8 -32
  52. package/dist/cjs/spawn/spawn-server.js +398 -212
  53. package/dist/cjs/spawn/spawn-server.js.map +1 -1
  54. package/dist/cjs/spawn/spawn-servers.d.cts +18 -8
  55. package/dist/cjs/spawn/spawn-servers.d.ts +18 -8
  56. package/dist/cjs/spawn/spawn-servers.js +342 -113
  57. package/dist/cjs/spawn/spawn-servers.js.map +1 -1
  58. package/dist/cjs/types.d.cts +1 -1
  59. package/dist/cjs/types.d.ts +1 -1
  60. package/dist/cjs/types.js.map +1 -1
  61. package/dist/esm/auth/capability-discovery.d.ts +3 -1
  62. package/dist/esm/auth/capability-discovery.js +21 -11
  63. package/dist/esm/auth/capability-discovery.js.map +1 -1
  64. package/dist/esm/auth/discovery-fetch.d.ts +2 -0
  65. package/dist/esm/auth/discovery-fetch.js +64 -23
  66. package/dist/esm/auth/discovery-fetch.js.map +1 -1
  67. package/dist/esm/auth/interactive-oauth-flow.d.ts +1 -1
  68. package/dist/esm/auth/interactive-oauth-flow.js +13 -6
  69. package/dist/esm/auth/interactive-oauth-flow.js.map +1 -1
  70. package/dist/esm/auth/oauth-callback-listener.d.ts +1 -1
  71. package/dist/esm/auth/oauth-callback-listener.js +32 -2
  72. package/dist/esm/auth/oauth-callback-listener.js.map +1 -1
  73. package/dist/esm/auth/rfc9728-discovery.d.ts +7 -2
  74. package/dist/esm/auth/rfc9728-discovery.js +40 -16
  75. package/dist/esm/auth/rfc9728-discovery.js.map +1 -1
  76. package/dist/esm/auth/types.d.ts +4 -0
  77. package/dist/esm/auth/types.js.map +1 -1
  78. package/dist/esm/connection/connect-client.d.ts +9 -1
  79. package/dist/esm/connection/connect-client.js +97 -35
  80. package/dist/esm/connection/connect-client.js.map +1 -1
  81. package/dist/esm/connection/existing-process-transport.d.ts +18 -18
  82. package/dist/esm/connection/existing-process-transport.js +132 -70
  83. package/dist/esm/connection/existing-process-transport.js.map +1 -1
  84. package/dist/esm/connection/wait-for-http-ready.d.ts +1 -1
  85. package/dist/esm/connection/wait-for-http-ready.js +19 -4
  86. package/dist/esm/connection/wait-for-http-ready.js.map +1 -1
  87. package/dist/esm/dcr/dcr-authenticator.d.ts +1 -1
  88. package/dist/esm/dcr/dcr-authenticator.js +26 -15
  89. package/dist/esm/dcr/dcr-authenticator.js.map +1 -1
  90. package/dist/esm/dcr/dynamic-client-registrar.js +2 -1
  91. package/dist/esm/dcr/dynamic-client-registrar.js.map +1 -1
  92. package/dist/esm/index.d.ts +1 -1
  93. package/dist/esm/index.js.map +1 -1
  94. package/dist/esm/spawn/owned-process-tree.d.ts +12 -0
  95. package/dist/esm/spawn/owned-process-tree.js +39 -0
  96. package/dist/esm/spawn/owned-process-tree.js.map +1 -0
  97. package/dist/esm/spawn/spawn-server.d.ts +8 -32
  98. package/dist/esm/spawn/spawn-server.js +224 -130
  99. package/dist/esm/spawn/spawn-server.js.map +1 -1
  100. package/dist/esm/spawn/spawn-servers.d.ts +18 -8
  101. package/dist/esm/spawn/spawn-servers.js +180 -55
  102. package/dist/esm/spawn/spawn-servers.js.map +1 -1
  103. package/dist/esm/types.d.ts +1 -1
  104. package/dist/esm/types.js.map +1 -1
  105. package/package.json +8 -5
  106. package/schemas/servers.d.ts +24 -0
  107. package/schemas/servers.schema.json +31 -0
@@ -2,7 +2,7 @@
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
- import { discoveryFetch, isLoopbackUrl, readDiscoveryJson } from './discovery-fetch.js';
5
+ import { discoveryFetch, isLoopbackUrl, readDiscoveryJson, throwIfAborted } from './discovery-fetch.js';
6
6
  /** Returns `url`'s origin (protocol + host), or the original string if it doesn't parse. */ function getOrigin(url) {
7
7
  try {
8
8
  return new URL(url).origin;
@@ -24,13 +24,15 @@ import { discoveryFetch, isLoopbackUrl, readDiscoveryJson } from './discovery-fe
24
24
  *
25
25
  * @param resourceUrl - URL of the protected resource (e.g. `https://ai.todoist.net/mcp`).
26
26
  * @returns Discovered metadata, or `null` if none is found.
27
- */ export async function discoverProtectedResourceMetadata(resourceUrl) {
27
+ */ export async function discoverProtectedResourceMetadata(resourceUrl, options = {}) {
28
+ const { signal } = options;
28
29
  try {
30
+ throwIfAborted(signal);
29
31
  const normalizedResourceUrl = normalizeUrl(resourceUrl);
30
32
  // resourceUrl is the server the caller configured (never remote-supplied);
31
33
  // its loopback-ness is the trust signal every fetch below relies on.
32
34
  const allowLoopback = isLoopbackUrl(normalizedResourceUrl);
33
- const headerMetadata = await discoverProtectedResourceMetadataFromHeader(normalizedResourceUrl, allowLoopback);
35
+ const headerMetadata = await discoverProtectedResourceMetadataFromHeader(normalizedResourceUrl, allowLoopback, signal);
34
36
  if (headerMetadata) return headerMetadata;
35
37
  // Strategy 0: Try path-local well-known (supports path-prefixed deployments like /outlook)
36
38
  const localWellKnownUrl = joinWellKnown(normalizedResourceUrl, '/.well-known/oauth-protected-resource');
@@ -40,7 +42,8 @@ import { discoveryFetch, isLoopbackUrl, readDiscoveryJson } from './discovery-fe
40
42
  headers: {
41
43
  Accept: 'application/json',
42
44
  Connection: 'close'
43
- }
45
+ },
46
+ signal
44
47
  }, 'protected resource metadata (path-local)', {
45
48
  allowLoopback
46
49
  });
@@ -48,6 +51,7 @@ import { discoveryFetch, isLoopbackUrl, readDiscoveryJson } from './discovery-fe
48
51
  return await readDiscoveryJson(response, 'protected resource metadata (path-local)');
49
52
  }
50
53
  } catch {
54
+ throwIfAborted(signal);
51
55
  // Continue to origin-based discovery
52
56
  }
53
57
  const origin = getOrigin(normalizedResourceUrl);
@@ -60,7 +64,8 @@ import { discoveryFetch, isLoopbackUrl, readDiscoveryJson } from './discovery-fe
60
64
  headers: {
61
65
  Accept: 'application/json',
62
66
  Connection: 'close'
63
- }
67
+ },
68
+ signal
64
69
  }, 'protected resource metadata (root)', {
65
70
  allowLoopback
66
71
  });
@@ -89,7 +94,8 @@ import { discoveryFetch, isLoopbackUrl, readDiscoveryJson } from './discovery-fe
89
94
  headers: {
90
95
  Accept: 'application/json',
91
96
  Connection: 'close'
92
- }
97
+ },
98
+ signal
93
99
  }, 'protected resource metadata (sub-path)', {
94
100
  allowLoopback
95
101
  });
@@ -97,6 +103,7 @@ import { discoveryFetch, isLoopbackUrl, readDiscoveryJson } from './discovery-fe
97
103
  return await readDiscoveryJson(subPathResponse, 'protected resource metadata (sub-path)');
98
104
  }
99
105
  } catch {
106
+ throwIfAborted(signal);
100
107
  // Sub-path failed, use root metadata
101
108
  }
102
109
  // Return root metadata as it applies to this resource
@@ -105,6 +112,7 @@ import { discoveryFetch, isLoopbackUrl, readDiscoveryJson } from './discovery-fe
105
112
  // Otherwise, try sub-path location before giving up
106
113
  }
107
114
  } catch {
115
+ throwIfAborted(signal);
108
116
  // Continue to sub-path location
109
117
  }
110
118
  // Strategy 2: Try sub-path location (MCP spec extension)
@@ -117,7 +125,8 @@ import { discoveryFetch, isLoopbackUrl, readDiscoveryJson } from './discovery-fe
117
125
  headers: {
118
126
  Accept: 'application/json',
119
127
  Connection: 'close'
120
- }
128
+ },
129
+ signal
121
130
  }, 'protected resource metadata (sub-path)', {
122
131
  allowLoopback
123
132
  });
@@ -125,24 +134,28 @@ import { discoveryFetch, isLoopbackUrl, readDiscoveryJson } from './discovery-fe
125
134
  return await readDiscoveryJson(response, 'protected resource metadata (sub-path)');
126
135
  }
127
136
  } catch {
137
+ throwIfAborted(signal);
128
138
  // Fall through to return null
129
139
  }
130
140
  }
131
141
  // Neither location found or resource didn't match
132
142
  return null;
133
143
  } catch (_error) {
144
+ throwIfAborted(signal);
134
145
  // Network error, invalid URL, or other failure
135
146
  return null;
136
147
  }
137
148
  }
138
- async function discoverProtectedResourceMetadataFromHeader(resourceUrl, allowLoopback) {
149
+ async function discoverProtectedResourceMetadataFromHeader(resourceUrl, allowLoopback, signal) {
139
150
  try {
151
+ throwIfAborted(signal);
140
152
  const response = await fetch(resourceUrl, {
141
153
  method: 'GET',
142
154
  headers: {
143
155
  Accept: 'application/json',
144
156
  Connection: 'close'
145
- }
157
+ },
158
+ signal
146
159
  });
147
160
  let header = response.headers.get('www-authenticate');
148
161
  if (!header) {
@@ -153,7 +166,8 @@ async function discoverProtectedResourceMetadataFromHeader(resourceUrl, allowLoo
153
166
  Connection: 'close',
154
167
  'Content-Type': 'application/json'
155
168
  },
156
- body: '{}'
169
+ body: '{}',
170
+ signal
157
171
  });
158
172
  header = postResponse.headers.get('www-authenticate');
159
173
  }
@@ -168,7 +182,8 @@ async function discoverProtectedResourceMetadataFromHeader(resourceUrl, allowLoo
168
182
  headers: {
169
183
  Accept: 'application/json',
170
184
  Connection: 'close'
171
- }
185
+ },
186
+ signal
172
187
  }, 'resource_metadata URL', {
173
188
  allowLoopback
174
189
  });
@@ -177,6 +192,7 @@ async function discoverProtectedResourceMetadataFromHeader(resourceUrl, allowLoo
177
192
  }
178
193
  return await readDiscoveryJson(metadataResponse, 'resource_metadata URL');
179
194
  } catch (_error) {
195
+ throwIfAborted(signal);
180
196
  return null;
181
197
  }
182
198
  }
@@ -188,8 +204,9 @@ async function discoverProtectedResourceMetadataFromHeader(resourceUrl, allowLoo
188
204
  * @param options.allowLoopback - Loopback trust grant computed from the server the caller is actually talking to, never from `authServerUrl` itself. Defaults to `false`.
189
205
  * @returns Discovered metadata, or `null` if none is found.
190
206
  */ export async function discoverAuthorizationServerMetadata(authServerUrl, options = {}) {
191
- const { allowLoopback = false } = options;
207
+ const { allowLoopback = false, signal } = options;
192
208
  try {
209
+ throwIfAborted(signal);
193
210
  const normalizedAuthServerUrl = normalizeUrl(authServerUrl);
194
211
  const localWellKnownUrl = joinWellKnown(normalizedAuthServerUrl, '/.well-known/oauth-authorization-server');
195
212
  const localResponse = await discoveryFetch(localWellKnownUrl, {
@@ -197,7 +214,8 @@ async function discoverProtectedResourceMetadataFromHeader(resourceUrl, allowLoo
197
214
  headers: {
198
215
  Accept: 'application/json',
199
216
  Connection: 'close'
200
- }
217
+ },
218
+ signal
201
219
  }, 'authorization server metadata (path-local)', {
202
220
  allowLoopback
203
221
  });
@@ -211,7 +229,8 @@ async function discoverProtectedResourceMetadataFromHeader(resourceUrl, allowLoo
211
229
  headers: {
212
230
  Accept: 'application/json',
213
231
  Connection: 'close'
214
- }
232
+ },
233
+ signal
215
234
  }, 'authorization server metadata', {
216
235
  allowLoopback
217
236
  });
@@ -220,6 +239,7 @@ async function discoverProtectedResourceMetadataFromHeader(resourceUrl, allowLoo
220
239
  }
221
240
  return await readDiscoveryJson(response, 'authorization server metadata');
222
241
  } catch (_error) {
242
+ throwIfAborted(signal);
223
243
  return null;
224
244
  }
225
245
  }
@@ -228,15 +248,18 @@ async function discoverProtectedResourceMetadataFromHeader(resourceUrl, allowLoo
228
248
  *
229
249
  * @param resourceUrl - URL of the protected resource
230
250
  * @returns Issuer URL if present in WWW-Authenticate header, null otherwise
231
- */ export async function discoverAuthorizationServerIssuer(resourceUrl) {
251
+ */ export async function discoverAuthorizationServerIssuer(resourceUrl, options = {}) {
252
+ const { signal } = options;
232
253
  try {
233
254
  var _match_;
255
+ throwIfAborted(signal);
234
256
  const response = await fetch(resourceUrl, {
235
257
  method: 'GET',
236
258
  headers: {
237
259
  Accept: 'application/json',
238
260
  Connection: 'close'
239
- }
261
+ },
262
+ signal
240
263
  });
241
264
  const header = response.headers.get('www-authenticate');
242
265
  if (!header) return null;
@@ -244,6 +267,7 @@ async function discoverProtectedResourceMetadataFromHeader(resourceUrl, allowLoo
244
267
  if (!match) return null;
245
268
  return (_match_ = match[1]) !== null && _match_ !== void 0 ? _match_ : null;
246
269
  } catch (_error) {
270
+ throwIfAborted(signal);
247
271
  return null;
248
272
  }
249
273
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/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 { discoveryFetch, isLoopbackUrl, readDiscoveryJson } from './discovery-fetch.ts';\nimport type { AuthorizationServerMetadata, ProtectedResourceMetadata } from './types.ts';\n\n/** Returns `url`'s origin (protocol + host), or the original string if it doesn't parse. */\nfunction getOrigin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n return url;\n }\n}\n\n/** Returns `url`'s pathname, or `''` for the root path or an unparseable URL. */\nfunction getPath(url: string): string {\n try {\n const parsed = new URL(url);\n return parsed.pathname === '/' ? '' : parsed.pathname;\n } catch {\n return '';\n }\n}\n\n/**\n * Discovers RFC 9728 Protected Resource Metadata: the `WWW-Authenticate`\n * header first, then `.well-known/oauth-protected-resource` at the origin root and, for a path-prefixed resource, its sub-path variant.\n *\n * @param resourceUrl - URL of the protected resource (e.g. `https://ai.todoist.net/mcp`).\n * @returns Discovered metadata, or `null` if none is found.\n */\nexport async function discoverProtectedResourceMetadata(resourceUrl: string): Promise<ProtectedResourceMetadata | null> {\n try {\n const normalizedResourceUrl = normalizeUrl(resourceUrl);\n // resourceUrl is the server the caller configured (never remote-supplied);\n // its loopback-ness is the trust signal every fetch below relies on.\n const allowLoopback = isLoopbackUrl(normalizedResourceUrl);\n const headerMetadata = await discoverProtectedResourceMetadataFromHeader(normalizedResourceUrl, allowLoopback);\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 discoveryFetch(\n localWellKnownUrl,\n {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n },\n 'protected resource metadata (path-local)',\n { allowLoopback }\n );\n if (response.ok) {\n return await readDiscoveryJson<ProtectedResourceMetadata>(response, 'protected resource metadata (path-local)');\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 discoveryFetch(\n rootUrl,\n {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n },\n 'protected resource metadata (root)',\n { allowLoopback }\n );\n\n if (response.ok) {\n const metadata = await readDiscoveryJson<ProtectedResourceMetadata>(response, 'protected resource metadata (root)');\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 discoveryFetch(\n subPathUrl,\n {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n },\n 'protected resource metadata (sub-path)',\n { allowLoopback }\n );\n if (subPathResponse.ok) {\n return await readDiscoveryJson<ProtectedResourceMetadata>(subPathResponse, 'protected resource metadata (sub-path)');\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 discoveryFetch(\n subPathUrl,\n {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n },\n 'protected resource metadata (sub-path)',\n { allowLoopback }\n );\n\n if (response.ok) {\n return await readDiscoveryJson<ProtectedResourceMetadata>(response, 'protected resource metadata (sub-path)');\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, allowLoopback: boolean): 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 // metadataUrl is remote-server-chosen (the primary SSRF vector here);\n // allowLoopback reflects trust in resourceUrl, not in metadataUrl.\n const metadataUrl = match[1];\n const metadataResponse = await discoveryFetch(\n metadataUrl,\n {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n },\n 'resource_metadata URL',\n { allowLoopback }\n );\n\n if (!metadataResponse.ok) {\n return null;\n }\n\n return await readDiscoveryJson<ProtectedResourceMetadata>(metadataResponse, 'resource_metadata URL');\n } catch (_error) {\n return null;\n }\n}\n\n/**\n * Discovers RFC 8414 Authorization Server Metadata at\n * `.well-known/oauth-authorization-server`, path-local variant first.\n *\n * @param authServerUrl - URL of the authorization server (typically from RFC 9728 discovery).\n * @param options.allowLoopback - Loopback trust grant computed from the server the caller is actually talking to, never from `authServerUrl` itself. Defaults to `false`.\n * @returns Discovered metadata, or `null` if none is found.\n */\nexport async function discoverAuthorizationServerMetadata(authServerUrl: string, options: { allowLoopback?: boolean } = {}): Promise<AuthorizationServerMetadata | null> {\n const { allowLoopback = false } = options;\n try {\n const normalizedAuthServerUrl = normalizeUrl(authServerUrl);\n const localWellKnownUrl = joinWellKnown(normalizedAuthServerUrl, '/.well-known/oauth-authorization-server');\n const localResponse = await discoveryFetch(\n localWellKnownUrl,\n {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n },\n 'authorization server metadata (path-local)',\n { allowLoopback }\n );\n\n if (localResponse.ok) {\n return await readDiscoveryJson<AuthorizationServerMetadata>(localResponse, 'authorization server metadata (path-local)');\n }\n\n const origin = getOrigin(normalizedAuthServerUrl);\n const wellKnownUrl = `${origin}/.well-known/oauth-authorization-server`;\n\n const response = await discoveryFetch(\n wellKnownUrl,\n {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n },\n 'authorization server metadata',\n { allowLoopback }\n );\n\n if (!response.ok) {\n return null;\n }\n\n return await readDiscoveryJson<AuthorizationServerMetadata>(response, 'authorization server metadata');\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","discoveryFetch","isLoopbackUrl","readDiscoveryJson","getOrigin","url","URL","origin","getPath","parsed","pathname","discoverProtectedResourceMetadata","resourceUrl","normalizedResourceUrl","allowLoopback","headerMetadata","discoverProtectedResourceMetadataFromHeader","localWellKnownUrl","response","method","headers","Accept","Connection","ok","path","rootUrl","metadata","resource","startsWith","rootMetadata","subPathUrl","subPathResponse","_error","fetch","header","get","postResponse","body","match","metadataUrl","metadataResponse","discoverAuthorizationServerMetadata","authServerUrl","options","normalizedAuthServerUrl","localResponse","wellKnownUrl","discoverAuthorizationServerIssuer"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/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 { discoveryFetch, isLoopbackUrl, readDiscoveryJson, throwIfAborted } from './discovery-fetch.ts';\nimport type { AuthorizationServerMetadata, ProtectedResourceMetadata } from './types.ts';\n\n/** Returns `url`'s origin (protocol + host), or the original string if it doesn't parse. */\nfunction getOrigin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n return url;\n }\n}\n\n/** Returns `url`'s pathname, or `''` for the root path or an unparseable URL. */\nfunction getPath(url: string): string {\n try {\n const parsed = new URL(url);\n return parsed.pathname === '/' ? '' : parsed.pathname;\n } catch {\n return '';\n }\n}\n\n/**\n * Discovers RFC 9728 Protected Resource Metadata: the `WWW-Authenticate`\n * header first, then `.well-known/oauth-protected-resource` at the origin root and, for a path-prefixed resource, its sub-path variant.\n *\n * @param resourceUrl - URL of the protected resource (e.g. `https://ai.todoist.net/mcp`).\n * @returns Discovered metadata, or `null` if none is found.\n */\nexport async function discoverProtectedResourceMetadata(resourceUrl: string, options: { signal?: AbortSignal } = {}): Promise<ProtectedResourceMetadata | null> {\n const { signal } = options;\n try {\n throwIfAborted(signal);\n const normalizedResourceUrl = normalizeUrl(resourceUrl);\n // resourceUrl is the server the caller configured (never remote-supplied);\n // its loopback-ness is the trust signal every fetch below relies on.\n const allowLoopback = isLoopbackUrl(normalizedResourceUrl);\n const headerMetadata = await discoverProtectedResourceMetadataFromHeader(normalizedResourceUrl, allowLoopback, signal);\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 discoveryFetch(\n localWellKnownUrl,\n {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n signal,\n },\n 'protected resource metadata (path-local)',\n { allowLoopback }\n );\n if (response.ok) {\n return await readDiscoveryJson<ProtectedResourceMetadata>(response, 'protected resource metadata (path-local)');\n }\n } catch {\n throwIfAborted(signal);\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 discoveryFetch(\n rootUrl,\n {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n signal,\n },\n 'protected resource metadata (root)',\n { allowLoopback }\n );\n\n if (response.ok) {\n const metadata = await readDiscoveryJson<ProtectedResourceMetadata>(response, 'protected resource metadata (root)');\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 discoveryFetch(\n subPathUrl,\n {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n signal,\n },\n 'protected resource metadata (sub-path)',\n { allowLoopback }\n );\n if (subPathResponse.ok) {\n return await readDiscoveryJson<ProtectedResourceMetadata>(subPathResponse, 'protected resource metadata (sub-path)');\n }\n } catch {\n throwIfAborted(signal);\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 throwIfAborted(signal);\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 discoveryFetch(\n subPathUrl,\n {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n signal,\n },\n 'protected resource metadata (sub-path)',\n { allowLoopback }\n );\n\n if (response.ok) {\n return await readDiscoveryJson<ProtectedResourceMetadata>(response, 'protected resource metadata (sub-path)');\n }\n } catch {\n throwIfAborted(signal);\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 throwIfAborted(signal);\n // Network error, invalid URL, or other failure\n return null;\n }\n}\n\nasync function discoverProtectedResourceMetadataFromHeader(resourceUrl: string, allowLoopback: boolean, signal?: AbortSignal): Promise<ProtectedResourceMetadata | null> {\n try {\n throwIfAborted(signal);\n const response = await fetch(resourceUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n signal,\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 signal,\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 // metadataUrl is remote-server-chosen (the primary SSRF vector here);\n // allowLoopback reflects trust in resourceUrl, not in metadataUrl.\n const metadataUrl = match[1];\n const metadataResponse = await discoveryFetch(\n metadataUrl,\n {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n signal,\n },\n 'resource_metadata URL',\n { allowLoopback }\n );\n\n if (!metadataResponse.ok) {\n return null;\n }\n\n return await readDiscoveryJson<ProtectedResourceMetadata>(metadataResponse, 'resource_metadata URL');\n } catch (_error) {\n throwIfAborted(signal);\n return null;\n }\n}\n\n/**\n * Discovers RFC 8414 Authorization Server Metadata at\n * `.well-known/oauth-authorization-server`, path-local variant first.\n *\n * @param authServerUrl - URL of the authorization server (typically from RFC 9728 discovery).\n * @param options.allowLoopback - Loopback trust grant computed from the server the caller is actually talking to, never from `authServerUrl` itself. Defaults to `false`.\n * @returns Discovered metadata, or `null` if none is found.\n */\nexport async function discoverAuthorizationServerMetadata(authServerUrl: string, options: { allowLoopback?: boolean; signal?: AbortSignal } = {}): Promise<AuthorizationServerMetadata | null> {\n const { allowLoopback = false, signal } = options;\n try {\n throwIfAborted(signal);\n const normalizedAuthServerUrl = normalizeUrl(authServerUrl);\n const localWellKnownUrl = joinWellKnown(normalizedAuthServerUrl, '/.well-known/oauth-authorization-server');\n const localResponse = await discoveryFetch(\n localWellKnownUrl,\n {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n signal,\n },\n 'authorization server metadata (path-local)',\n { allowLoopback }\n );\n\n if (localResponse.ok) {\n return await readDiscoveryJson<AuthorizationServerMetadata>(localResponse, 'authorization server metadata (path-local)');\n }\n\n const origin = getOrigin(normalizedAuthServerUrl);\n const wellKnownUrl = `${origin}/.well-known/oauth-authorization-server`;\n\n const response = await discoveryFetch(\n wellKnownUrl,\n {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n signal,\n },\n 'authorization server metadata',\n { allowLoopback }\n );\n\n if (!response.ok) {\n return null;\n }\n\n return await readDiscoveryJson<AuthorizationServerMetadata>(response, 'authorization server metadata');\n } catch (_error) {\n throwIfAborted(signal);\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, options: { signal?: AbortSignal } = {}): Promise<string | null> {\n const { signal } = options;\n try {\n throwIfAborted(signal);\n const response = await fetch(resourceUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n signal,\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 throwIfAborted(signal);\n return null;\n }\n}\n"],"names":["joinWellKnown","normalizeUrl","discoveryFetch","isLoopbackUrl","readDiscoveryJson","throwIfAborted","getOrigin","url","URL","origin","getPath","parsed","pathname","discoverProtectedResourceMetadata","resourceUrl","options","signal","normalizedResourceUrl","allowLoopback","headerMetadata","discoverProtectedResourceMetadataFromHeader","localWellKnownUrl","response","method","headers","Accept","Connection","ok","path","rootUrl","metadata","resource","startsWith","rootMetadata","subPathUrl","subPathResponse","_error","fetch","header","get","postResponse","body","match","metadataUrl","metadataResponse","discoverAuthorizationServerMetadata","authServerUrl","normalizedAuthServerUrl","localResponse","wellKnownUrl","discoverAuthorizationServerIssuer"],"mappings":"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"}
@@ -142,11 +142,15 @@ export interface DcrRegistrationOptions {
142
142
  * remote-controlled AS metadata). Defaults to `false`.
143
143
  */
144
144
  allowLoopback?: boolean;
145
+ /** Cancels an in-progress registration request. */
146
+ signal?: AbortSignal;
145
147
  }
146
148
  /**
147
149
  * Options for OAuth authorization flow
148
150
  */
149
151
  export interface OAuthFlowOptions {
152
+ /** Cancels an in-progress authorization and closes its callback listener. */
153
+ signal?: AbortSignal;
150
154
  /** Port for OAuth callback listener (required - use get-port to find available port) */
151
155
  port: number;
152
156
  /** Issuer identifier discovered before the flow starts; the `iss` in the authorization response must match it (RFC 9207) */
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/auth/types.ts"],"sourcesContent":["/**\n * Shared types for OAuth and DCR authentication\n */\n\n/**\n * OAuth callback result from authorization server\n */\nexport interface CallbackResult {\n /** Authorization code from OAuth server */\n code: string;\n /** State parameter for CSRF protection */\n state?: string;\n /** Issuer identifier of the authorization server that minted the response (RFC 9207) */\n iss?: string;\n}\n\n/**\n * PKCE (Proof Key for Code Exchange) parameters (RFC 7636)\n * Used to secure OAuth 2.0 authorization code flow for public clients\n */\nexport interface PkceParams {\n /** Code verifier - cryptographically random string (43-128 characters) */\n codeVerifier: string;\n /** Code challenge - derived from code verifier using challenge method */\n codeChallenge: string;\n /** Code challenge method - S256 (SHA-256) or plain */\n codeChallengeMethod: 'S256' | 'plain';\n}\n\n/**\n * OAuth token set with access and refresh tokens\n */\nexport interface TokenSet {\n /** Access token for API requests */\n accessToken: string;\n /** Refresh token for obtaining new access tokens */\n refreshToken: string;\n /** Timestamp when access token expires (milliseconds since epoch) */\n expiresAt: number;\n /** Scopes granted for this token set */\n scopes?: string[];\n /** Client ID used for DCR registration (stored for future use) */\n clientId?: string;\n /** Client secret used for DCR registration (stored for future use) */\n clientSecret?: string;\n /** Issuer identifier of the authorization server these credentials belong to (SEP-2352) */\n issuer?: string;\n}\n\n/**\n * OAuth 2.0 Protected Resource Metadata (RFC 9728)\n * Response from .well-known/oauth-protected-resource endpoint\n */\nexport interface ProtectedResourceMetadata {\n /** The protected resource identifier */\n resource: string;\n /** List of authorization server URLs that can issue tokens for this resource */\n authorization_servers: string[];\n /** Optional list of scopes supported by this resource */\n scopes_supported?: string[];\n /** Optional list of bearer token methods supported (header, query, body) */\n bearer_methods_supported?: string[];\n}\n\n/**\n * OAuth 2.0 Authorization Server Metadata (RFC 8414)\n * Response from .well-known/oauth-authorization-server endpoint\n */\nexport interface AuthorizationServerMetadata {\n /** The authorization server's issuer identifier */\n issuer?: string;\n /** URL of the authorization endpoint */\n authorization_endpoint?: string;\n /** URL of the token endpoint */\n token_endpoint?: string;\n /** URL of the client registration endpoint (DCR - RFC 7591) */\n registration_endpoint?: string;\n /** URL of the token introspection endpoint */\n introspection_endpoint?: string;\n /** List of OAuth scopes supported by the authorization server */\n scopes_supported?: string[];\n /** Response types supported (code, token, etc.) */\n response_types_supported?: string[];\n /** Grant types supported (authorization_code, refresh_token, etc.) */\n grant_types_supported?: string[];\n /** Token endpoint authentication methods supported */\n token_endpoint_auth_methods_supported?: string[];\n /** Whether the authorization response carries an `iss` parameter (RFC 9207) */\n authorization_response_iss_parameter_supported?: boolean;\n}\n\n/**\n * OAuth server capabilities discovered from .well-known endpoint\n */\nexport interface AuthCapabilities {\n /** Whether the server supports Dynamic Client Registration (RFC 7591) */\n supportsDcr: boolean;\n /** Issuer identifier from the authorization server metadata (RFC 8414) */\n issuer?: string;\n /**\n * The protected resource's canonical identifier, from the RFC 9728 metadata\n * document's `resource` field. This is what an RFC 8707 `resource` indicator\n * must carry, and it is the resource server's own statement of its identity -\n * not the URL we happened to dial, and never the base URL discovery was\n * performed against, which has any `/mcp` segment stripped off it.\n *\n * Absent when no protected-resource metadata was published and the\n * authorization server was reached by direct RFC 8414 discovery instead.\n */\n resource?: string;\n /** Whether the authorization response carries an `iss` parameter (RFC 9207) */\n authorizationResponseIssSupported?: boolean;\n /** DCR client registration endpoint */\n registrationEndpoint?: string;\n /** OAuth authorization endpoint */\n authorizationEndpoint?: string;\n /** OAuth token endpoint */\n tokenEndpoint?: string;\n /** Token introspection endpoint */\n introspectionEndpoint?: string;\n /** Supported OAuth scopes */\n scopes?: string[];\n}\n\n/**\n * Client credentials from DCR registration\n */\nexport interface ClientCredentials {\n /** OAuth client ID */\n clientId: string;\n /** OAuth client secret */\n clientSecret: string;\n /** Timestamp when client was registered */\n issuedAt?: number;\n}\n\n/**\n * Options for DCR client registration\n */\nexport interface DcrRegistrationOptions {\n /** Redirect URI for OAuth callback, from the loopback listener the caller has already bound (RFC 8252) */\n redirectUri: string;\n /** Client name to register */\n clientName?: string;\n /**\n * Loopback trust grant for the registration_endpoint fetch (SSRF\n * mitigation - see `src/auth/discovery-fetch.ts`). Compute this from the\n * MCP server the caller is actually talking to, never from\n * `registrationEndpoint` itself (which is typically sourced from\n * remote-controlled AS metadata). Defaults to `false`.\n */\n allowLoopback?: boolean;\n}\n\n/**\n * Options for OAuth authorization flow\n */\nexport interface OAuthFlowOptions {\n /** Port for OAuth callback listener (required - use get-port to find available port) */\n port: number;\n /** Issuer identifier discovered before the flow starts; the `iss` in the authorization response must match it (RFC 9207) */\n issuer: string;\n /** Canonical resource server URI, sent as `resource` on the authorization, token, and refresh requests (RFC 8707) */\n resource: string;\n /** Redirect URI for OAuth callback (optional - will be built from port if not provided) */\n redirectUri?: string;\n /** OAuth scopes to request */\n scopes?: string[];\n /** Whether the authorization server advertises `authorization_response_iss_parameter_supported` (RFC 9207) */\n authorizationResponseIssSupported?: boolean;\n /** Enable PKCE (RFC 7636) - recommended for all clients, required for public clients */\n pkce?: boolean;\n /** Headless mode (don't open browser) */\n headless?: boolean;\n /** Timeout for callback (milliseconds) */\n timeout?: number;\n /** Optional logger for debug output (defaults to singleton logger) */\n logger?: import('../utils/logger.ts').Logger;\n /**\n * Loopback trust grant for the token endpoint fetch (SSRF mitigation - see\n * `src/auth/discovery-fetch.ts`). Compute this from the MCP server the\n * caller is actually talking to, never from `tokenEndpoint` itself (which\n * is typically sourced from remote-controlled AS metadata). Defaults to\n * `false`.\n */\n allowLoopback?: boolean;\n}\n"],"names":[],"mappings":"AAAA;;CAEC,GAED;;CAEC,GAoJD;;CAEC,GACD,WA6BC"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/auth/types.ts"],"sourcesContent":["/**\n * Shared types for OAuth and DCR authentication\n */\n\n/**\n * OAuth callback result from authorization server\n */\nexport interface CallbackResult {\n /** Authorization code from OAuth server */\n code: string;\n /** State parameter for CSRF protection */\n state?: string;\n /** Issuer identifier of the authorization server that minted the response (RFC 9207) */\n iss?: string;\n}\n\n/**\n * PKCE (Proof Key for Code Exchange) parameters (RFC 7636)\n * Used to secure OAuth 2.0 authorization code flow for public clients\n */\nexport interface PkceParams {\n /** Code verifier - cryptographically random string (43-128 characters) */\n codeVerifier: string;\n /** Code challenge - derived from code verifier using challenge method */\n codeChallenge: string;\n /** Code challenge method - S256 (SHA-256) or plain */\n codeChallengeMethod: 'S256' | 'plain';\n}\n\n/**\n * OAuth token set with access and refresh tokens\n */\nexport interface TokenSet {\n /** Access token for API requests */\n accessToken: string;\n /** Refresh token for obtaining new access tokens */\n refreshToken: string;\n /** Timestamp when access token expires (milliseconds since epoch) */\n expiresAt: number;\n /** Scopes granted for this token set */\n scopes?: string[];\n /** Client ID used for DCR registration (stored for future use) */\n clientId?: string;\n /** Client secret used for DCR registration (stored for future use) */\n clientSecret?: string;\n /** Issuer identifier of the authorization server these credentials belong to (SEP-2352) */\n issuer?: string;\n}\n\n/**\n * OAuth 2.0 Protected Resource Metadata (RFC 9728)\n * Response from .well-known/oauth-protected-resource endpoint\n */\nexport interface ProtectedResourceMetadata {\n /** The protected resource identifier */\n resource: string;\n /** List of authorization server URLs that can issue tokens for this resource */\n authorization_servers: string[];\n /** Optional list of scopes supported by this resource */\n scopes_supported?: string[];\n /** Optional list of bearer token methods supported (header, query, body) */\n bearer_methods_supported?: string[];\n}\n\n/**\n * OAuth 2.0 Authorization Server Metadata (RFC 8414)\n * Response from .well-known/oauth-authorization-server endpoint\n */\nexport interface AuthorizationServerMetadata {\n /** The authorization server's issuer identifier */\n issuer?: string;\n /** URL of the authorization endpoint */\n authorization_endpoint?: string;\n /** URL of the token endpoint */\n token_endpoint?: string;\n /** URL of the client registration endpoint (DCR - RFC 7591) */\n registration_endpoint?: string;\n /** URL of the token introspection endpoint */\n introspection_endpoint?: string;\n /** List of OAuth scopes supported by the authorization server */\n scopes_supported?: string[];\n /** Response types supported (code, token, etc.) */\n response_types_supported?: string[];\n /** Grant types supported (authorization_code, refresh_token, etc.) */\n grant_types_supported?: string[];\n /** Token endpoint authentication methods supported */\n token_endpoint_auth_methods_supported?: string[];\n /** Whether the authorization response carries an `iss` parameter (RFC 9207) */\n authorization_response_iss_parameter_supported?: boolean;\n}\n\n/**\n * OAuth server capabilities discovered from .well-known endpoint\n */\nexport interface AuthCapabilities {\n /** Whether the server supports Dynamic Client Registration (RFC 7591) */\n supportsDcr: boolean;\n /** Issuer identifier from the authorization server metadata (RFC 8414) */\n issuer?: string;\n /**\n * The protected resource's canonical identifier, from the RFC 9728 metadata\n * document's `resource` field. This is what an RFC 8707 `resource` indicator\n * must carry, and it is the resource server's own statement of its identity -\n * not the URL we happened to dial, and never the base URL discovery was\n * performed against, which has any `/mcp` segment stripped off it.\n *\n * Absent when no protected-resource metadata was published and the\n * authorization server was reached by direct RFC 8414 discovery instead.\n */\n resource?: string;\n /** Whether the authorization response carries an `iss` parameter (RFC 9207) */\n authorizationResponseIssSupported?: boolean;\n /** DCR client registration endpoint */\n registrationEndpoint?: string;\n /** OAuth authorization endpoint */\n authorizationEndpoint?: string;\n /** OAuth token endpoint */\n tokenEndpoint?: string;\n /** Token introspection endpoint */\n introspectionEndpoint?: string;\n /** Supported OAuth scopes */\n scopes?: string[];\n}\n\n/**\n * Client credentials from DCR registration\n */\nexport interface ClientCredentials {\n /** OAuth client ID */\n clientId: string;\n /** OAuth client secret */\n clientSecret: string;\n /** Timestamp when client was registered */\n issuedAt?: number;\n}\n\n/**\n * Options for DCR client registration\n */\nexport interface DcrRegistrationOptions {\n /** Redirect URI for OAuth callback, from the loopback listener the caller has already bound (RFC 8252) */\n redirectUri: string;\n /** Client name to register */\n clientName?: string;\n /**\n * Loopback trust grant for the registration_endpoint fetch (SSRF\n * mitigation - see `src/auth/discovery-fetch.ts`). Compute this from the\n * MCP server the caller is actually talking to, never from\n * `registrationEndpoint` itself (which is typically sourced from\n * remote-controlled AS metadata). Defaults to `false`.\n */\n allowLoopback?: boolean;\n /** Cancels an in-progress registration request. */\n signal?: AbortSignal;\n}\n\n/**\n * Options for OAuth authorization flow\n */\nexport interface OAuthFlowOptions {\n /** Cancels an in-progress authorization and closes its callback listener. */\n signal?: AbortSignal;\n /** Port for OAuth callback listener (required - use get-port to find available port) */\n port: number;\n /** Issuer identifier discovered before the flow starts; the `iss` in the authorization response must match it (RFC 9207) */\n issuer: string;\n /** Canonical resource server URI, sent as `resource` on the authorization, token, and refresh requests (RFC 8707) */\n resource: string;\n /** Redirect URI for OAuth callback (optional - will be built from port if not provided) */\n redirectUri?: string;\n /** OAuth scopes to request */\n scopes?: string[];\n /** Whether the authorization server advertises `authorization_response_iss_parameter_supported` (RFC 9207) */\n authorizationResponseIssSupported?: boolean;\n /** Enable PKCE (RFC 7636) - recommended for all clients, required for public clients */\n pkce?: boolean;\n /** Headless mode (don't open browser) */\n headless?: boolean;\n /** Timeout for callback (milliseconds) */\n timeout?: number;\n /** Optional logger for debug output (defaults to singleton logger) */\n logger?: import('../utils/logger.ts').Logger;\n /**\n * Loopback trust grant for the token endpoint fetch (SSRF mitigation - see\n * `src/auth/discovery-fetch.ts`). Compute this from the MCP server the\n * caller is actually talking to, never from `tokenEndpoint` itself (which\n * is typically sourced from remote-controlled AS metadata). Defaults to\n * `false`.\n */\n allowLoopback?: boolean;\n}\n"],"names":[],"mappings":"AAAA;;CAEC,GAED;;CAEC,GAsJD;;CAEC,GACD,WA+BC"}
@@ -4,7 +4,7 @@
4
4
  * Helper to connect MCP SDK clients to servers with intelligent transport inference.
5
5
  * Automatically detects transport type from URL protocol or type field.
6
6
  */
7
- import type { VersionNegotiationOptions } from '@modelcontextprotocol/client';
7
+ import type { Transport, VersionNegotiationOptions } from '@modelcontextprotocol/client';
8
8
  import { Client } from '@modelcontextprotocol/client';
9
9
  import { type DcrAuthenticatorOptions } from '../dcr/index.js';
10
10
  import type { ServerProcess } from '../spawn/spawn-server.js';
@@ -18,6 +18,12 @@ interface RegistryLike {
18
18
  servers: Map<string, ServerProcess>;
19
19
  }
20
20
  import { type Logger } from '../utils/logger.js';
21
+ /** @internal - Bounds work by aborting the actual request signal at the deadline. */
22
+ export declare function withAbortTimeout<T>(operationFn: (signal: AbortSignal) => Promise<T>, ms: number, operation: string, parentSignal?: AbortSignal): Promise<T>;
23
+ /** @internal - Connects a transport with a deadline that aborts and cleans up the actual connection. */
24
+ export declare function connectTransportWithTimeout(client: Client, transport: Transport, ms: number, operation: string, parentSignal?: AbortSignal): Promise<void>;
25
+ /** @internal - Connects one client/transport pair and reports any failed cleanup. */
26
+ export declare function connectTransport(client: Client, transport: Transport, signal?: AbortSignal): Promise<void>;
21
27
  /**
22
28
  * Connect MCP SDK client to server with full readiness handling.
23
29
  * @internal - Use registry.connect() instead
@@ -65,5 +71,7 @@ export declare function connectMcpClient(registryOrConfig: RegistryLike | Server
65
71
  dcrAuthenticator?: Partial<DcrAuthenticatorOptions>;
66
72
  logger?: Logger;
67
73
  versionNegotiation?: VersionNegotiationOptions;
74
+ /** Cancels readiness, authentication, and transport connection work. */
75
+ signal?: AbortSignal;
68
76
  }): Promise<Client>;
69
77
  export {};
@@ -13,21 +13,84 @@ import { normalizeUrl } from '../lib/url-utils.js';
13
13
  import { logger as defaultLogger } from '../utils/logger.js';
14
14
  import { ExistingProcessTransport } from './existing-process-transport.js';
15
15
  import { waitForHttpReady } from './wait-for-http-ready.js';
16
- /**
17
- * Wrap promise with timeout - throws if promise takes too long
18
- * Clears timeout when promise completes to prevent hanging event loop
19
- * @param promise - Promise to wrap
20
- * @param ms - Timeout in milliseconds
21
- * @param operation - Description of operation for error message
22
- * @returns Promise result or timeout error
23
- */ async function withTimeout(promise, ms, operation) {
16
+ const HTTP_CONNECTION_TIMEOUT_MS = 30000;
17
+ const CLIENT_CLEANUP_TIMEOUT_MS = 5000;
18
+ /** @internal - Bounds work by aborting the actual request signal at the deadline. */ export async function withAbortTimeout(operationFn, ms, operation, parentSignal) {
19
+ const controller = new AbortController();
20
+ const timeoutError = new Error(`Timeout after ${ms}ms: ${operation}`);
21
+ const abortFromParent = ()=>controller.abort((parentSignal === null || parentSignal === void 0 ? void 0 : parentSignal.reason) instanceof Error ? parentSignal.reason : new Error('Connection was cancelled'));
22
+ parentSignal === null || parentSignal === void 0 ? void 0 : parentSignal.addEventListener('abort', abortFromParent, {
23
+ once: true
24
+ });
25
+ if (parentSignal === null || parentSignal === void 0 ? void 0 : parentSignal.aborted) abortFromParent();
26
+ const timeoutId = setTimeout(()=>controller.abort(timeoutError), ms);
27
+ try {
28
+ // Preserve the operation's error unchanged: connection cleanup may have
29
+ // attached failures to the original cancellation or timeout.
30
+ return await operationFn(controller.signal);
31
+ } finally{
32
+ clearTimeout(timeoutId);
33
+ parentSignal === null || parentSignal === void 0 ? void 0 : parentSignal.removeEventListener('abort', abortFromParent);
34
+ }
35
+ }
36
+ /** @internal - Connects a transport with a deadline that aborts and cleans up the actual connection. */ export async function connectTransportWithTimeout(client, transport, ms, operation, parentSignal) {
37
+ return withAbortTimeout((signal)=>connectTransport(client, transport, signal), ms, operation, parentSignal);
38
+ }
39
+ function throwIfAborted(signal) {
40
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted) throw signal.reason instanceof Error ? signal.reason : new Error('Connection was cancelled');
41
+ }
42
+ async function closeClientWithinTimeout(client) {
24
43
  let timeoutId;
25
- return Promise.race([
26
- promise.finally(()=>clearTimeout(timeoutId)),
27
- new Promise((_, reject)=>{
28
- timeoutId = setTimeout(()=>reject(new Error(`Timeout after ${ms}ms: ${operation}`)), ms);
29
- })
30
- ]);
44
+ try {
45
+ await Promise.race([
46
+ client.close(),
47
+ new Promise((_, reject)=>{
48
+ timeoutId = setTimeout(()=>reject(new Error(`Client cleanup did not complete within ${CLIENT_CLEANUP_TIMEOUT_MS}ms`)), CLIENT_CLEANUP_TIMEOUT_MS);
49
+ })
50
+ ]);
51
+ } finally{
52
+ if (timeoutId) clearTimeout(timeoutId);
53
+ }
54
+ }
55
+ async function rethrowAfterClientCleanup(error, clients, message) {
56
+ const cleanup = await Promise.allSettled(clients.map((client)=>closeClientWithinTimeout(client)));
57
+ const cleanupErrors = cleanup.flatMap((result)=>result.status === 'rejected' ? [
58
+ result.reason
59
+ ] : []);
60
+ if (cleanupErrors.length > 0) throw new AggregateError([
61
+ error,
62
+ ...cleanupErrors
63
+ ], message, {
64
+ cause: error
65
+ });
66
+ throw error;
67
+ }
68
+ /** @internal - Connects one client/transport pair and reports any failed cleanup. */ export async function connectTransport(client, transport, signal) {
69
+ let abortHandler;
70
+ try {
71
+ throwIfAborted(signal);
72
+ const connecting = client.connect(transport);
73
+ if (signal) {
74
+ const aborted = new Promise((_, reject)=>{
75
+ abortHandler = ()=>reject(signal.reason instanceof Error ? signal.reason : new Error('Connection was cancelled'));
76
+ signal.addEventListener('abort', abortHandler, {
77
+ once: true
78
+ });
79
+ });
80
+ await Promise.race([
81
+ connecting,
82
+ aborted
83
+ ]);
84
+ } else {
85
+ await connecting;
86
+ }
87
+ } catch (error) {
88
+ await rethrowAfterClientCleanup(error, [
89
+ client
90
+ ], 'MCP connection failed and client cleanup also failed');
91
+ } finally{
92
+ if (signal && abortHandler) signal.removeEventListener('abort', abortHandler);
93
+ }
31
94
  }
32
95
  /**
33
96
  * Infer transport type from server configuration with validation.
@@ -117,6 +180,7 @@ import { waitForHttpReady } from './wait-for-http-ready.js';
117
180
  const serversConfig = isRegistry ? registryOrConfig.config : registryOrConfig;
118
181
  const registry = isRegistry ? registryOrConfig : undefined;
119
182
  const logger = (_ref = options === null || options === void 0 ? void 0 : options.logger) !== null && _ref !== void 0 ? _ref : defaultLogger;
183
+ throwIfAborted(options === null || options === void 0 ? void 0 : options.signal);
120
184
  const serverConfig = serversConfig[serverName];
121
185
  if (!serverConfig) {
122
186
  const available = Object.keys(serversConfig).join(', ');
@@ -145,7 +209,7 @@ import { waitForHttpReady } from './wait-for-http-ready.js';
145
209
  if (serverHandle) {
146
210
  // Reuse the already-spawned process
147
211
  const transport = new ExistingProcessTransport(serverHandle.process);
148
- await client.connect(transport);
212
+ await connectTransport(client, transport, options === null || options === void 0 ? void 0 : options.signal);
149
213
  } else {
150
214
  // No registry or server not in registry - spawn new process directly
151
215
  // This is the standard fallback when process management is not used
@@ -158,7 +222,7 @@ import { waitForHttpReady } from './wait-for-http-ready.js';
158
222
  env: serverConfig.env || {}
159
223
  });
160
224
  // client.connect() performs initialize handshake - when it resolves, server is ready
161
- await client.connect(transport);
225
+ await connectTransport(client, transport, options === null || options === void 0 ? void 0 : options.signal);
162
226
  }
163
227
  } else if (transportType === 'http') {
164
228
  if (!('url' in serverConfig) || !serverConfig.url) {
@@ -169,7 +233,7 @@ import { waitForHttpReady } from './wait-for-http-ready.js';
169
233
  const isSpawnedHttp = registry === null || registry === void 0 ? void 0 : registry.servers.has(serverName);
170
234
  if (isSpawnedHttp) {
171
235
  logger.debug(`[connectMcpClient] waiting for HTTP server '${serverName}' at ${serverConfig.url}`);
172
- await waitForHttpReady(serverConfig.url);
236
+ await waitForHttpReady(serverConfig.url, 30000, options === null || options === void 0 ? void 0 : options.signal);
173
237
  logger.debug(`[connectMcpClient] HTTP server '${serverName}' ready`);
174
238
  }
175
239
  const url = new URL(serverConfig.url);
@@ -181,7 +245,9 @@ import { waitForHttpReady } from './wait-for-http-ready.js';
181
245
  // base names a different resource, which authorization servers that validate
182
246
  // the `resource` indicator reject as `invalid_target`.
183
247
  const mcpServerUrl = normalizeUrl(serverConfig.url);
184
- const capabilities = await withTimeout(probeAuthCapabilities(mcpServerUrl), DCR_CAPABILTY_DISCOVERY_TIMEOUT, 'DCR capability discovery');
248
+ const capabilities = await withAbortTimeout((signal)=>probeAuthCapabilities(mcpServerUrl, {
249
+ signal
250
+ }), DCR_CAPABILTY_DISCOVERY_TIMEOUT, 'DCR capability discovery', options === null || options === void 0 ? void 0 : options.signal);
185
251
  let authToken;
186
252
  if (capabilities.supportsDcr) {
187
253
  logger.debug(`🔐 Server '${serverName}' supports DCR authentication`);
@@ -196,7 +262,7 @@ import { waitForHttpReady } from './wait-for-http-ready.js';
196
262
  ...options === null || options === void 0 ? void 0 : options.dcrAuthenticator
197
263
  });
198
264
  // Ensure we have valid tokens (performs DCR + OAuth if needed)
199
- const tokens = await authenticator.ensureAuthenticated(mcpServerUrl, capabilities);
265
+ const tokens = await authenticator.ensureAuthenticated(mcpServerUrl, capabilities, options === null || options === void 0 ? void 0 : options.signal);
200
266
  authToken = tokens.accessToken;
201
267
  logger.debug(`✅ Authentication complete for '${serverName}'`);
202
268
  } else {
@@ -221,8 +287,12 @@ import { waitForHttpReady } from './wait-for-http-ready.js';
221
287
  const transport = new StreamableHTTPClientTransport(url, transportOptions);
222
288
  // Type assertion: SDK transport has sessionId: string | undefined but Transport expects string
223
289
  // This is safe at runtime - the undefined is valid per MCP spec
224
- await withTimeout(client.connect(transport), 30000, 'StreamableHTTP connection');
290
+ await connectTransportWithTimeout(client, transport, HTTP_CONNECTION_TIMEOUT_MS, 'StreamableHTTP connection', options === null || options === void 0 ? void 0 : options.signal);
225
291
  } catch (error) {
292
+ var _options_signal;
293
+ // Cancellation and cleanup failures must reach the caller; neither can
294
+ // safely be converted into an SSE fallback attempt.
295
+ if ((options === null || options === void 0 ? void 0 : (_options_signal = options.signal) === null || _options_signal === void 0 ? void 0 : _options_signal.aborted) || error instanceof AggregateError) throw error;
226
296
  // Fall back to SSE transport (MCP protocol version 2024-11-05)
227
297
  // SSE is a standard MCP transport used by many servers (e.g., FastMCP ecosystem)
228
298
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -231,9 +301,10 @@ import { waitForHttpReady } from './wait-for-http-ready.js';
231
301
  const cause = error instanceof Error ? error.cause : undefined;
232
302
  const isConnectionRefused = (cause === null || cause === void 0 ? void 0 : cause.code) === 'ECONNREFUSED' || errorMessage.includes('Connection refused');
233
303
  if (isConnectionRefused) {
234
- // Clean up client resources before throwing
235
- await client.close().catch(()=>{});
236
- throw new Error(`Server not running at ${url}`);
304
+ const unavailable = new Error(`Server not running at ${url}`, {
305
+ cause: error
306
+ });
307
+ throw unavailable;
237
308
  }
238
309
  // Check for known errors that indicate SSE fallback is needed
239
310
  const shouldFallback = errorMessage.includes('Missing session ID') || // FastMCP specific
@@ -265,18 +336,9 @@ import { waitForHttpReady } from './wait-for-http-ready.js';
265
336
  }
266
337
  } : undefined;
267
338
  const sseTransport = new SSEClientTransport(url, sseTransportOptions);
268
- try {
269
- await withTimeout(sseClient.connect(sseTransport), 30000, 'SSE connection');
270
- // Return SSE client instead of original
271
- return sseClient;
272
- } catch (sseError) {
273
- // SSE connection failed - clean up both clients before throwing
274
- await Promise.all([
275
- client.close().catch(()=>{}),
276
- sseClient.close().catch(()=>{})
277
- ]);
278
- throw sseError;
279
- }
339
+ await connectTransportWithTimeout(sseClient, sseTransport, HTTP_CONNECTION_TIMEOUT_MS, 'SSE connection', options === null || options === void 0 ? void 0 : options.signal);
340
+ // Return SSE client instead of original
341
+ return sseClient;
280
342
  }
281
343
  }
282
344
  return client; // Guaranteed ready when returned