Skip to content

Commit 28f5a0e

Browse files
committed
fix: multiple fixes on auth flow edge cases
1 parent 71b3595 commit 28f5a0e

File tree

3 files changed

+47
-25
lines changed

3 files changed

+47
-25
lines changed

server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpEventHandler.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export class McpEventHandler {
8787
* Handles the list MCP servers event
8888
*/
8989
async onListMcpServers(params: ListMcpServersParams) {
90+
this.#isProgrammaticChange = false
9091
this.#currentEditingServerName = undefined
9192
const mcpManager = McpManager.instance
9293

@@ -797,7 +798,7 @@ export class McpEventHandler {
797798
command: selectedTransport === TransportType.STDIO ? params.optionsValues.command : undefined,
798799
url: selectedTransport === TransportType.HTTP ? params.optionsValues.url : undefined,
799800
enabled: true,
800-
numTools: McpManager.instance.getAllToolsWithPermissions(serverName).length,
801+
numTools: McpManager.instance.getAllToolsWithPermissions(sanitizedServerName).length,
801802
scope: params.optionsValues['scope'] === 'global' ? 'global' : 'workspace',
802803
transportType: selectedTransport,
803804
languageServerVersion: this.#features.runtime.serverInfo.version,
@@ -1430,7 +1431,8 @@ export class McpEventHandler {
14301431
*/
14311432
#getServerStatusError(serverName: string): { title: string; icon: string; status: Status } | undefined {
14321433
const serverStates = McpManager.instance.getAllServerStates()
1433-
const serverState = serverStates.get(serverName)
1434+
const key = serverName ? sanitizeName(serverName) : serverName
1435+
const serverState = key ? serverStates.get(key) : undefined
14341436

14351437
if (!serverState) {
14361438
return undefined
@@ -1494,7 +1496,6 @@ export class McpEventHandler {
14941496
if (!this.#lastProgrammaticState) {
14951497
await this.#handleRefreshMCPList({ id: 'refresh-mcp-list' })
14961498
} else {
1497-
this.#isProgrammaticChange = false
14981499
this.#features.logging.debug('Skipping refresh due to programmatic change')
14991500
}
15001501
this.#debounceTimer = null

server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ export class McpManager {
170170
/**
171171
* Load configurations and initialize each enabled server.
172172
*/
173-
private async discoverAllServers(authIntent: AuthIntent = AuthIntent.Silent): Promise<void> {
173+
private async discoverAllServers(): Promise<void> {
174174
// Load agent config
175175
const result = await loadAgentConfig(this.features.workspace, this.features.logging, this.agentPaths)
176176

@@ -221,7 +221,7 @@ export class McpManager {
221221
// Process servers in batches
222222
for (let i = 0; i < totalServers; i += MAX_CONCURRENT_SERVERS) {
223223
const batch = serversToInit.slice(i, i + MAX_CONCURRENT_SERVERS)
224-
const batchPromises = batch.map(([name, cfg]) => this.initOneServer(name, cfg, authIntent))
224+
const batchPromises = batch.map(([name, cfg]) => this.initOneServer(name, cfg, AuthIntent.Silent))
225225

226226
this.features.logging.debug(
227227
`MCP: initializing batch of ${batch.length} servers (${i + 1}-${Math.min(i + MAX_CONCURRENT_SERVERS, totalServers)} of ${totalServers})`
@@ -373,19 +373,29 @@ export class McpManager {
373373

374374
if (needsOAuth) {
375375
OAuthClient.initialize(this.features.workspace, this.features.logging)
376-
const bearer = await OAuthClient.getValidAccessToken(base, {
377-
interactive: authIntent === 'interactive',
378-
})
379-
// add authorization header if we are able to obtain a bearer token
380-
if (bearer) {
381-
headers = { ...headers, Authorization: `Bearer ${bearer}` }
382-
} else if (authIntent === 'silent') {
383-
// In silent mode we never launch a browser. If we cannot obtain a token
384-
// from cache/refresh, surface a clear auth-required error and stop here.
385-
throw new AgenticChatError(
386-
`MCP: server '${serverName}' requires OAuth. Open "Edit MCP Server" and save to sign in.`,
387-
'MCPServerAuthFailed'
388-
)
376+
try {
377+
const bearer = await OAuthClient.getValidAccessToken(base, {
378+
interactive: authIntent === AuthIntent.Interactive,
379+
})
380+
if (bearer) {
381+
headers = { ...headers, Authorization: `Bearer ${bearer}` }
382+
} else if (authIntent === AuthIntent.Silent) {
383+
throw new AgenticChatError(
384+
`MCP: server '${serverName}' requires OAuth. Open "Edit MCP Server" and save to sign in.`,
385+
'MCPServerAuthFailed'
386+
)
387+
}
388+
} catch (e: any) {
389+
const msg = e?.message || ''
390+
const short = /authorization_timed_out/i.test(msg)
391+
? 'Sign-in timed out. Please try again.'
392+
: /Authorization error|PKCE|access_denied|login|consent|token exchange failed/i.test(
393+
msg
394+
)
395+
? 'Sign-in was cancelled or failed. Please try again.'
396+
: `OAuth failed: ${msg}`
397+
398+
throw new AgenticChatError(`MCP: ${short}`, 'MCPServerAuthFailed')
389399
}
390400
}
391401

@@ -1156,15 +1166,16 @@ export class McpManager {
11561166
*/
11571167
public async removeServerFromConfigFile(serverName: string): Promise<void> {
11581168
try {
1159-
const cfg = this.mcpServers.get(serverName)
1169+
const sanitized = sanitizeName(serverName)
1170+
const cfg = this.mcpServers.get(sanitized)
11601171
if (!cfg || !cfg.__configPath__) {
11611172
this.features.logging.warn(
11621173
`Cannot remove config for server '${serverName}': Config not found or missing path`
11631174
)
11641175
return
11651176
}
11661177

1167-
const unsanitizedName = this.serverNameMapping.get(serverName) || serverName
1178+
const unsanitizedName = this.serverNameMapping.get(sanitized) || serverName
11681179

11691180
// Remove from agent config
11701181
if (unsanitizedName && this.agentConfig) {

server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpOauthClient.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ export class OAuthClient {
4646
*/
4747
public static async getValidAccessToken(
4848
mcpBase: URL,
49-
opts: { interactive?: boolean } = { interactive: true }
49+
opts: { interactive?: boolean } = { interactive: false }
5050
): Promise<string | undefined> {
51-
const interactive = opts?.interactive !== false
51+
const interactive = opts?.interactive === true
5252
const key = this.computeKey(mcpBase)
5353
const regPath = path.join(this.cacheDir, `${key}.registration.json`)
5454
const tokPath = path.join(this.cacheDir, `${key}.token.json`)
@@ -333,6 +333,7 @@ export class OAuthClient {
333333
redirectUri: string,
334334
server: http.Server
335335
): Promise<Token> {
336+
const DEFAULT_PKCE_TIMEOUT_MS = 20_000
336337
// a) generate PKCE params
337338
const verifier = this.b64url(crypto.randomBytes(32))
338339
const challenge = this.b64url(crypto.createHash('sha256').update(verifier).digest())
@@ -361,17 +362,26 @@ export class OAuthClient {
361362
void spawn(opener.cmd, opener.args, { detached: true, stdio: 'ignore' }).unref()
362363

363364
// c) wait for code on our loopback
364-
const { code, rxState, err } = await new Promise<{ code: string; rxState: string; err?: string }>(resolve => {
365+
const waitForFlow = new Promise<{ code: string; rxState: string; err?: string; errDesc?: string }>(resolve => {
365366
server.on('request', (req, res) => {
366367
const u = new URL(req.url || '/', redirectUri)
367368
const c = u.searchParams.get('code') || ''
368369
const s = u.searchParams.get('state') || ''
369370
const e = u.searchParams.get('error') || undefined
371+
const ed = u.searchParams.get('error_description') || undefined
370372
res.writeHead(200, { 'content-type': 'text/html' }).end('<h2>You may close this tab.</h2>')
371-
resolve({ code: c, rxState: s, err: e })
373+
resolve({ code: c, rxState: s, err: e, errDesc: ed })
372374
})
373375
})
374-
if (err) throw new Error(`Authorization error: ${err}`)
376+
const { code, rxState, err, errDesc } = await Promise.race([
377+
waitForFlow,
378+
new Promise<never>((_, reject) =>
379+
setTimeout(() => reject(new Error('authorization_timed_out')), DEFAULT_PKCE_TIMEOUT_MS)
380+
),
381+
])
382+
if (err) {
383+
throw new Error(`Authorization error: ${err}${errDesc ? ` - ${errDesc}` : ''}`)
384+
}
375385
if (!code || rxState !== state) throw new Error('Invalid authorization response (state mismatch)')
376386

377387
// d) exchange code for token

0 commit comments

Comments
 (0)