Skip to content

Commit 8fd2c5c

Browse files
authored
Fix: MCP OAuth authentication for servers with client allowlists (#88)
1 parent 7f701da commit 8fd2c5c

4 files changed

Lines changed: 78 additions & 24 deletions

File tree

src/main/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ const FAVICON_SVG = `<svg width="32" height="32" viewBox="0 0 1024 1024" fill="n
248248
const FAVICON_DATA_URI = `data:image/svg+xml,${encodeURIComponent(FAVICON_SVG)}`
249249

250250
// Start local HTTP server for auth callbacks
251-
// This catches http://localhost:{AUTH_SERVER_PORT}/auth/callback?code=xxx and /mcp-oauth/callback
251+
// This catches http://localhost:{AUTH_SERVER_PORT}/auth/callback?code=xxx and /callback (for MCP OAuth)
252252
const server = createServer((req, res) => {
253253
const url = new URL(req.url || "", `http://localhost:${AUTH_SERVER_PORT}`)
254254

@@ -339,8 +339,8 @@ const server = createServer((req, res) => {
339339
res.writeHead(400, { "Content-Type": "text/plain" })
340340
res.end("Missing code parameter")
341341
}
342-
} else if (url.pathname === "/mcp-oauth/callback") {
343-
// Handle MCP OAuth callback in dev mode
342+
} else if (url.pathname === "/callback") {
343+
// Handle MCP OAuth callback
344344
const code = url.searchParams.get("code")
345345
const state = url.searchParams.get("state")
346346
console.log(

src/main/lib/mcp-auth.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,8 @@ const OAUTH_TIMEOUT_MS = 5 * 60 * 1000;
139139

140140
function getMcpOAuthRedirectUri(): string {
141141
return IS_DEV
142-
? `http://localhost:${AUTH_SERVER_PORT}/mcp-oauth/callback`
143-
: `http://127.0.0.1:${AUTH_SERVER_PORT}/mcp-oauth/callback`;
142+
? `http://localhost:${AUTH_SERVER_PORT}/callback`
143+
: `http://127.0.0.1:${AUTH_SERVER_PORT}/callback`;
144144
}
145145

146146
interface PendingOAuth {
@@ -149,6 +149,7 @@ interface PendingOAuth {
149149
codeVerifier: string;
150150
tokenEndpoint: string;
151151
clientId: string;
152+
clientSecret?: string;
152153
redirectUri: string;
153154
resolve: (result: { success: boolean; error?: string }) => void;
154155
timeoutId: NodeJS.Timeout;
@@ -169,7 +170,7 @@ export async function startMcpOAuth(
169170
const serverConfig = getMcpServerConfig(config, projectPath, serverName);
170171

171172
if (!serverConfig?.url) {
172-
throw new Error(`MCP server "${serverName}" URL not configured`);
173+
return { success: false, error: `MCP server "${serverName}" URL not configured` };
173174
}
174175

175176
// 2. Use CraftOAuth for OAuth logic
@@ -180,7 +181,16 @@ export async function startMcpOAuth(
180181
);
181182

182183
// 3. Start OAuth flow (fetches metadata from .well-known, then gets auth URL)
183-
const { authUrl, state, codeVerifier, tokenEndpoint, clientId } = await oauth.startAuthFlow();
184+
let authFlowResult;
185+
try {
186+
authFlowResult = await oauth.startAuthFlow();
187+
} catch (error) {
188+
const msg = error instanceof Error ? error.message : String(error);
189+
console.error(`[MCP OAuth] Failed to start auth flow: ${msg}`);
190+
return { success: false, error: msg };
191+
}
192+
193+
const { authUrl, state, codeVerifier, tokenEndpoint, clientId, clientSecret } = authFlowResult;
184194

185195
// 4. Store pending flow and wait for callback
186196
return new Promise((resolve) => {
@@ -195,6 +205,7 @@ export async function startMcpOAuth(
195205
codeVerifier,
196206
tokenEndpoint,
197207
clientId,
208+
clientSecret,
198209
redirectUri,
199210
resolve,
200211
timeoutId,
@@ -237,7 +248,8 @@ export async function handleMcpOAuthCallback(code: string, state: string): Promi
237248
code,
238249
pending.codeVerifier,
239250
pending.tokenEndpoint,
240-
pending.clientId
251+
pending.clientId,
252+
pending.clientSecret
241253
);
242254

243255
// 3. Save to ~/.claude.json

src/main/lib/oauth.ts

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,11 @@ export interface OAuthCallbacks {
4545
}
4646

4747
const CALLBACK_PORT = 8914;
48-
const CALLBACK_PATH = '/oauth/callback';
48+
const CALLBACK_PATH = '/callback';
49+
// Client names for OAuth registration
50+
// Some MCP servers (like Figma) have an allowlist - try '1code' first, fall back to 'Codex'
4951
const CLIENT_NAME = '1code';
52+
const FALLBACK_CLIENT_NAME = 'Codex';
5053

5154
/**
5255
* Generate a styled OAuth callback page with terminal emulator aesthetic
@@ -537,7 +540,7 @@ export class CraftOAuth {
537540
}
538541

539542
// Register OAuth client dynamically
540-
private async registerClient(registrationEndpoint: string): Promise<{
543+
private async registerClient(registrationEndpoint: string, clientName: string): Promise<{
541544
client_id: string;
542545
client_secret?: string;
543546
}> {
@@ -547,7 +550,7 @@ export class CraftOAuth {
547550
method: 'POST',
548551
headers: { 'Content-Type': 'application/json' },
549552
body: JSON.stringify({
550-
client_name: CLIENT_NAME,
553+
client_name: clientName,
551554
redirect_uris: [redirectUri],
552555
grant_types: ['authorization_code', 'refresh_token'],
553556
response_types: ['code'],
@@ -572,7 +575,8 @@ export class CraftOAuth {
572575
code: string,
573576
codeVerifier: string,
574577
clientId: string,
575-
redirectUri?: string
578+
redirectUri?: string,
579+
clientSecret?: string
576580
): Promise<OAuthTokens> {
577581
const uri = redirectUri || this.config.redirectUri || `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
578582

@@ -584,6 +588,11 @@ export class CraftOAuth {
584588
code_verifier: codeVerifier,
585589
});
586590

591+
// Add client_secret if provided (some servers require it)
592+
if (clientSecret) {
593+
params.set('client_secret', clientSecret);
594+
}
595+
587596
const response = await fetch(tokenEndpoint, {
588597
method: 'POST',
589598
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
@@ -686,15 +695,24 @@ export class CraftOAuth {
686695
// Register client if endpoint available
687696
let clientId: string;
688697
if (metadata.registration_endpoint) {
689-
this.callbacks.onStatus(`Registering client at ${metadata.registration_endpoint}...`);
698+
// Try primary client name first, fall back to alternative if rejected
699+
this.callbacks.onStatus(`Registering client as '${CLIENT_NAME}'...`);
690700
try {
691-
const client = await this.registerClient(metadata.registration_endpoint);
701+
const client = await this.registerClient(metadata.registration_endpoint, CLIENT_NAME);
692702
clientId = client.client_id;
693703
this.callbacks.onStatus(`Registered as client: ${clientId}`);
694704
} catch (error) {
695-
const msg = error instanceof Error ? error.message : 'Unknown error';
696-
this.callbacks.onStatus(`Client registration failed: ${msg}`);
697-
throw error;
705+
// Try fallback client name (some servers have allowlists)
706+
this.callbacks.onStatus(`Registration as '${CLIENT_NAME}' failed, trying '${FALLBACK_CLIENT_NAME}'...`);
707+
try {
708+
const client = await this.registerClient(metadata.registration_endpoint, FALLBACK_CLIENT_NAME);
709+
clientId = client.client_id;
710+
this.callbacks.onStatus(`Registered as client: ${clientId}`);
711+
} catch (fallbackError) {
712+
const msg = fallbackError instanceof Error ? fallbackError.message : 'Unknown error';
713+
this.callbacks.onStatus(`Client registration failed: ${msg}`);
714+
throw fallbackError;
715+
}
698716
}
699717
} else {
700718
// Use a default client ID for public clients
@@ -754,16 +772,32 @@ export class CraftOAuth {
754772
codeVerifier: string;
755773
tokenEndpoint: string;
756774
clientId: string;
775+
clientSecret?: string;
757776
}> {
758777
this.callbacks.onStatus('Fetching OAuth server configuration...');
759778
const metadata = preloadedMetadata || await this.getServerMetadata();
760779

761780
// Register client if endpoint available
762781
let clientId: string;
782+
let clientSecret: string | undefined;
763783
if (metadata.registration_endpoint) {
764-
const client = await this.registerClient(metadata.registration_endpoint);
765-
clientId = client.client_id;
784+
// Try primary client name first, fall back to alternative if rejected
785+
this.callbacks.onStatus(`Registering client as '${CLIENT_NAME}'...`);
786+
try {
787+
const client = await this.registerClient(metadata.registration_endpoint, CLIENT_NAME);
788+
clientId = client.client_id;
789+
clientSecret = client.client_secret;
790+
this.callbacks.onStatus(`Registered as client: ${clientId}`);
791+
} catch (error) {
792+
// Try fallback client name (some servers have allowlists)
793+
this.callbacks.onStatus(`Registration as '${CLIENT_NAME}' failed, trying '${FALLBACK_CLIENT_NAME}'...`);
794+
const client = await this.registerClient(metadata.registration_endpoint, FALLBACK_CLIENT_NAME);
795+
clientId = client.client_id;
796+
clientSecret = client.client_secret;
797+
this.callbacks.onStatus(`Registered as client: ${clientId}`);
798+
}
766799
} else {
800+
// No registration endpoint - use default client ID
767801
clientId = '1code';
768802
}
769803

@@ -785,6 +819,7 @@ export class CraftOAuth {
785819
codeVerifier: pkce.verifier,
786820
tokenEndpoint: metadata.token_endpoint,
787821
clientId,
822+
clientSecret,
788823
};
789824
}
790825

@@ -795,10 +830,11 @@ export class CraftOAuth {
795830
code: string,
796831
codeVerifier: string,
797832
tokenEndpoint: string,
798-
clientId: string
833+
clientId: string,
834+
clientSecret?: string
799835
): Promise<OAuthTokens> {
800836
const redirectUri = this.config.redirectUri || `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
801-
return this.exchangeCodeForTokens(tokenEndpoint, code, codeVerifier, clientId, redirectUri);
837+
return this.exchangeCodeForTokens(tokenEndpoint, code, codeVerifier, clientId, redirectUri, clientSecret);
802838
}
803839

804840
// Start local HTTP server to receive OAuth callback

src/renderer/components/dialogs/settings-tabs/agents-mcp-tab.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,11 @@ function ServerRow({ server, isExpanded, onToggle, onAuth }: ServerRowProps) {
8080

8181
return (
8282
<div>
83-
<button
83+
<div
84+
role={hasTools ? "button" : undefined}
85+
tabIndex={hasTools ? 0 : undefined}
8486
onClick={hasTools ? onToggle : undefined}
87+
onKeyDown={hasTools ? (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onToggle(); } } : undefined}
8588
className={cn(
8689
"w-full flex items-center gap-3 p-3 text-left transition-colors",
8790
hasTools && "hover:bg-muted/50 cursor-pointer",
@@ -140,7 +143,7 @@ function ServerRow({ server, isExpanded, onToggle, onAuth }: ServerRowProps) {
140143
{isConnected ? "Reconnect" : "Auth"}
141144
</Button>
142145
)}
143-
</button>
146+
</div>
144147

145148
{/* Expanded tools list */}
146149
<AnimatePresence>
@@ -233,7 +236,10 @@ export function AgentsMcpTab() {
233236
toast.error(result.error || "Authentication failed")
234237
}
235238
} catch (error) {
236-
toast.error("Authentication failed")
239+
// Extract actual error message from tRPC error
240+
const message = error instanceof Error ? error.message : "Authentication failed";
241+
console.error(`[MCP Auth] Error authenticating ${serverName}:`, error);
242+
toast.error(message)
237243
}
238244
}
239245

0 commit comments

Comments
 (0)