@fitchmultz/pi-mcp-adapter 8.0.5 → 8.0.7
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/CHANGELOG.md +140 -7
- package/OAUTH.md +13 -16
- package/README.md +8 -8
- package/cli.js +67 -28
- package/dist/abort.js +18 -9
- package/dist/client-operations.js +4 -2
- package/dist/commands.js +77 -43
- package/dist/config.js +194 -105
- package/dist/connection-runtime.d.ts +2 -2
- package/dist/connection-runtime.js +29 -18
- package/dist/consent-manager.d.ts +2 -2
- package/dist/consent-manager.js +5 -2
- package/dist/direct-tools.d.ts +1 -1
- package/dist/direct-tools.js +13 -7
- package/dist/elicitation-handler.d.ts +2 -3
- package/dist/elicitation-handler.js +113 -58
- package/dist/gateway-arguments.d.ts +1 -1
- package/dist/gateway-arguments.js +90 -23
- package/dist/glimpse-ui.js +10 -5
- package/dist/host-html-template.d.ts +3 -1
- package/dist/host-html-template.js +9 -12
- package/dist/index.d.ts +1 -1
- package/dist/index.js +264 -119
- package/dist/init.d.ts +1 -3
- package/dist/init.js +91 -51
- package/dist/legacy-migration.d.ts +1 -1
- package/dist/legacy-migration.js +17 -12
- package/dist/lifecycle.d.ts +3 -3
- package/dist/lifecycle.js +33 -17
- package/dist/logger.d.ts +2 -2
- package/dist/logger.js +4 -2
- package/dist/mcp-auth-flow.js +150 -58
- package/dist/mcp-auth.d.ts +7 -7
- package/dist/mcp-auth.js +189 -138
- package/dist/mcp-callback-server.js +19 -8
- package/dist/mcp-code.js +168 -54
- package/dist/mcp-keyring-helper.cjs +57 -23
- package/dist/mcp-oauth-provider.d.ts +6 -7
- package/dist/mcp-oauth-provider.js +146 -72
- package/dist/mcp-oauth-runtime.js +29 -16
- package/dist/mcp-output-guard.js +193 -65
- package/dist/mcp-output-preview.js +41 -20
- package/dist/mcp-panel.d.ts +9 -9
- package/dist/mcp-panel.js +141 -70
- package/dist/mcp-script-worker.mjs +135 -43
- package/dist/mcp-setup-panel.d.ts +8 -8
- package/dist/mcp-setup-panel.js +130 -44
- package/dist/mcp-status.js +14 -7
- package/dist/mcp-trace.d.ts +10 -5
- package/dist/mcp-trace.js +127 -61
- package/dist/metadata-cache.d.ts +3 -4
- package/dist/metadata-cache.js +83 -43
- package/dist/npx-resolver.js +101 -60
- package/dist/onboarding-state.js +4 -2
- package/dist/plain-result-reader.js +78 -31
- package/dist/prompts.js +36 -20
- package/dist/proxy-modes.js +670 -193
- package/dist/resource-tools.js +30 -9
- package/dist/runtime-owner.js +36 -18
- package/dist/sampling-handler.d.ts +1 -2
- package/dist/sampling-handler.js +41 -21
- package/dist/search-ranking.d.ts +1 -1
- package/dist/search-ranking.js +67 -34
- package/dist/server-manager.d.ts +11 -11
- package/dist/server-manager.js +283 -166
- package/dist/session-recovery.js +113 -61
- package/dist/tool-approval.js +9 -3
- package/dist/tool-loader.js +154 -55
- package/dist/tool-metadata.d.ts +1 -1
- package/dist/tool-metadata.js +51 -23
- package/dist/tool-registrar.js +32 -10
- package/dist/tool-result-renderer.d.ts +3 -3
- package/dist/tool-result-renderer.js +65 -44
- package/dist/types.d.ts +2 -2
- package/dist/types.js +39 -18
- package/dist/ui-message-history.js +33 -13
- package/dist/ui-resource-handler.d.ts +3 -3
- package/dist/ui-resource-handler.js +37 -21
- package/dist/ui-server.js +101 -43
- package/dist/ui-session.js +66 -29
- package/dist/ui-stream-types.js +14 -4
- package/dist/ui-tool-visibility.js +12 -6
- package/dist/unix-socket-transport.d.ts +1 -2
- package/dist/unix-socket-transport.js +19 -12
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +60 -35
- package/package.json +29 -5
- package/skills/mcp-scripting/SKILL.md +6 -6
package/dist/mcp-auth-flow.js
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { auth as runSdkAuth, extractWWWAuthenticateParams, UnauthorizedError, Client, StreamableHTTPClientTransport, validateAuthorizationResponseIssuer, InsufficientScopeError, SdkHttpError, computeScopeUnion, isStrictScopeSuperset, } from "@modelcontextprotocol/client";
|
|
7
7
|
import open from "open";
|
|
8
|
-
import { McpOAuthProvider, issuersMatch, loopbackRedirectsMatch, validateOAuthClientMetadataUrl, validateOAuthPrivateKeyJwt, validateOAuthCrossAppAccess } from "./mcp-oauth-provider.js";
|
|
8
|
+
import { McpOAuthProvider, issuersMatch, loopbackRedirectsMatch, validateOAuthClientMetadataUrl, validateOAuthPrivateKeyJwt, validateOAuthCrossAppAccess, } from "./mcp-oauth-provider.js";
|
|
9
9
|
import { ensureCallbackServer, waitForCallback, cancelPendingCallback, releaseCallbackServer, takeCallbackResult, } from "./mcp-callback-server.js";
|
|
10
10
|
import { getAuthForUrl, clearAllCredentials, clearClientInfo, clearTokens, clearCodeVerifier, getOAuthState, clearOAuthState, getAuthBaseDir, } from "./mcp-auth.js";
|
|
11
|
-
import { isServerDisabled, isNonInteractiveOAuth, validateServerProtocolConfig } from "./types.js";
|
|
12
|
-
import { ADAPTER_VERSION, formatTerminalError, interpolateEnvRecord, interpolateEnvVars, normalizeRequestTimeoutMs } from "./utils.js";
|
|
11
|
+
import { isServerDisabled, isNonInteractiveOAuth, validateServerProtocolConfig, } from "./types.js";
|
|
12
|
+
import { ADAPTER_VERSION, formatTerminalError, interpolateEnvRecord, interpolateEnvVars, normalizeRequestTimeoutMs, } from "./utils.js";
|
|
13
13
|
import { abortable, throwIfAborted } from "./abort.js";
|
|
14
14
|
import { combineAbortSignals, isAbortError } from "./runtime-owner.js";
|
|
15
15
|
import { clearPendingAuth, getPendingAuthKey, getRuntime, getRuntimeState, legacyRuntime, } from "./mcp-oauth-runtime.js";
|
|
@@ -18,7 +18,9 @@ function applyConfiguredScope(discovery, config) {
|
|
|
18
18
|
return {
|
|
19
19
|
...discovery,
|
|
20
20
|
...(config.scope !== undefined ? { scope: config.scope } : {}),
|
|
21
|
-
...(config.skipIssuerMetadataValidation !== undefined
|
|
21
|
+
...(config.skipIssuerMetadataValidation !== undefined
|
|
22
|
+
? { skipIssuerMetadataValidation: config.skipIssuerMetadataValidation }
|
|
23
|
+
: {}),
|
|
22
24
|
};
|
|
23
25
|
}
|
|
24
26
|
function validateCallbackIssuer(serverName, context, response) {
|
|
@@ -26,7 +28,11 @@ function validateCallbackIssuer(serverName, context, response) {
|
|
|
26
28
|
throw new Error("Cannot verify the OAuth error callback issuer");
|
|
27
29
|
}
|
|
28
30
|
try {
|
|
29
|
-
validateAuthorizationResponseIssuer({
|
|
31
|
+
validateAuthorizationResponseIssuer({
|
|
32
|
+
expectedIssuer: context.expectedIssuer ?? context.fallbackIssuer,
|
|
33
|
+
issParameterSupported: context.issParameterSupported === true,
|
|
34
|
+
iss: response.iss,
|
|
35
|
+
});
|
|
30
36
|
}
|
|
31
37
|
catch {
|
|
32
38
|
if (response.iss === undefined && context.issParameterSupported) {
|
|
@@ -37,32 +43,39 @@ function validateCallbackIssuer(serverName, context, response) {
|
|
|
37
43
|
}
|
|
38
44
|
}
|
|
39
45
|
export function isOAuthChallenge(error) {
|
|
40
|
-
return error instanceof InsufficientScopeError ||
|
|
41
|
-
|
|
46
|
+
return (error instanceof InsufficientScopeError ||
|
|
47
|
+
error instanceof UnauthorizedError ||
|
|
48
|
+
(error instanceof SdkHttpError && error.status === 401));
|
|
42
49
|
}
|
|
43
50
|
export function getOAuthRequest(serverName, serverUrl, storageBase, runtime = legacyRuntime) {
|
|
44
|
-
if (runtime.signal.aborted)
|
|
51
|
+
if (runtime.signal.aborted) {
|
|
45
52
|
return undefined;
|
|
53
|
+
}
|
|
46
54
|
const request = getRuntimeState(runtime).requests.get(JSON.stringify([serverName, storageBase]));
|
|
47
55
|
return request?.serverUrl === serverUrl ? request : undefined;
|
|
48
56
|
}
|
|
49
57
|
/** Observe native challenges only; no credential reads or auth work here. */
|
|
50
58
|
export function recordOAuthChallenge(serverName, serverUrl, storageBase, error, runtime = legacyRuntime) {
|
|
51
|
-
if (runtime.signal.aborted || !isOAuthChallenge(error))
|
|
59
|
+
if (runtime.signal.aborted || !isOAuthChallenge(error)) {
|
|
52
60
|
return;
|
|
61
|
+
}
|
|
53
62
|
const previous = getOAuthRequest(serverName, serverUrl, storageBase, runtime);
|
|
54
63
|
const request = { ...previous, serverName, serverUrl };
|
|
55
64
|
if (error instanceof InsufficientScopeError) {
|
|
56
65
|
const requiredScope = computeScopeUnion(previous?.challenge?.requiredScope, error.requiredScope);
|
|
57
66
|
const resourceMetadataUrl = error.resourceMetadataUrl ?? previous?.challenge?.resourceMetadataUrl;
|
|
58
|
-
request.challenge = {
|
|
67
|
+
request.challenge = {
|
|
68
|
+
...(requiredScope ? { requiredScope } : {}),
|
|
69
|
+
...(resourceMetadataUrl ? { resourceMetadataUrl } : {}),
|
|
70
|
+
};
|
|
59
71
|
}
|
|
60
72
|
getRuntimeState(runtime).requests.set(JSON.stringify([serverName, storageBase]), request);
|
|
61
73
|
}
|
|
62
74
|
export function bindOAuthRequestIssuer(serverName, serverUrl, storageBase, issuer, runtime = legacyRuntime) {
|
|
63
75
|
const request = getOAuthRequest(serverName, serverUrl, storageBase, runtime);
|
|
64
|
-
if (!request)
|
|
76
|
+
if (!request) {
|
|
65
77
|
return;
|
|
78
|
+
}
|
|
66
79
|
if (request.issuer && !issuersMatch(request.issuer, issuer)) {
|
|
67
80
|
throw new Error(`OAuth authorization server issuer changed for ${serverName}; clear credentials before authenticating again`);
|
|
68
81
|
}
|
|
@@ -87,18 +100,22 @@ export function extractOAuthConfig(definition) {
|
|
|
87
100
|
return {};
|
|
88
101
|
}
|
|
89
102
|
const config = {};
|
|
90
|
-
if (definition.oauth?.skipIssuerMetadataValidation !== undefined)
|
|
103
|
+
if (definition.oauth?.skipIssuerMetadataValidation !== undefined) {
|
|
91
104
|
config.skipIssuerMetadataValidation = definition.oauth.skipIssuerMetadataValidation;
|
|
92
|
-
|
|
105
|
+
}
|
|
106
|
+
if (definition.oauth?.grantType !== undefined) {
|
|
93
107
|
config.grantType = definition.oauth.grantType;
|
|
108
|
+
}
|
|
94
109
|
if (definition.oauth?.clientId !== undefined) {
|
|
95
|
-
if (typeof definition.oauth.clientId !== "string")
|
|
110
|
+
if (typeof definition.oauth.clientId !== "string") {
|
|
96
111
|
throw new Error("OAuth clientId must be a string");
|
|
112
|
+
}
|
|
97
113
|
config.clientId = interpolateEnvVars(definition.oauth.clientId);
|
|
98
114
|
}
|
|
99
115
|
if (definition.oauth?.clientSecret !== undefined) {
|
|
100
|
-
if (typeof definition.oauth.clientSecret !== "string")
|
|
116
|
+
if (typeof definition.oauth.clientSecret !== "string") {
|
|
101
117
|
throw new Error("OAuth clientSecret must be a string");
|
|
118
|
+
}
|
|
102
119
|
// Preserve command expressions for the provider; interpolation remains eager for ordinary values.
|
|
103
120
|
config.clientSecret = definition.oauth.clientSecret.startsWith("!")
|
|
104
121
|
? definition.oauth.clientSecret
|
|
@@ -106,8 +123,9 @@ export function extractOAuthConfig(definition) {
|
|
|
106
123
|
}
|
|
107
124
|
if (definition.oauth?.clientMetadataUrl !== undefined) {
|
|
108
125
|
const value = definition.oauth.clientMetadataUrl;
|
|
109
|
-
if (value !== false && typeof value !== "string")
|
|
126
|
+
if (value !== false && typeof value !== "string") {
|
|
110
127
|
throw new Error("OAuth clientMetadataUrl must be a string or false");
|
|
128
|
+
}
|
|
111
129
|
config.clientMetadataUrl = value === false ? false : interpolateEnvVars(value);
|
|
112
130
|
validateOAuthClientMetadataUrl(config.clientMetadataUrl);
|
|
113
131
|
}
|
|
@@ -115,13 +133,15 @@ export function extractOAuthConfig(definition) {
|
|
|
115
133
|
// Keep environment and command sources lazy; each authentication resolves its own key.
|
|
116
134
|
config.privateKeyJwt = definition.oauth.privateKeyJwt;
|
|
117
135
|
}
|
|
118
|
-
if (definition.oauth?.crossAppAccess !== undefined)
|
|
136
|
+
if (definition.oauth?.crossAppAccess !== undefined) {
|
|
119
137
|
config.crossAppAccess = definition.oauth.crossAppAccess;
|
|
138
|
+
}
|
|
120
139
|
validateOAuthPrivateKeyJwt(config);
|
|
121
140
|
validateOAuthCrossAppAccess(config);
|
|
122
141
|
if (definition.oauth?.scope !== undefined) {
|
|
123
|
-
if (typeof definition.oauth.scope !== "string")
|
|
142
|
+
if (typeof definition.oauth.scope !== "string") {
|
|
124
143
|
throw new Error("OAuth scope must be a string");
|
|
144
|
+
}
|
|
125
145
|
config.scope = interpolateEnvVars(definition.oauth.scope);
|
|
126
146
|
}
|
|
127
147
|
if (definition.oauth?.authorizationParams !== undefined) {
|
|
@@ -131,10 +151,12 @@ export function extractOAuthConfig(definition) {
|
|
|
131
151
|
}
|
|
132
152
|
config.authorizationParams = {};
|
|
133
153
|
for (const [key, value] of Object.entries(params)) {
|
|
134
|
-
if (!key)
|
|
154
|
+
if (!key) {
|
|
135
155
|
throw new Error("OAuth authorizationParams keys must not be empty");
|
|
136
|
-
|
|
156
|
+
}
|
|
157
|
+
if (typeof value !== "string") {
|
|
137
158
|
throw new Error(`OAuth authorizationParams.${key} must be a string`);
|
|
159
|
+
}
|
|
138
160
|
config.authorizationParams[key] = interpolateEnvVars(value);
|
|
139
161
|
}
|
|
140
162
|
}
|
|
@@ -182,15 +204,26 @@ async function probeAuthDiscovery(serverUrl, definition, signal) {
|
|
|
182
204
|
const client = new Client({ name: "pi-mcp-auth-discovery", version: ADAPTER_VERSION }, {
|
|
183
205
|
versionNegotiation: { mode: definition?.protocolVersion ?? "auto" },
|
|
184
206
|
...(isNonInteractiveOAuth(definition?.oauth)
|
|
185
|
-
? {
|
|
186
|
-
|
|
207
|
+
? {
|
|
208
|
+
capabilities: {
|
|
209
|
+
extensions: {
|
|
210
|
+
[definition?.oauth && definition.oauth.crossAppAccess
|
|
211
|
+
? "io.modelcontextprotocol/enterprise-managed-authorization"
|
|
212
|
+
: "io.modelcontextprotocol/oauth-client-credentials"]: {},
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
}
|
|
216
|
+
: {}),
|
|
187
217
|
});
|
|
188
218
|
const transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
|
|
189
219
|
requestInit: { headers },
|
|
190
220
|
fetch: async (input, init) => {
|
|
191
221
|
const response = await fetch(input, init);
|
|
192
222
|
const { resourceMetadataUrl, scope } = extractWWWAuthenticateParams(response);
|
|
193
|
-
discovery = {
|
|
223
|
+
discovery = {
|
|
224
|
+
...(resourceMetadataUrl ? { resourceMetadataUrl } : {}),
|
|
225
|
+
...(scope ? { scope } : {}),
|
|
226
|
+
};
|
|
194
227
|
return response;
|
|
195
228
|
},
|
|
196
229
|
});
|
|
@@ -198,8 +231,9 @@ async function probeAuthDiscovery(serverUrl, definition, signal) {
|
|
|
198
231
|
await abortable(client.connect(transport, { timeout }), discoverySignal);
|
|
199
232
|
}
|
|
200
233
|
catch (error) {
|
|
201
|
-
if (signal?.aborted)
|
|
234
|
+
if (signal?.aborted) {
|
|
202
235
|
throwIfAborted(signal);
|
|
236
|
+
}
|
|
203
237
|
}
|
|
204
238
|
finally {
|
|
205
239
|
await (client.transport ? client.close() : transport.close());
|
|
@@ -215,7 +249,10 @@ function parseOAuthRedirectUri(redirectUri) {
|
|
|
215
249
|
throw new Error(`Invalid OAuth redirectUri: ${redirectUri}`, { cause: error });
|
|
216
250
|
}
|
|
217
251
|
const hostname = url.hostname.toLowerCase();
|
|
218
|
-
const isLocalhost = hostname === "localhost" ||
|
|
252
|
+
const isLocalhost = hostname === "localhost" ||
|
|
253
|
+
hostname === "127.0.0.1" ||
|
|
254
|
+
hostname === "[::1]" ||
|
|
255
|
+
hostname === "::1";
|
|
219
256
|
if (url.protocol !== "http:" || !isLocalhost) {
|
|
220
257
|
throw new Error("OAuth redirectUri must be an http:// localhost or loopback URI");
|
|
221
258
|
}
|
|
@@ -240,8 +277,9 @@ function parseOAuthRedirectUri(redirectUri) {
|
|
|
240
277
|
* Returns the authorization URL when browser authorization is required.
|
|
241
278
|
*/
|
|
242
279
|
export async function startAuth(serverName, serverUrl, definition, options = {}) {
|
|
243
|
-
if (isServerDisabled(definition))
|
|
280
|
+
if (isServerDisabled(definition)) {
|
|
244
281
|
throw new Error(`MCP server "${serverName}" is disabled`);
|
|
282
|
+
}
|
|
245
283
|
const runtime = getRuntime(options);
|
|
246
284
|
const runtimeState = getRuntimeState(runtime);
|
|
247
285
|
const config = definition ? extractOAuthConfig(definition) : {};
|
|
@@ -251,7 +289,10 @@ export async function startAuth(serverName, serverUrl, definition, options = {})
|
|
|
251
289
|
throwIfAborted(signal);
|
|
252
290
|
if (isNonInteractiveOAuth(config)) {
|
|
253
291
|
const storedAuth = await getAuthForUrl(serverName, serverUrl, authStorageOptions);
|
|
254
|
-
if (!config.crossAppAccess &&
|
|
292
|
+
if (!config.crossAppAccess &&
|
|
293
|
+
storedAuth?.clientInfo &&
|
|
294
|
+
!storedAuth.tokens &&
|
|
295
|
+
!config.clientId) {
|
|
255
296
|
clearClientInfo(serverName, authStorageOptions);
|
|
256
297
|
clearCodeVerifier(serverName, authStorageOptions);
|
|
257
298
|
await clearOAuthState(serverName, authStorageOptions);
|
|
@@ -289,8 +330,14 @@ export async function startAuth(serverName, serverUrl, definition, options = {})
|
|
|
289
330
|
strictPort: Boolean(config.clientId) || config.redirectUri !== undefined,
|
|
290
331
|
oauthState,
|
|
291
332
|
reserveState: true,
|
|
292
|
-
validate: response => validateCallbackIssuer(serverName, callbackIssuer, response),
|
|
293
|
-
...(redirectCallback
|
|
333
|
+
validate: (response) => validateCallbackIssuer(serverName, callbackIssuer, response),
|
|
334
|
+
...(redirectCallback
|
|
335
|
+
? {
|
|
336
|
+
port: redirectCallback.port,
|
|
337
|
+
callbackHost: redirectCallback.callbackHost,
|
|
338
|
+
callbackPath: redirectCallback.callbackPath,
|
|
339
|
+
}
|
|
340
|
+
: {}),
|
|
294
341
|
});
|
|
295
342
|
throwIfAborted(signal);
|
|
296
343
|
}
|
|
@@ -311,28 +358,41 @@ export async function startAuth(serverName, serverUrl, definition, options = {})
|
|
|
311
358
|
let discovery = applyConfiguredScope(await probeAuthDiscovery(serverUrl, definition, signal), config);
|
|
312
359
|
if (request?.challenge) {
|
|
313
360
|
const scope = computeScopeUnion(discovery.scope, request.requestedScope, storedAuth?.tokens?.scope, request.challenge.requiredScope);
|
|
314
|
-
discovery = {
|
|
361
|
+
discovery = {
|
|
362
|
+
...discovery,
|
|
363
|
+
...(scope ? { scope } : {}),
|
|
364
|
+
...(request.challenge.resourceMetadataUrl
|
|
365
|
+
? { resourceMetadataUrl: request.challenge.resourceMetadataUrl }
|
|
366
|
+
: {}),
|
|
367
|
+
};
|
|
315
368
|
}
|
|
316
369
|
throwIfAborted(signal);
|
|
317
|
-
const flowConfig = {
|
|
370
|
+
const flowConfig = {
|
|
371
|
+
...config,
|
|
372
|
+
...(request?.challenge && discovery.scope ? { scope: discovery.scope } : {}),
|
|
373
|
+
};
|
|
318
374
|
authProvider = new McpOAuthProvider(serverName, serverUrl, flowConfig, {
|
|
319
|
-
onRedirect: url => {
|
|
375
|
+
onRedirect: (url) => {
|
|
320
376
|
capturedUrl = url;
|
|
321
377
|
const requestedScope = url.searchParams.get("scope") ?? undefined;
|
|
322
378
|
if (requestedScope) {
|
|
323
379
|
discovery = { ...discovery, scope: requestedScope };
|
|
324
|
-
if (request?.challenge)
|
|
380
|
+
if (request?.challenge) {
|
|
325
381
|
flowConfig.scope = requestedScope;
|
|
382
|
+
}
|
|
326
383
|
}
|
|
327
384
|
const current = getOAuthRequest(serverName, serverUrl, storageBase, runtime);
|
|
328
385
|
const scope = computeScopeUnion(current?.requestedScope, requestedScope);
|
|
329
|
-
if (current && scope)
|
|
386
|
+
if (current && scope) {
|
|
330
387
|
current.requestedScope = scope;
|
|
388
|
+
}
|
|
331
389
|
},
|
|
332
|
-
onDiscoveryState: state => {
|
|
390
|
+
onDiscoveryState: (state) => {
|
|
333
391
|
callbackIssuer.expectedIssuer = state.authorizationServerMetadata?.issuer;
|
|
334
392
|
callbackIssuer.fallbackIssuer = state.authorizationServerUrl;
|
|
335
|
-
callbackIssuer.issParameterSupported =
|
|
393
|
+
callbackIssuer.issParameterSupported =
|
|
394
|
+
state.authorizationServerMetadata?.authorization_response_iss_parameter_supported ===
|
|
395
|
+
true;
|
|
336
396
|
bindOAuthRequestIssuer(serverName, serverUrl, storageBase, callbackIssuer.expectedIssuer ?? state.authorizationServerUrl, runtime);
|
|
337
397
|
},
|
|
338
398
|
}, authStorageOptions, runtime.signal, oauthState);
|
|
@@ -345,9 +405,10 @@ export async function startAuth(serverName, serverUrl, definition, options = {})
|
|
|
345
405
|
else {
|
|
346
406
|
const redirectUris = storedAuth.clientInfo.redirectUris;
|
|
347
407
|
const redirectUrl = authProvider.redirectUrl ?? "";
|
|
348
|
-
const matchesRedirect = Array.isArray(redirectUris) &&
|
|
349
|
-
|
|
350
|
-
|
|
408
|
+
const matchesRedirect = Array.isArray(redirectUris) &&
|
|
409
|
+
(redirectUris.includes(redirectUrl) ||
|
|
410
|
+
(storedAuth.clientInfo.registrationType === "cimd" &&
|
|
411
|
+
redirectUris.some((uri) => loopbackRedirectsMatch(redirectUrl, uri))));
|
|
351
412
|
if (!matchesRedirect) {
|
|
352
413
|
clearClientInfo(serverName, authStorageOptions);
|
|
353
414
|
clearTokens(serverName, authStorageOptions);
|
|
@@ -358,8 +419,13 @@ export async function startAuth(serverName, serverUrl, definition, options = {})
|
|
|
358
419
|
}
|
|
359
420
|
throwIfAborted(signal);
|
|
360
421
|
const result = await abortable(runSdkAuth(authProvider, {
|
|
361
|
-
serverUrl,
|
|
362
|
-
...
|
|
422
|
+
serverUrl,
|
|
423
|
+
...discovery,
|
|
424
|
+
...(request?.challenge
|
|
425
|
+
? {
|
|
426
|
+
forceReauthorization: isStrictScopeSuperset(discovery.scope, storedAuth?.tokens?.scope),
|
|
427
|
+
}
|
|
428
|
+
: {}),
|
|
363
429
|
}), signal);
|
|
364
430
|
throwIfAborted(signal);
|
|
365
431
|
if (result === "AUTHORIZED") {
|
|
@@ -371,7 +437,16 @@ export async function startAuth(serverName, serverUrl, definition, options = {})
|
|
|
371
437
|
if (!capturedUrl) {
|
|
372
438
|
throw new UnauthorizedError("OAuth authorization URL was not provided");
|
|
373
439
|
}
|
|
374
|
-
await setPendingAuth(runtime, serverName, {
|
|
440
|
+
await setPendingAuth(runtime, serverName, {
|
|
441
|
+
serverName,
|
|
442
|
+
authProvider,
|
|
443
|
+
serverUrl,
|
|
444
|
+
authorizationUrl: capturedUrl.toString(),
|
|
445
|
+
discovery,
|
|
446
|
+
callbackIssuer: { ...callbackIssuer },
|
|
447
|
+
authStorageOptions,
|
|
448
|
+
...(request ? { request } : {}),
|
|
449
|
+
}, oauthState, signal, generation);
|
|
375
450
|
return { authorizationUrl: capturedUrl.toString() };
|
|
376
451
|
}
|
|
377
452
|
catch (error) {
|
|
@@ -390,12 +465,13 @@ async function setPendingAuth(runtime, serverName, pendingAuth, oauthState, sign
|
|
|
390
465
|
const key = getPendingAuthKey(serverName, pendingAuth.authStorageOptions);
|
|
391
466
|
await clearPendingAuth(runtime, serverName, undefined, pendingAuth.authStorageOptions);
|
|
392
467
|
throwIfAborted(signal);
|
|
393
|
-
if (generation !== state.generation)
|
|
468
|
+
if (generation !== state.generation) {
|
|
394
469
|
throw new Error("OAuth runtime stopped");
|
|
470
|
+
}
|
|
395
471
|
state.pendingAuths.set(key, pendingAuth);
|
|
396
472
|
state.pendingAuthStates.set(key, oauthState);
|
|
397
473
|
const cleanupTimer = setTimeout(() => {
|
|
398
|
-
void clearPendingAuth(runtime, serverName, oauthState, pendingAuth.authStorageOptions).catch(error => {
|
|
474
|
+
void clearPendingAuth(runtime, serverName, oauthState, pendingAuth.authStorageOptions).catch((error) => {
|
|
399
475
|
console.error(`MCP Auth: Timed-out flow cleanup failed: ${formatTerminalError(error)}`);
|
|
400
476
|
});
|
|
401
477
|
}, MANUAL_AUTH_TIMEOUT_MS);
|
|
@@ -410,8 +486,9 @@ function getSearchParamsFromInput(input) {
|
|
|
410
486
|
const hash = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash;
|
|
411
487
|
const hashParams = new URLSearchParams(hash);
|
|
412
488
|
for (const [key, value] of hashParams) {
|
|
413
|
-
if (!params.has(key))
|
|
489
|
+
if (!params.has(key)) {
|
|
414
490
|
params.set(key, value);
|
|
491
|
+
}
|
|
415
492
|
}
|
|
416
493
|
}
|
|
417
494
|
return params;
|
|
@@ -440,7 +517,11 @@ export function parseAuthorizationRedirectInput(input, expectedState) {
|
|
|
440
517
|
if (error) {
|
|
441
518
|
const description = params.get("error_description");
|
|
442
519
|
const iss = params.get("iss");
|
|
443
|
-
return {
|
|
520
|
+
return {
|
|
521
|
+
error,
|
|
522
|
+
...(description !== null ? { errorDescription: description } : {}),
|
|
523
|
+
...(iss !== null ? { iss } : {}),
|
|
524
|
+
};
|
|
444
525
|
}
|
|
445
526
|
const code = params.get("code");
|
|
446
527
|
if (code) {
|
|
@@ -485,21 +566,25 @@ export async function completeAuth(serverName, authorizationCode, options = {})
|
|
|
485
566
|
}
|
|
486
567
|
const oauthState = runtimeState.pendingAuthStates.get(key);
|
|
487
568
|
throwIfAborted(signal);
|
|
488
|
-
const response = typeof authorizationCode === "string"
|
|
489
|
-
|
|
569
|
+
const response = typeof authorizationCode === "string"
|
|
570
|
+
? { code: authorizationCode }
|
|
571
|
+
: (authorizationCode ?? (oauthState ? takeCallbackResult(oauthState) : undefined));
|
|
490
572
|
if (!response) {
|
|
491
573
|
throw new Error(`No OAuth callback received yet for ${serverName}. Finish authorization in the browser, then retry auth-complete. ` +
|
|
492
|
-
|
|
574
|
+
'If the browser cannot reach this Pi session\'s callback, pass args: { redirectUrl: "FULL_REDIRECT_URL" } instead.');
|
|
493
575
|
}
|
|
494
576
|
const { iss } = response;
|
|
495
577
|
let keepPendingForRetry = false;
|
|
496
578
|
let caughtError;
|
|
497
579
|
try {
|
|
498
|
-
keepPendingForRetry =
|
|
580
|
+
keepPendingForRetry =
|
|
581
|
+
iss === undefined && pendingAuth.callbackIssuer.issParameterSupported === true;
|
|
499
582
|
validateCallbackIssuer(serverName, pendingAuth.callbackIssuer, response);
|
|
500
583
|
if ("error" in response) {
|
|
501
584
|
keepPendingForRetry = true;
|
|
502
|
-
throw new Error(response.errorDescription
|
|
585
|
+
throw new Error(response.errorDescription
|
|
586
|
+
? `${response.error}: ${response.errorDescription}`
|
|
587
|
+
: response.error);
|
|
503
588
|
}
|
|
504
589
|
const result = await abortable(runSdkAuth(pendingAuth.authProvider, {
|
|
505
590
|
serverUrl: pendingAuth.serverUrl,
|
|
@@ -512,11 +597,14 @@ export async function completeAuth(serverName, authorizationCode, options = {})
|
|
|
512
597
|
throw new UnauthorizedError("Failed to authorize");
|
|
513
598
|
}
|
|
514
599
|
const request = pendingAuth.request;
|
|
515
|
-
if (request?.challenge?.requiredScope
|
|
516
|
-
|
|
600
|
+
if (request?.challenge?.requiredScope &&
|
|
601
|
+
request ===
|
|
602
|
+
getOAuthRequest(serverName, pendingAuth.serverUrl, getAuthBaseDir(authStorageOptions), runtime)) {
|
|
517
603
|
const issuedScope = (await getAuthForUrl(serverName, pendingAuth.serverUrl, authStorageOptions))?.tokens?.scope;
|
|
518
|
-
if (issuedScope !== undefined &&
|
|
604
|
+
if (issuedScope !== undefined &&
|
|
605
|
+
!isStrictScopeSuperset(request.challenge.requiredScope, issuedScope)) {
|
|
519
606
|
delete request.challenge;
|
|
607
|
+
}
|
|
520
608
|
}
|
|
521
609
|
return "authenticated";
|
|
522
610
|
}
|
|
@@ -547,8 +635,9 @@ export async function completeAuth(serverName, authorizationCode, options = {})
|
|
|
547
635
|
* @returns The final auth status
|
|
548
636
|
*/
|
|
549
637
|
export async function authenticate(serverName, serverUrl, definition, options = {}) {
|
|
550
|
-
if (isServerDisabled(definition))
|
|
638
|
+
if (isServerDisabled(definition)) {
|
|
551
639
|
throw new Error(`MCP server "${serverName}" is disabled`);
|
|
640
|
+
}
|
|
552
641
|
const runtime = getRuntime(options);
|
|
553
642
|
const runtimeState = getRuntimeState(runtime);
|
|
554
643
|
const authStorageOptions = options.authStorageOptions ?? {};
|
|
@@ -595,8 +684,9 @@ export async function authenticate(serverName, serverUrl, definition, options =
|
|
|
595
684
|
await abortable(open(authorizationUrl), signal);
|
|
596
685
|
}
|
|
597
686
|
catch (error) {
|
|
598
|
-
if (isAbortError(error, signal))
|
|
687
|
+
if (isAbortError(error, signal)) {
|
|
599
688
|
throw error;
|
|
689
|
+
}
|
|
600
690
|
console.warn(`MCP Auth: Failed to open browser for ${serverName}; waiting for manual callback`, { error });
|
|
601
691
|
}
|
|
602
692
|
// Wait for callback
|
|
@@ -611,8 +701,9 @@ export async function authenticate(serverName, serverUrl, definition, options =
|
|
|
611
701
|
});
|
|
612
702
|
}
|
|
613
703
|
catch (error) {
|
|
614
|
-
if (oauthState)
|
|
704
|
+
if (oauthState) {
|
|
615
705
|
cancelPendingCallback(oauthState);
|
|
706
|
+
}
|
|
616
707
|
try {
|
|
617
708
|
await clearPendingAuth(runtime, serverName, oauthState, authStorageOptions);
|
|
618
709
|
}
|
|
@@ -650,8 +741,9 @@ export async function removeAuth(serverName, options = {}) {
|
|
|
650
741
|
await clearPendingAuth(runtime, serverName, oauthState, authStorageOptions);
|
|
651
742
|
throwIfAborted(signal);
|
|
652
743
|
for (const [key, request] of getRuntimeState(runtime).requests) {
|
|
653
|
-
if (request.serverName === serverName)
|
|
744
|
+
if (request.serverName === serverName) {
|
|
654
745
|
getRuntimeState(runtime).requests.delete(key);
|
|
746
|
+
}
|
|
655
747
|
}
|
|
656
748
|
clearAllCredentials(serverName, authStorageOptions);
|
|
657
749
|
await clearOAuthState(serverName, authStorageOptions);
|
package/dist/mcp-auth.d.ts
CHANGED
|
@@ -52,16 +52,16 @@ export interface AuthStorageOptions {
|
|
|
52
52
|
}
|
|
53
53
|
export declare class OAuthCredentialStoreError extends Error {
|
|
54
54
|
readonly code = "OAUTH_CREDENTIAL_STORE_UNAVAILABLE";
|
|
55
|
-
readonly operation:
|
|
56
|
-
constructor(message: string, operation:
|
|
55
|
+
readonly operation: "read" | "write" | "remove";
|
|
56
|
+
constructor(message: string, operation: "read" | "write" | "remove", cause: unknown);
|
|
57
57
|
}
|
|
58
58
|
export type OAuthCredentialStatus = {
|
|
59
|
-
status:
|
|
59
|
+
status: "present";
|
|
60
60
|
entry: AuthEntry;
|
|
61
61
|
} | {
|
|
62
|
-
status:
|
|
62
|
+
status: "absent";
|
|
63
63
|
} | {
|
|
64
|
-
status:
|
|
64
|
+
status: "unavailable";
|
|
65
65
|
message: string;
|
|
66
66
|
};
|
|
67
67
|
export declare function formatOAuthCredentialStoreUnavailable(error: OAuthCredentialStoreError): string;
|
|
@@ -70,9 +70,9 @@ export declare function getAuthBaseDir(options?: AuthStorageOptions): string;
|
|
|
70
70
|
/**
|
|
71
71
|
* Get the legacy plaintext tokens file path for a server.
|
|
72
72
|
*/
|
|
73
|
-
export declare function getAuthEntryFilePath(serverName:
|
|
73
|
+
export declare function getAuthEntryFilePath(serverName: unknown, options?: AuthStorageOptions): string;
|
|
74
74
|
/** Explicit copy-only upgrade. Normal reads and logout never access the old service. */
|
|
75
|
-
export declare function migrateLegacyAuthEntry(serverName: string, legacyDirectory?: string):
|
|
75
|
+
export declare function migrateLegacyAuthEntry(serverName: string, legacyDirectory?: string): "copied" | "existing" | "absent";
|
|
76
76
|
/**
|
|
77
77
|
* Get auth entry for a server.
|
|
78
78
|
*/
|