@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.
- package/README.md +12 -1
- package/dist/cjs/auth/capability-discovery.d.cts +3 -1
- package/dist/cjs/auth/capability-discovery.d.ts +3 -1
- package/dist/cjs/auth/capability-discovery.js +46 -32
- package/dist/cjs/auth/capability-discovery.js.map +1 -1
- package/dist/cjs/auth/discovery-fetch.d.cts +2 -0
- package/dist/cjs/auth/discovery-fetch.d.ts +2 -0
- package/dist/cjs/auth/discovery-fetch.js +85 -31
- package/dist/cjs/auth/discovery-fetch.js.map +1 -1
- package/dist/cjs/auth/interactive-oauth-flow.d.cts +1 -1
- package/dist/cjs/auth/interactive-oauth-flow.d.ts +1 -1
- package/dist/cjs/auth/interactive-oauth-flow.js +13 -6
- package/dist/cjs/auth/interactive-oauth-flow.js.map +1 -1
- package/dist/cjs/auth/oauth-callback-listener.d.cts +1 -1
- package/dist/cjs/auth/oauth-callback-listener.d.ts +1 -1
- package/dist/cjs/auth/oauth-callback-listener.js +38 -2
- package/dist/cjs/auth/oauth-callback-listener.js.map +1 -1
- package/dist/cjs/auth/rfc9728-discovery.d.cts +7 -2
- package/dist/cjs/auth/rfc9728-discovery.d.ts +7 -2
- package/dist/cjs/auth/rfc9728-discovery.js +120 -88
- package/dist/cjs/auth/rfc9728-discovery.js.map +1 -1
- package/dist/cjs/auth/types.d.cts +4 -0
- package/dist/cjs/auth/types.d.ts +4 -0
- package/dist/cjs/auth/types.js.map +1 -1
- package/dist/cjs/connection/connect-client.d.cts +9 -1
- package/dist/cjs/connection/connect-client.d.ts +9 -1
- package/dist/cjs/connection/connect-client.js +282 -75
- package/dist/cjs/connection/connect-client.js.map +1 -1
- package/dist/cjs/connection/existing-process-transport.d.cts +18 -18
- package/dist/cjs/connection/existing-process-transport.d.ts +18 -18
- package/dist/cjs/connection/existing-process-transport.js +187 -82
- package/dist/cjs/connection/existing-process-transport.js.map +1 -1
- package/dist/cjs/connection/wait-for-http-ready.d.cts +1 -1
- package/dist/cjs/connection/wait-for-http-ready.d.ts +1 -1
- package/dist/cjs/connection/wait-for-http-ready.js +30 -5
- package/dist/cjs/connection/wait-for-http-ready.js.map +1 -1
- package/dist/cjs/dcr/dcr-authenticator.d.cts +1 -1
- package/dist/cjs/dcr/dcr-authenticator.d.ts +1 -1
- package/dist/cjs/dcr/dcr-authenticator.js +29 -18
- package/dist/cjs/dcr/dcr-authenticator.js.map +1 -1
- package/dist/cjs/dcr/dynamic-client-registrar.js +2 -1
- package/dist/cjs/dcr/dynamic-client-registrar.js.map +1 -1
- package/dist/cjs/index.d.cts +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/spawn/owned-process-tree.d.cts +12 -0
- package/dist/cjs/spawn/owned-process-tree.d.ts +12 -0
- package/dist/cjs/spawn/owned-process-tree.js +93 -0
- package/dist/cjs/spawn/owned-process-tree.js.map +1 -0
- package/dist/cjs/spawn/spawn-server.d.cts +8 -32
- package/dist/cjs/spawn/spawn-server.d.ts +8 -32
- package/dist/cjs/spawn/spawn-server.js +398 -212
- package/dist/cjs/spawn/spawn-server.js.map +1 -1
- package/dist/cjs/spawn/spawn-servers.d.cts +18 -8
- package/dist/cjs/spawn/spawn-servers.d.ts +18 -8
- package/dist/cjs/spawn/spawn-servers.js +342 -113
- package/dist/cjs/spawn/spawn-servers.js.map +1 -1
- package/dist/cjs/types.d.cts +1 -1
- package/dist/cjs/types.d.ts +1 -1
- package/dist/cjs/types.js.map +1 -1
- package/dist/esm/auth/capability-discovery.d.ts +3 -1
- package/dist/esm/auth/capability-discovery.js +21 -11
- package/dist/esm/auth/capability-discovery.js.map +1 -1
- package/dist/esm/auth/discovery-fetch.d.ts +2 -0
- package/dist/esm/auth/discovery-fetch.js +64 -23
- package/dist/esm/auth/discovery-fetch.js.map +1 -1
- package/dist/esm/auth/interactive-oauth-flow.d.ts +1 -1
- package/dist/esm/auth/interactive-oauth-flow.js +13 -6
- package/dist/esm/auth/interactive-oauth-flow.js.map +1 -1
- package/dist/esm/auth/oauth-callback-listener.d.ts +1 -1
- package/dist/esm/auth/oauth-callback-listener.js +32 -2
- package/dist/esm/auth/oauth-callback-listener.js.map +1 -1
- package/dist/esm/auth/rfc9728-discovery.d.ts +7 -2
- package/dist/esm/auth/rfc9728-discovery.js +40 -16
- package/dist/esm/auth/rfc9728-discovery.js.map +1 -1
- package/dist/esm/auth/types.d.ts +4 -0
- package/dist/esm/auth/types.js.map +1 -1
- package/dist/esm/connection/connect-client.d.ts +9 -1
- package/dist/esm/connection/connect-client.js +97 -35
- package/dist/esm/connection/connect-client.js.map +1 -1
- package/dist/esm/connection/existing-process-transport.d.ts +18 -18
- package/dist/esm/connection/existing-process-transport.js +132 -70
- package/dist/esm/connection/existing-process-transport.js.map +1 -1
- package/dist/esm/connection/wait-for-http-ready.d.ts +1 -1
- package/dist/esm/connection/wait-for-http-ready.js +19 -4
- package/dist/esm/connection/wait-for-http-ready.js.map +1 -1
- package/dist/esm/dcr/dcr-authenticator.d.ts +1 -1
- package/dist/esm/dcr/dcr-authenticator.js +26 -15
- package/dist/esm/dcr/dcr-authenticator.js.map +1 -1
- package/dist/esm/dcr/dynamic-client-registrar.js +2 -1
- package/dist/esm/dcr/dynamic-client-registrar.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/spawn/owned-process-tree.d.ts +12 -0
- package/dist/esm/spawn/owned-process-tree.js +39 -0
- package/dist/esm/spawn/owned-process-tree.js.map +1 -0
- package/dist/esm/spawn/spawn-server.d.ts +8 -32
- package/dist/esm/spawn/spawn-server.js +224 -130
- package/dist/esm/spawn/spawn-server.js.map +1 -1
- package/dist/esm/spawn/spawn-servers.d.ts +18 -8
- package/dist/esm/spawn/spawn-servers.js +180 -55
- package/dist/esm/spawn/spawn-servers.js.map +1 -1
- package/dist/esm/types.d.ts +1 -1
- package/dist/esm/types.js.map +1 -1
- package/package.json +8 -5
- package/schemas/servers.d.ts +24 -0
- 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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"}
|
package/dist/esm/auth/types.d.ts
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|