pi-mcp-adapter 2.30.0 → 2.31.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/CHANGELOG.md +15 -0
- package/README.md +2 -2
- package/commands.ts +1 -1
- package/dist/types.d.ts +1 -1
- package/mcp-auth-flow.ts +105 -32
- package/package.json +1 -1
- package/proxy-modes.ts +22 -12
- package/server-manager.ts +6 -0
- package/types.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.31.0] - 2026-08-28
|
|
11
|
+
|
|
12
|
+
### Highlights
|
|
13
|
+
- MCP Apps-aware servers can now recognize Pi as a UI-capable host and expose interactive resources.
|
|
14
|
+
- UI capability advertising works consistently across legacy and modern MCP protocol negotiation.
|
|
15
|
+
- Manual OAuth callback completion now supports HTTPS redirect URLs for pre-registered clients.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Pi now advertises MCP Apps UI support with the `io.modelcontextprotocol/ui` extension capability, so compatible servers can expose UI resources. Thanks to [@VikashLoomba](https://github.com/VikashLoomba) for #465.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Pre-registered OAuth clients can use HTTPS callback URLs through manual callback completion instead of being rejected as non-local redirects. Thanks to [@jluisrojas](https://github.com/jluisrojas) for PR #464.
|
|
24
|
+
|
|
10
25
|
## [2.30.0] - 2026-08-28
|
|
11
26
|
|
|
12
27
|
### Highlights
|
package/README.md
CHANGED
|
@@ -272,7 +272,7 @@ In the configuration examples below, `30000` is illustrative only. If `requestTi
|
|
|
272
272
|
| `oauth.clientId` | Pre-registered OAuth client ID. MCP 2026 prefers pre-registered clients or Client ID Metadata Documents; this adapter falls back to Dynamic Client Registration when the ID is omitted and the server supports it. |
|
|
273
273
|
| `oauth.clientSecret` | OAuth client secret for confidential clients; a value beginning with `!` runs a command when OAuth authenticates, while `!!` escapes a literal leading `!` |
|
|
274
274
|
| `oauth.scope` | Requested OAuth scopes |
|
|
275
|
-
| `oauth.redirectUri` | Exact
|
|
275
|
+
| `oauth.redirectUri` | Exact redirect URI for browser OAuth. Local `http://` loopback URIs need an explicit port. Pre-registered `https://` callbacks use manual completion by pasting the full callback URL. |
|
|
276
276
|
| `oauth.clientName` | Client display name advertised during Dynamic Client Registration fallback |
|
|
277
277
|
| `oauth.clientUri` | Client homepage URI advertised during Dynamic Client Registration fallback. Defaults to `piConfig.clientUri` from the host's manifest when set, and is omitted rather than guessed under a rebranded host |
|
|
278
278
|
| `oauth.logoUri` | Client logo URL advertised during Dynamic Client Registration fallback (RFC 7591 `logo_uri`). Must be an absolute `http(s)` URL — consent screens fetch it server-side, so local paths render nothing. Omitted from the registration request when unset |
|
|
@@ -304,7 +304,7 @@ Use `"2026-07-28"` to pin that revision. Pinning has no legacy or SSE fallback a
|
|
|
304
304
|
|
|
305
305
|
The stable SDK handles era-specific request envelopes, result decoding, list-changed subscriptions, cancellation, and multi-round-trip sampling/elicitation. The adapter keeps strict OAuth issuer validation in every mode. Adapter-level roots support, standard MCP logging presentation, and configuration/UI for protocol cache hints are not yet implemented.
|
|
306
306
|
|
|
307
|
-
For pre-registered browser OAuth clients, set `oauth.redirectUri` to the exact callback registered with the provider, for example `"http://localhost:3118/callback"`. Dynamic clients normally omit it and use a lazy OS-assigned localhost callback port.
|
|
307
|
+
For pre-registered browser OAuth clients, set `oauth.redirectUri` to the exact callback registered with the provider, for example `"http://localhost:3118/callback"`. Dynamic clients normally omit it and use a lazy OS-assigned localhost callback port. A configured `https://` callback runs in manual mode because the adapter cannot receive a callback on another host. After authorization, copy the full callback URL from the browser address bar and paste it into `/mcp-auth` or `mcp({ action: "auth-complete", ... })`.
|
|
308
308
|
|
|
309
309
|
If an internal authorization server publishes mismatched OAuth metadata and cannot be fixed immediately, set `oauth.skipIssuerMetadataValidation: true` on that server only. This is security-weakening. It disables the RFC 8414 issuer echo check and should not be used for public or untrusted servers.
|
|
310
310
|
|
package/commands.ts
CHANGED
|
@@ -297,7 +297,7 @@ export async function authenticateServer(
|
|
|
297
297
|
return ui.input(
|
|
298
298
|
`Complete ${serverName} OAuth\n\n` +
|
|
299
299
|
`${terminalHyperlink("Open authorization page", authorizationUrl)}\n${authorizationUrl}\n\n` +
|
|
300
|
-
"Approve access, then paste the full
|
|
300
|
+
"Approve access, then paste the full callback URL from the browser address bar below.",
|
|
301
301
|
undefined,
|
|
302
302
|
{ signal: inputSignal },
|
|
303
303
|
);
|
package/dist/types.d.ts
CHANGED
|
@@ -228,7 +228,7 @@ export interface OAuthConfig {
|
|
|
228
228
|
scope?: string;
|
|
229
229
|
/** Extra authorization URL parameters for provider-specific extensions. Flow-owned parameters cannot be overridden. */
|
|
230
230
|
authorizationParams?: Record<string, string>;
|
|
231
|
-
/** Exact authorization-code redirect URI for pre-registered clients */
|
|
231
|
+
/** Exact authorization-code redirect URI for pre-registered clients. HTTPS redirects use manual callback URL completion. */
|
|
232
232
|
redirectUri?: string;
|
|
233
233
|
/** Client display name for dynamic registration */
|
|
234
234
|
clientName?: string;
|
package/mcp-auth-flow.ts
CHANGED
|
@@ -75,6 +75,8 @@ type PendingAuth = {
|
|
|
75
75
|
authProvider: McpOAuthProvider
|
|
76
76
|
serverUrl: string
|
|
77
77
|
authorizationUrl: string
|
|
78
|
+
manualRedirect: boolean
|
|
79
|
+
manualCompletionController?: AbortController
|
|
78
80
|
discovery: AuthDiscovery
|
|
79
81
|
authStorageOptions: AuthStorageOptions
|
|
80
82
|
}
|
|
@@ -307,7 +309,11 @@ async function probeAuthDiscovery(serverUrl: string, definition?: ServerEntry, s
|
|
|
307
309
|
}
|
|
308
310
|
}
|
|
309
311
|
|
|
310
|
-
|
|
312
|
+
type OAuthRedirectTarget =
|
|
313
|
+
| { mode: "local"; port: number; callbackHost: string; callbackPath: string }
|
|
314
|
+
| { mode: "manual" }
|
|
315
|
+
|
|
316
|
+
function parseOAuthRedirectUri(redirectUri: string): OAuthRedirectTarget {
|
|
311
317
|
let url: URL
|
|
312
318
|
try {
|
|
313
319
|
url = new URL(redirectUri)
|
|
@@ -315,12 +321,6 @@ function parseOAuthRedirectUri(redirectUri: string): { port: number; callbackHos
|
|
|
315
321
|
throw new Error(`Invalid OAuth redirectUri: ${redirectUri}`, { cause: error })
|
|
316
322
|
}
|
|
317
323
|
|
|
318
|
-
const hostname = url.hostname.toLowerCase()
|
|
319
|
-
const isLocalhost = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1"
|
|
320
|
-
if (url.protocol !== "http:" || !isLocalhost) {
|
|
321
|
-
throw new Error("OAuth redirectUri must be an http:// localhost or loopback URI")
|
|
322
|
-
}
|
|
323
|
-
|
|
324
324
|
if (url.username || url.password) {
|
|
325
325
|
throw new Error("OAuth redirectUri must not include username or password")
|
|
326
326
|
}
|
|
@@ -329,17 +329,32 @@ function parseOAuthRedirectUri(redirectUri: string): { port: number; callbackHos
|
|
|
329
329
|
throw new Error("OAuth redirectUri must not include a fragment")
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
+
const hostname = url.hostname.toLowerCase()
|
|
333
|
+
const isLocalhost = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1"
|
|
334
|
+
if (url.port) {
|
|
335
|
+
const parsedPort = Number.parseInt(url.port, 10)
|
|
336
|
+
if (!Number.isInteger(parsedPort) || parsedPort <= 0 || parsedPort > 65535) {
|
|
337
|
+
throw new Error("OAuth redirectUri port must be a positive numeric port")
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (url.protocol === "https:" && !isLocalhost) {
|
|
341
|
+
return { mode: "manual" }
|
|
342
|
+
}
|
|
343
|
+
if (url.protocol !== "http:" || !isLocalhost) {
|
|
344
|
+
throw new Error("OAuth redirectUri must be an https:// URI or an http:// localhost or loopback URI")
|
|
345
|
+
}
|
|
346
|
+
|
|
332
347
|
if (!url.port) {
|
|
333
|
-
throw new Error("OAuth redirectUri must include an explicit numeric port")
|
|
348
|
+
throw new Error("OAuth localhost redirectUri must include an explicit numeric port")
|
|
334
349
|
}
|
|
335
350
|
|
|
336
351
|
const port = Number.parseInt(url.port, 10)
|
|
337
352
|
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
338
|
-
throw new Error("OAuth redirectUri must include an explicit numeric port")
|
|
353
|
+
throw new Error("OAuth localhost redirectUri must include an explicit numeric port")
|
|
339
354
|
}
|
|
340
355
|
|
|
341
356
|
const callbackHost = hostname === "[::1]" ? "::1" : hostname
|
|
342
|
-
return { port, callbackHost, callbackPath: url.pathname }
|
|
357
|
+
return { mode: "local", port, callbackHost, callbackPath: url.pathname }
|
|
343
358
|
}
|
|
344
359
|
|
|
345
360
|
/**
|
|
@@ -393,25 +408,30 @@ export async function startAuth(
|
|
|
393
408
|
return { authorizationUrl: existingPendingAuth.authorizationUrl }
|
|
394
409
|
}
|
|
395
410
|
|
|
396
|
-
const
|
|
411
|
+
const redirectTarget = config.redirectUri !== undefined ? parseOAuthRedirectUri(config.redirectUri) : undefined
|
|
412
|
+
const manualRedirect = redirectTarget?.mode === "manual"
|
|
397
413
|
const oauthState = generateState()
|
|
398
414
|
|
|
399
|
-
|
|
400
|
-
await ensureCallbackServer({
|
|
401
|
-
strictPort: Boolean(config.clientId) || config.redirectUri !== undefined,
|
|
402
|
-
oauthState,
|
|
403
|
-
reserveState: true,
|
|
404
|
-
...(redirectCallback ? { port: redirectCallback.port, callbackHost: redirectCallback.callbackHost, callbackPath: redirectCallback.callbackPath } : {}),
|
|
405
|
-
})
|
|
406
|
-
throwIfAborted(signal)
|
|
407
|
-
} catch (error) {
|
|
408
|
-
releaseCallbackServer(oauthState)
|
|
415
|
+
if (!manualRedirect) {
|
|
409
416
|
try {
|
|
410
|
-
await
|
|
411
|
-
|
|
412
|
-
|
|
417
|
+
await ensureCallbackServer({
|
|
418
|
+
strictPort: Boolean(config.clientId) || config.redirectUri !== undefined,
|
|
419
|
+
oauthState,
|
|
420
|
+
reserveState: true,
|
|
421
|
+
...(redirectTarget?.mode === "local"
|
|
422
|
+
? { port: redirectTarget.port, callbackHost: redirectTarget.callbackHost, callbackPath: redirectTarget.callbackPath }
|
|
423
|
+
: {}),
|
|
424
|
+
})
|
|
425
|
+
throwIfAborted(signal)
|
|
426
|
+
} catch (error) {
|
|
427
|
+
releaseCallbackServer(oauthState)
|
|
428
|
+
try {
|
|
429
|
+
await cleanupAndReleaseCallbackServerIfIdle(() => clearOAuthState(serverName, authStorageOptions))
|
|
430
|
+
} catch (cleanupError) {
|
|
431
|
+
throw new AggregateError([error, cleanupError], "OAuth startup cleanup failed")
|
|
432
|
+
}
|
|
433
|
+
throw error
|
|
413
434
|
}
|
|
414
|
-
throw error
|
|
415
435
|
}
|
|
416
436
|
|
|
417
437
|
let capturedUrl: URL | undefined
|
|
@@ -455,7 +475,16 @@ export async function startAuth(
|
|
|
455
475
|
if (!capturedUrl) {
|
|
456
476
|
throw new UnauthorizedError("OAuth authorization URL was not provided")
|
|
457
477
|
}
|
|
458
|
-
await setPendingAuth(runtime, serverName, {
|
|
478
|
+
await setPendingAuth(runtime, serverName, {
|
|
479
|
+
serverName,
|
|
480
|
+
authProvider,
|
|
481
|
+
serverUrl,
|
|
482
|
+
authorizationUrl: capturedUrl.toString(),
|
|
483
|
+
manualRedirect,
|
|
484
|
+
...(manualRedirect ? { manualCompletionController: new AbortController() } : {}),
|
|
485
|
+
discovery,
|
|
486
|
+
authStorageOptions,
|
|
487
|
+
}, oauthState, signal, generation)
|
|
459
488
|
return { authorizationUrl: capturedUrl.toString() }
|
|
460
489
|
} catch (error) {
|
|
461
490
|
authProvider.deactivate()
|
|
@@ -484,7 +513,13 @@ async function setPendingAuth(
|
|
|
484
513
|
state.pendingAuths.set(key, pendingAuth)
|
|
485
514
|
state.pendingAuthStates.set(key, oauthState)
|
|
486
515
|
const cleanupTimer = setTimeout(() => {
|
|
487
|
-
void clearPendingAuthAndReleaseIfIdle(
|
|
516
|
+
void clearPendingAuthAndReleaseIfIdle(
|
|
517
|
+
runtime,
|
|
518
|
+
serverName,
|
|
519
|
+
oauthState,
|
|
520
|
+
pendingAuth.authStorageOptions,
|
|
521
|
+
new Error("OAuth authorization timeout - authorization took too long"),
|
|
522
|
+
).catch(error => {
|
|
488
523
|
console.error(`MCP Auth: Timed-out flow cleanup failed: ${formatTerminalError(error)}`)
|
|
489
524
|
})
|
|
490
525
|
}, MANUAL_AUTH_TIMEOUT_MS)
|
|
@@ -492,7 +527,13 @@ async function setPendingAuth(
|
|
|
492
527
|
state.pendingAuthCleanupTimers.set(key, cleanupTimer)
|
|
493
528
|
}
|
|
494
529
|
|
|
495
|
-
async function clearPendingAuth(
|
|
530
|
+
async function clearPendingAuth(
|
|
531
|
+
runtime: McpOAuthRuntime,
|
|
532
|
+
serverName: string,
|
|
533
|
+
oauthState?: string,
|
|
534
|
+
fallbackStorageOptions: AuthStorageOptions = {},
|
|
535
|
+
reason: Error = new Error("Authorization cancelled"),
|
|
536
|
+
): Promise<void> {
|
|
496
537
|
const state = getRuntimeState(runtime)
|
|
497
538
|
const key = getPendingAuthKey(serverName, fallbackStorageOptions)
|
|
498
539
|
const pendingAuth = state.pendingAuths.get(key)
|
|
@@ -506,6 +547,7 @@ async function clearPendingAuth(runtime: McpOAuthRuntime, serverName: string, oa
|
|
|
506
547
|
state.pendingAuthCleanupTimers.delete(key)
|
|
507
548
|
}
|
|
508
549
|
|
|
550
|
+
pendingAuth?.manualCompletionController?.abort(reason)
|
|
509
551
|
pendingAuth?.authProvider.deactivate()
|
|
510
552
|
state.pendingAuths.delete(key)
|
|
511
553
|
state.pendingAuthStates.delete(key)
|
|
@@ -524,9 +566,10 @@ async function clearPendingAuthAndReleaseIfIdle(
|
|
|
524
566
|
serverName: string,
|
|
525
567
|
oauthState: string | undefined,
|
|
526
568
|
fallbackStorageOptions: AuthStorageOptions = {},
|
|
569
|
+
reason?: Error,
|
|
527
570
|
): Promise<void> {
|
|
528
571
|
await cleanupAndReleaseCallbackServerIfIdle(
|
|
529
|
-
() => clearPendingAuth(runtime, serverName, oauthState, fallbackStorageOptions),
|
|
572
|
+
() => clearPendingAuth(runtime, serverName, oauthState, fallbackStorageOptions, reason),
|
|
530
573
|
)
|
|
531
574
|
}
|
|
532
575
|
|
|
@@ -629,6 +672,19 @@ type AuthorizationResponse = {
|
|
|
629
672
|
source: "callback" | "manual"
|
|
630
673
|
}
|
|
631
674
|
|
|
675
|
+
function waitForManualCompletionCancellation(signal: AbortSignal): Promise<AuthorizationCodeInput> {
|
|
676
|
+
return new Promise((_, reject) => {
|
|
677
|
+
const rejectFromSignal = () => {
|
|
678
|
+
reject(signal.reason instanceof Error ? signal.reason : new Error("Authorization cancelled"))
|
|
679
|
+
}
|
|
680
|
+
if (signal.aborted) {
|
|
681
|
+
rejectFromSignal()
|
|
682
|
+
return
|
|
683
|
+
}
|
|
684
|
+
signal.addEventListener("abort", rejectFromSignal, { once: true })
|
|
685
|
+
})
|
|
686
|
+
}
|
|
687
|
+
|
|
632
688
|
/**
|
|
633
689
|
* Wait for either the localhost callback or a manually pasted redirect URL.
|
|
634
690
|
* The manual input prompt is dismissed as soon as either path finishes.
|
|
@@ -686,7 +742,11 @@ export async function completeAuthFromInput(
|
|
|
686
742
|
throwIfAborted(signal)
|
|
687
743
|
const key = getPendingAuthKey(serverName, fallbackAuthStorageOptions)
|
|
688
744
|
const oauthState = runtimeState.pendingAuthStates.get(key)
|
|
745
|
+
const pendingAuth = runtimeState.pendingAuths.get(key)
|
|
689
746
|
throwIfAborted(signal)
|
|
747
|
+
if (pendingAuth?.manualRedirect && !getSearchParamsFromInput(input.trim())) {
|
|
748
|
+
throw new Error("Paste the full OAuth callback URL, including its code and state parameters")
|
|
749
|
+
}
|
|
690
750
|
const parsed = parseAuthorizationRedirectInput(input, oauthState)
|
|
691
751
|
return completeAuth(serverName, parsed, options)
|
|
692
752
|
}
|
|
@@ -805,15 +865,28 @@ export async function authenticate(
|
|
|
805
865
|
try {
|
|
806
866
|
// Get the state that was already generated and stored in startAuth().
|
|
807
867
|
// Keep this lookup and its abort check inside the cleanup boundary because
|
|
808
|
-
// startAuth
|
|
868
|
+
// startAuth already owns the pending flow at this point.
|
|
809
869
|
oauthState = runtimeState.pendingAuthStates.get(getPendingAuthKey(serverName, authStorageOptions))
|
|
810
870
|
throwIfAborted(signal)
|
|
811
871
|
if (!oauthState) {
|
|
812
872
|
throw new Error("OAuth state not found - this should not happen")
|
|
813
873
|
}
|
|
814
874
|
|
|
815
|
-
|
|
816
|
-
|
|
875
|
+
const pendingAuth = runtimeState.pendingAuths.get(getPendingAuthKey(serverName, authStorageOptions))
|
|
876
|
+
if (!pendingAuth) {
|
|
877
|
+
throw new Error(`No pending OAuth flow for server: ${serverName}`)
|
|
878
|
+
}
|
|
879
|
+
if (pendingAuth.manualRedirect && !options.onAuthorizationInput) {
|
|
880
|
+
throw new Error(
|
|
881
|
+
`OAuth for ${serverName} uses a remote redirect URI. Complete it with auth-start/auth-complete or /mcp-auth.`,
|
|
882
|
+
)
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Register the localhost callback before opening the browser. Remote
|
|
886
|
+
// pre-registered callbacks are completed by pasting their full URL.
|
|
887
|
+
const callbackPromise: Promise<AuthorizationCodeInput> = pendingAuth.manualRedirect
|
|
888
|
+
? waitForManualCompletionCancellation(pendingAuth.manualCompletionController!.signal)
|
|
889
|
+
: waitForCallback(oauthState)
|
|
817
890
|
void callbackPromise.catch(() => {})
|
|
818
891
|
|
|
819
892
|
// Open browser. Always surface the URL first so remote/headless users can copy it
|
package/package.json
CHANGED
package/proxy-modes.ts
CHANGED
|
@@ -143,22 +143,30 @@ function getAuthFailedMessage(state: McpExtensionState, serverName: string, mess
|
|
|
143
143
|
return `OAuth authentication failed for "${serverName}": ${message}. Run mcp({ action: "auth-start", server: "${serverName}" }) to get a browser URL, or /mcp-auth ${serverName} in an interactive local session.`;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
function
|
|
146
|
+
function getRedirectDetails(authorizationUrl: string): { port?: number; remote: boolean } {
|
|
147
147
|
try {
|
|
148
148
|
const redirectUri = new URL(authorizationUrl).searchParams.get("redirect_uri");
|
|
149
|
-
if (!redirectUri) return
|
|
150
|
-
const
|
|
151
|
-
|
|
149
|
+
if (!redirectUri) return { remote: false };
|
|
150
|
+
const redirect = new URL(redirectUri);
|
|
151
|
+
const hostname = redirect.hostname.toLowerCase();
|
|
152
|
+
const local = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1";
|
|
153
|
+
const port = Number.parseInt(redirect.port, 10);
|
|
154
|
+
return {
|
|
155
|
+
...(Number.isInteger(port) ? { port } : {}),
|
|
156
|
+
remote: !local,
|
|
157
|
+
};
|
|
152
158
|
} catch {
|
|
153
|
-
return
|
|
159
|
+
return { remote: false };
|
|
154
160
|
}
|
|
155
161
|
}
|
|
156
162
|
|
|
157
163
|
function formatManualAuthInstructions(serverName: string, authorizationUrl: string): string {
|
|
158
|
-
const
|
|
159
|
-
const
|
|
160
|
-
?
|
|
161
|
-
:
|
|
164
|
+
const redirect = getRedirectDetails(authorizationUrl);
|
|
165
|
+
const redirectNote = redirect.remote
|
|
166
|
+
? "The provider uses a pre-registered HTTPS callback. Copy its full URL from the browser address bar, even if the destination page reports an error."
|
|
167
|
+
: redirect.port
|
|
168
|
+
? `The redirect URL will use local port ${redirect.port}. On a remote server it is expected for that localhost page to fail locally; copy the address bar URL anyway.`
|
|
169
|
+
: "";
|
|
162
170
|
|
|
163
171
|
return [
|
|
164
172
|
`MCP OAuth required for "${serverName}".`,
|
|
@@ -167,11 +175,13 @@ function formatManualAuthInstructions(serverName: string, authorizationUrl: stri
|
|
|
167
175
|
"",
|
|
168
176
|
authorizationUrl,
|
|
169
177
|
"",
|
|
170
|
-
"After approving, copy the full
|
|
178
|
+
"After approving, copy the full callback URL from your browser address bar and send it back with:",
|
|
171
179
|
`mcp({ action: "auth-complete", server: "${serverName}", args: { redirectUrl: "PASTE_REDIRECT_URL_HERE" } })`,
|
|
172
180
|
"",
|
|
173
|
-
|
|
174
|
-
|
|
181
|
+
redirect.remote
|
|
182
|
+
? "Remote HTTPS callbacks must include the full callback URL so the OAuth state can be checked. JSON-string args remain supported."
|
|
183
|
+
: 'You can also pass just the `code` query parameter as `args: { code: "PASTE_CODE_HERE" }`. JSON-string args remain supported.',
|
|
184
|
+
redirectNote,
|
|
175
185
|
].filter(Boolean).join("\n");
|
|
176
186
|
}
|
|
177
187
|
|
package/server-manager.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
import { resolveNpxBinary } from "./npx-resolver.ts";
|
|
35
35
|
import { createJsonSchemaValidator } from "./json-schema-validator.ts";
|
|
36
36
|
import { logger } from "./logger.ts";
|
|
37
|
+
import { RESOURCE_MIME_TYPE } from "./ui-app-bridge-helpers.ts";
|
|
37
38
|
import { McpOAuthProvider } from "./mcp-oauth-provider.ts";
|
|
38
39
|
import { extractOAuthConfig, supportsOAuth, type McpOAuthRuntime } from "./mcp-auth-flow.ts";
|
|
39
40
|
import { invalidateAuthEntryCache, type AuthStorageOptions } from "./mcp-auth.ts";
|
|
@@ -725,6 +726,11 @@ export class McpServerManager {
|
|
|
725
726
|
|
|
726
727
|
private buildClientCapabilities() {
|
|
727
728
|
return {
|
|
729
|
+
extensions: {
|
|
730
|
+
"io.modelcontextprotocol/ui": {
|
|
731
|
+
mimeTypes: [RESOURCE_MIME_TYPE],
|
|
732
|
+
},
|
|
733
|
+
},
|
|
728
734
|
...(this.samplingConfig ? { sampling: {} } : {}),
|
|
729
735
|
...(this.elicitationConfig
|
|
730
736
|
? {
|
package/types.ts
CHANGED
|
@@ -378,7 +378,7 @@ export interface OAuthConfig {
|
|
|
378
378
|
scope?: string;
|
|
379
379
|
/** Extra authorization URL parameters for provider-specific extensions. Flow-owned parameters cannot be overridden. */
|
|
380
380
|
authorizationParams?: Record<string, string>;
|
|
381
|
-
/** Exact authorization-code redirect URI for pre-registered clients */
|
|
381
|
+
/** Exact authorization-code redirect URI for pre-registered clients. HTTPS redirects use manual callback URL completion. */
|
|
382
382
|
redirectUri?: string;
|
|
383
383
|
/** Client display name for dynamic registration */
|
|
384
384
|
clientName?: string;
|