Skip to content

Commit 071a34e

Browse files
committed
fix: relax condition for triggering oauth flow; add resilience checks for as availability
1 parent e84e808 commit 071a34e

File tree

2 files changed

+54
-22
lines changed

2 files changed

+54
-22
lines changed

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

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ export class McpManager {
281281
const isStdio = !!cfg.command
282282
const doConnect = async () => {
283283
if (isStdio) {
284+
// stdio transport
284285
const mergedEnv = {
285286
...(process.env as Record<string, string>),
286287
// Make sure we do not have empty key and value in mergedEnv, or adding server through UI will fail on Windows
@@ -321,22 +322,26 @@ export class McpManager {
321322
)
322323
}
323324
} else {
325+
// streamable http/SSE transport
324326
const base = new URL(cfg.url!)
325327
try {
326-
// first determine if authroization is needed
328+
// Use HEAD to check if it nees OAuth
327329
let headers: Record<string, string> = { ...(cfg.headers ?? {}) }
328-
const headStatus = await fetch(base, { method: 'HEAD', headers }).then(r => r.status)
329-
const needsOAuth = headStatus === 401
330+
let needsOAuth = false
331+
try {
332+
const headResp = await fetch(base, { method: 'HEAD', headers })
333+
const www = headResp.headers.get('www-authenticate') || ''
334+
needsOAuth = headResp.status === 401 || headResp.status === 403 || /bearer/i.test(www)
335+
} catch {
336+
this.features.logging.info(`MCP: HEAD not available`)
337+
}
330338

331339
if (needsOAuth) {
332340
OAuthClient.initialize(this.features.workspace, this.features.logging)
333-
const bearer = needsOAuth ? await OAuthClient.getValidAccessToken(base) : ''
341+
const bearer = await OAuthClient.getValidAccessToken(base)
334342
// add authorization header if we are able to obtain a bearer token
335343
if (bearer) {
336-
headers = {
337-
...headers,
338-
Authorization: `Bearer ${bearer}`,
339-
}
344+
headers = { ...headers, Authorization: `Bearer ${bearer}` }
340345
}
341346
}
342347

@@ -356,8 +361,9 @@ export class McpManager {
356361
}
357362
} catch (err: any) {
358363
let errorMessage = err?.message ?? String(err)
364+
const oauthHint = /oauth/i.test(errorMessage) ? ' (OAuth)' : ''
359365
throw new AgenticChatError(
360-
`MCP: server '${serverName}' failed to connect: ${errorMessage}`,
366+
`MCP: server '${serverName}' failed to connect${oauthHint}: ${errorMessage}`,
361367
'MCPServerConnectionFailed'
362368
)
363369
}

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

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,23 @@ export class OAuthClient {
8787
}
8888

8989
// 3) Discover AS metadata
90-
const meta = await this.discoverAS(mcpBase)
91-
const scopes = ['openid']
92-
90+
let meta: Meta
91+
try {
92+
meta = await this.discoverAS(mcpBase)
93+
} catch (e: any) {
94+
throw new Error(`OAuth discovery failed: ${e?.message ?? String(e)}`)
95+
}
9396
// 4) Register (or reuse) a dynamic client
94-
const reg = await this.obtainClient(meta, regPath, scopes, redirectUri)
97+
const scopes = ['openid', 'offline_access']
98+
let reg: Registration
99+
try {
100+
reg = await this.obtainClient(meta, regPath, scopes, redirectUri)
101+
} catch (e: any) {
102+
throw new Error(`OAuth client registration failed: ${e?.message ?? String(e)}`)
103+
}
95104

96105
// 5) Refresh‑token grant (one shot)
106+
const attemptedRefresh = !!cached?.refresh_token
97107
if (cached?.refresh_token) {
98108
const refreshed = await this.refreshGrant(meta, reg, mcpBase, cached.refresh_token)
99109
if (refreshed) {
@@ -105,9 +115,14 @@ export class OAuthClient {
105115
}
106116

107117
// 6) PKCE interactive flow
108-
const fresh = await this.pkceGrant(meta, reg, mcpBase, scopes, redirectUri, server)
109-
await this.write(tokPath, fresh)
110-
return fresh.access_token
118+
try {
119+
const fresh = await this.pkceGrant(meta, reg, mcpBase, scopes, redirectUri, server)
120+
await this.write(tokPath, fresh)
121+
return fresh.access_token
122+
} catch (e: any) {
123+
const suffix = attemptedRefresh ? ' after refresh attempt' : ''
124+
throw new Error(`OAuth authorization (PKCE) failed${suffix}: ${e?.message ?? String(e)}`)
125+
}
111126
} finally {
112127
await new Promise<void>(res => server.close(() => res()))
113128
}
@@ -133,7 +148,7 @@ export class OAuthClient {
133148
const metaUrl = new URL(m[1] || m[2], rs).toString()
134149
this.logger.info(`OAuth: resource_metadata → ${metaUrl}`)
135150
const raw = await this.json<any>(metaUrl)
136-
return this.fetchASFromResourceMeta(raw, metaUrl)
151+
return await this.fetchASFromResourceMeta(raw, metaUrl)
137152
}
138153
} catch {
139154
// ignore and fallback
@@ -155,7 +170,7 @@ export class OAuthClient {
155170
}
156171
}
157172

158-
// c) fallback to GitHub‑style
173+
// c) fallback to static OAuth2 endpoints
159174
const base = (rs.origin + rs.pathname).replace(/\/+$/, '')
160175
this.logger.warn(`OAuth: synthesising endpoints from ${base}`)
161176
return {
@@ -182,7 +197,12 @@ export class OAuthClient {
182197
// next
183198
}
184199
}
185-
throw new Error(`Failed well‑known fetch from ${asBase}, the server might not support OAuth authroization`)
200+
// fallback to static OAuth2 endpoints
201+
this.logger.warn(`OAuth: no well-known on ${asBase}, falling back to static endpoints`)
202+
return {
203+
authorization_endpoint: `${asBase}/authorize`,
204+
token_endpoint: `${asBase}/access_token`,
205+
}
186206
}
187207

188208
/** DCR: POST client metadata → client_id; cache to disk */
@@ -244,7 +264,11 @@ export class OAuthClient {
244264
headers: { 'content-type': 'application/x-www-form-urlencoded' },
245265
body: form,
246266
})
247-
if (!res.ok) return undefined
267+
if (!res.ok) {
268+
const msg = await res.text().catch(() => '')
269+
this.logger.warn(`OAuth: refresh grant HTTP ${res.status}${msg?.slice(0, 300)}`)
270+
return undefined
271+
}
248272
const j = (await res.json()) as Record<string, unknown>
249273
return { ...(j as object), obtained_at: Date.now() } as Token
250274
}
@@ -313,8 +337,10 @@ export class OAuthClient {
313337
headers: { 'content-type': 'application/x-www-form-urlencoded' },
314338
body: form2,
315339
})
316-
if (!res2.ok) throw new Error(`Token exchange failed: ${await res2.text()}`)
317-
340+
if (!res2.ok) {
341+
const txt = await res2.text().catch(() => '')
342+
throw new Error(`Token exchange failed (HTTP ${res2.status}): ${txt?.slice(0, 300)}`)
343+
}
318344
const tk = (await res2.json()) as Record<string, unknown>
319345
return { ...(tk as object), obtained_at: Date.now() } as Token
320346
}

0 commit comments

Comments
 (0)