feat(mcp): add OAuth redirect URI configuration for MCP servers (#7379)

This commit is contained in:
Christopher Tso
2026-01-16 16:36:48 +11:00
committed by GitHub
parent e4a34beb8b
commit 40b275d7e6
6 changed files with 151 additions and 19 deletions

View File

@@ -308,6 +308,8 @@ export namespace MCP {
let authProvider: McpOAuthProvider | undefined
if (!oauthDisabled) {
await McpOAuthCallback.ensureRunning(oauthConfig?.redirectUri)
authProvider = new McpOAuthProvider(
key,
mcp.url,
@@ -315,6 +317,7 @@ export namespace MCP {
clientId: oauthConfig?.clientId,
clientSecret: oauthConfig?.clientSecret,
scope: oauthConfig?.scope,
redirectUri: oauthConfig?.redirectUri,
},
{
onRedirect: async (url) => {
@@ -344,6 +347,7 @@ export namespace MCP {
let lastError: Error | undefined
const connectTimeout = mcp.timeout ?? DEFAULT_TIMEOUT
for (const { name, transport } of transports) {
try {
const client = new Client({
@@ -570,7 +574,8 @@ export namespace MCP {
for (const [clientName, client] of Object.entries(clientsSnapshot)) {
// Only include tools from connected MCPs (skip disabled ones)
if (s.status[clientName]?.status !== "connected") {
const clientStatus = s.status[clientName]?.status
if (clientStatus !== "connected") {
continue
}
@@ -720,8 +725,10 @@ export namespace MCP {
throw new Error(`MCP server ${mcpName} has OAuth explicitly disabled`)
}
// Start the callback server
await McpOAuthCallback.ensureRunning()
// OAuth config is optional - if not provided, we'll use auto-discovery
const oauthConfig = typeof mcpConfig.oauth === "object" ? mcpConfig.oauth : undefined
await McpOAuthCallback.ensureRunning(oauthConfig?.redirectUri)
// Generate and store a cryptographically secure state parameter BEFORE creating the provider
// The SDK will call provider.state() to read this value
@@ -731,8 +738,6 @@ export namespace MCP {
await McpAuth.updateOAuthState(mcpName, oauthState)
// Create a new auth provider for this flow
// OAuth config is optional - if not provided, we'll use auto-discovery
const oauthConfig = typeof mcpConfig.oauth === "object" ? mcpConfig.oauth : undefined
let capturedUrl: URL | undefined
const authProvider = new McpOAuthProvider(
mcpName,
@@ -741,6 +746,7 @@ export namespace MCP {
clientId: oauthConfig?.clientId,
clientSecret: oauthConfig?.clientSecret,
scope: oauthConfig?.scope,
redirectUri: oauthConfig?.redirectUri,
},
{
onRedirect: async (url) => {
@@ -769,6 +775,7 @@ export namespace MCP {
pendingOAuthTransports.set(mcpName, transport)
return { authorizationUrl: capturedUrl.toString() }
}
throw error
}
}
@@ -778,9 +785,9 @@ export namespace MCP {
* Opens the browser and waits for callback.
*/
export async function authenticate(mcpName: string): Promise<Status> {
const { authorizationUrl } = await startAuth(mcpName)
const result = await startAuth(mcpName)
if (!authorizationUrl) {
if (!result.authorizationUrl) {
// Already authenticated
const s = await state()
return s.status[mcpName] ?? { status: "connected" }
@@ -794,9 +801,9 @@ export namespace MCP {
// The SDK has already added the state parameter to the authorization URL
// We just need to open the browser
log.info("opening browser for oauth", { mcpName, url: authorizationUrl, state: oauthState })
log.info("opening browser for oauth", { mcpName, url: result.authorizationUrl, state: oauthState })
try {
const subprocess = await open(authorizationUrl)
const subprocess = await open(result.authorizationUrl)
// The open package spawns a detached process and returns immediately.
// We need to listen for errors which fire asynchronously:
// - "error" event: command not found (ENOENT)
@@ -819,7 +826,7 @@ export namespace MCP {
// Browser opening failed (e.g., in remote/headless sessions like SSH, devcontainers)
// Emit event so CLI can display the URL for manual opening
log.warn("failed to open browser, user must open URL manually", { mcpName, error })
Bus.publish(BrowserOpenFailed, { mcpName, url: authorizationUrl })
Bus.publish(BrowserOpenFailed, { mcpName, url: result.authorizationUrl })
}
// Wait for callback using the OAuth state parameter