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