From c38ad98147017e80ab0b37a6b8d9b9b2613790c8 Mon Sep 17 00:00:00 2001 From: Dung Dong Date: Wed, 16 Sep 2026 23:23:20 +0000 Subject: [PATCH] feat: add MCP configuration review action --- .../agenticChat/tools/mcp/mcpManager.test.ts | 20 ++++- .../agenticChat/tools/mcp/mcpManager.ts | 88 +++++++++++-------- 2 files changed, 72 insertions(+), 36 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.test.ts index 0667d149cc..8e643efaa9 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.test.ts @@ -1943,6 +1943,7 @@ describe('consent gate for workspace-scoped MCP servers (P417451767)', () => { const workspaceMcp = '/tmp/ws-a/.amazonq/mcp.json' let showMessageStub: sinon.SinonStub + let showDocumentStub: sinon.SinonStub let hasApprovalStub: sinon.SinonStub let recordApprovalStub: sinon.SinonStub let setStateSpy: sinon.SinonSpy @@ -1953,13 +1954,14 @@ describe('consent gate for workspace-scoped MCP servers (P417451767)', () => { recordApprovalStub = sinon.stub(consentStore, 'recordApproval').resolves() showMessageStub = sinon.stub() + showDocumentStub = sinon.stub().resolves({ success: true }) const featuresWithPrompt = { ...features, workspace: { ...fakeWorkspace, fs: { ...fakeWorkspace.fs, getUserHomeDir: () => fakeHome }, }, - lsp: { window: { showMessageRequest: showMessageStub } }, + lsp: { window: { showMessageRequest: showMessageStub, showDocument: showDocumentStub } }, } sinon.stub(mcpUtils, 'loadAgentConfig').resolves({ servers: new Map(), @@ -2028,6 +2030,22 @@ describe('consent gate for workspace-scoped MCP servers (P417451767)', () => { expect(showMessageStub.calledOnce).to.be.true }) + it('opens the workspace configuration and re-prompts before allowing', async () => { + const mgr = await buildMgr() + showMessageStub.onFirstCall().resolves({ title: 'View full configuration' }) + showMessageStub.onSecondCall().resolves({ title: 'Deny' }) + const cfg: MCPServerConfig = { command: 'sh', args: ['-c', 'x'], __configPath__: workspaceMcp } + + try { + await (mgr as any).initOneServerInternal('svc', cfg) + } catch {} + + expect(showMessageStub.calledTwice).to.be.true + expect(showDocumentStub.calledOnce).to.be.true + expect(showDocumentStub.firstCall.args[0].uri).to.equal('file:///tmp/ws-a/.amazonq/mcp.json') + expect(showDocumentStub.firstCall.args[0].takeFocus).to.be.true + expect(recordApprovalStub.called).to.be.false + }) it('denial sets DISABLED state and caches the decision', async () => { const mgr = await buildMgr() showMessageStub.resolves({ title: 'Deny' }) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts index bde82719d7..27f66728e1 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts @@ -460,42 +460,60 @@ export class McpManager { const headerLine = headerNames.length > 0 ? `Headers: ${headerNames.join(', ').slice(0, 200)}\n` : '' const allowBtn = { title: 'Allow for this server' } const denyBtn = { title: 'Deny' } - let choice: { title: string } | null | undefined - try { - choice = await this.features.lsp.window.showMessageRequest({ - type: MessageType.Warning, - message: - `Amazon Q — Untrusted MCP Server\n\n` + - `A workspace configuration file wants to start an MCP server.\n` + - `Server: ${serverName}\n` + - `Command: ${cmdLine}\n` + - envLine + - headerLine + - `Source: ${configPath}\n\n` + - `Running this server executes the above command on your machine, ` + - `with the environment variables listed above. ` + - `Review them in the configuration file if you are unsure — variables such as ` + - `NODE_OPTIONS can cause additional code to run. ` + - `Only allow if you trust the authors of this workspace.\n\n` + - `Your choice will be remembered for this workspace. ` + - `If you allow, you won't be asked again unless the server configuration changes.`, - actions: [allowBtn, denyBtn], - }) - } catch (e: any) { - this.features.logging.warn(`MCP: consent prompt failed for '${serverName}': ${e?.message}`) - this.setState(serverName, McpServerStatus.FAILED, 0, 'consent prompt failed') - return - } - if (choice?.title !== allowBtn.title) { - this.features.logging.info( - `MCP: user declined consent for workspace-scoped server '${serverName}' (response: ${choice?.title ?? 'dismissed'})` - ) - this.sessionDeniedConsent.add(denyKey) - this.setState(serverName, McpServerStatus.DISABLED, 0, 'consent not granted') - return + const reviewBtn = { title: 'View full configuration' } + + while (true) { + let choice: { title: string } | null | undefined + try { + choice = await this.features.lsp.window.showMessageRequest({ + type: MessageType.Warning, + message: + `Amazon Q — Untrusted MCP Server\n\n` + + `A workspace configuration file wants to start an MCP server.\n` + + `Server: ${serverName}\n` + + `Command: ${cmdLine}\n` + + envLine + + headerLine + + `Source: ${configPath}\n\n` + + `Running this server executes the above command on your machine, ` + + `with the environment variables listed above. ` + + `Use View full configuration to inspect the source before allowing. ` + + `Variables such as NODE_OPTIONS can cause additional code to run. ` + + `Only allow if you trust the authors of this workspace.\n\n` + + `Your choice will be remembered for this workspace. ` + + `If you allow, you won't be asked again unless the server configuration changes.`, + actions: [reviewBtn, allowBtn, denyBtn], + }) + } catch (e: any) { + this.features.logging.warn(`MCP: consent prompt failed for '${serverName}': ${e?.message}`) + this.setState(serverName, McpServerStatus.FAILED, 0, 'consent prompt failed') + return + } + if (choice?.title === reviewBtn.title) { + try { + await this.features.lsp.window.showDocument({ + uri: URI.file(configPath).toString(), + takeFocus: true, + }) + } catch (e: any) { + this.features.logging.warn( + `MCP: failed to open configuration for '${serverName}': ${e?.message}` + ) + } + continue + } + if (choice?.title !== allowBtn.title) { + this.features.logging.info( + `MCP: user declined consent for workspace-scoped server '${serverName}' (response: ${choice?.title ?? 'dismissed'})` + ) + this.sessionDeniedConsent.add(denyKey) + this.setState(serverName, McpServerStatus.DISABLED, 0, 'consent not granted') + return + } + await recordApproval(this.features.workspace, this.features.logging, serverName, cfg, configPath) + this.features.logging.info(`MCP: recorded consent for workspace-scoped server '${serverName}'`) + break } - await recordApproval(this.features.workspace, this.features.logging, serverName, cfg, configPath) - this.features.logging.info(`MCP: recorded consent for workspace-scoped server '${serverName}'`) } }