Skip to content

Commit bd590f0

Browse files
committed
feat(amazonq): adding feature flag and API for mcp admin configuration
1 parent 69516f4 commit bd590f0

File tree

5 files changed

+172
-1
lines changed

5 files changed

+172
-1
lines changed

packages/amazonq/src/extension.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { AuthUtils, CredentialsStore, LoginManager, initializeAuth } from 'aws-c
77
import { activate as activateCodeWhisperer, shutdown as shutdownCodeWhisperer } from 'aws-core-vscode/codewhisperer'
88
import { makeEndpointsProvider, registerGenericCommands } from 'aws-core-vscode'
99
import { CommonAuthWebview } from 'aws-core-vscode/login'
10+
import { checkMcpConfiguration } from 'aws-core-vscode/codewhisperer'
1011
import {
1112
amazonQDiffScheme,
1213
DefaultAWSClientBuilder,
@@ -48,6 +49,16 @@ import { hasGlibcPatch } from './lsp/client'
4849

4950
export const amazonQContextPrefix = 'amazonq'
5051

52+
// Global variable to store mcpAdmin feature flag
53+
export let mcpAdmin = true
54+
55+
/**
56+
* Safely checks MCP configuration from user profile and sets mcpAdmin feature flag
57+
*/
58+
async function setMcpAdminFlag(): Promise<void> {
59+
mcpAdmin = await checkMcpConfiguration()
60+
}
61+
5162
/**
5263
* Activation code for Amazon Q that will we want in all environments (eg Node.js, web mode)
5364
*/
@@ -126,6 +137,9 @@ export async function activateAmazonQCommon(context: vscode.ExtensionContext, is
126137
// This contains every lsp agnostic things (auth, security scan, code scan)
127138
await activateCodeWhisperer(extContext as ExtContext)
128139

140+
// Check MCP configuration from profile before activation
141+
await setMcpAdminFlag()
142+
129143
if (!isAmazonLinux2() || hasGlibcPatch()) {
130144
// Activate Amazon Q LSP for everyone unless they're using AL2 without the glibc patch
131145
await activateAmazonqLsp(context)

packages/amazonq/src/lsp/client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import { SessionManager } from '../app/inline/sessionManager'
5050
import { LineTracker } from '../app/inline/stateTracker/lineTracker'
5151
import { InlineTutorialAnnotation } from '../app/inline/tutorials/inlineTutorialAnnotation'
5252
import { InlineChatTutorialAnnotation } from '../app/inline/tutorials/inlineChatTutorialAnnotation'
53+
import { mcpAdmin } from '../extension'
5354

5455
const localize = nls.loadMessageBundle()
5556
const logger = getLogger('amazonqLsp.lspClient')
@@ -165,6 +166,7 @@ export async function startLanguageServer(
165166
pinnedContextEnabled: true,
166167
imageContextEnabled: true,
167168
mcp: true,
169+
mcpAdmin: mcpAdmin,
168170
shortcut: true,
169171
reroute: true,
170172
modelSelection: true,

packages/core/src/codewhisperer/client/user-service-2.json

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,37 @@
202202
{ "shape": "AccessDeniedException" }
203203
]
204204
},
205+
"GetProfile": {
206+
"name": "GetProfile",
207+
"http": {
208+
"method": "POST",
209+
"requestUri": "/"
210+
},
211+
"input": {
212+
"shape": "GetProfileRequest"
213+
},
214+
"output": {
215+
"shape": "GetProfileResponse"
216+
},
217+
"errors": [
218+
{
219+
"shape": "ThrottlingException"
220+
},
221+
{
222+
"shape": "ResourceNotFoundException"
223+
},
224+
{
225+
"shape": "InternalServerException"
226+
},
227+
{
228+
"shape": "ValidationException"
229+
},
230+
{
231+
"shape": "AccessDeniedException"
232+
}
233+
],
234+
"documentation": "<p>Get the requested CodeWhisperer profile.</p>"
235+
},
205236
"GetTaskAssistCodeGeneration": {
206237
"name": "GetTaskAssistCodeGeneration",
207238
"http": {
@@ -1997,6 +2028,24 @@
19972028
"suggestedFix": { "shape": "SuggestedFix" }
19982029
}
19992030
},
2031+
"GetProfileRequest": {
2032+
"type": "structure",
2033+
"required": ["profileArn"],
2034+
"members": {
2035+
"profileArn": {
2036+
"shape": "ProfileArn"
2037+
}
2038+
}
2039+
},
2040+
"GetProfileResponse": {
2041+
"type": "structure",
2042+
"required": ["profile"],
2043+
"members": {
2044+
"profile": {
2045+
"shape": "ProfileInfo"
2046+
}
2047+
}
2048+
},
20002049
"GetTaskAssistCodeGenerationRequest": {
20012050
"type": "structure",
20022051
"required": ["conversationId", "codeGenerationId"],
@@ -2426,6 +2475,15 @@
24262475
"type": "long",
24272476
"box": true
24282477
},
2478+
"MCPConfiguration": {
2479+
"type": "structure",
2480+
"required": ["toggle"],
2481+
"members": {
2482+
"toggle": {
2483+
"shape": "OptInFeatureToggle"
2484+
}
2485+
}
2486+
},
24292487
"MemoryEntry": {
24302488
"type": "structure",
24312489
"required": ["id", "memoryEntryString", "metadata"],
@@ -2532,7 +2590,8 @@
25322590
"byUserAnalytics": { "shape": "ByUserAnalytics" },
25332591
"dashboardAnalytics": { "shape": "DashboardAnalytics" },
25342592
"notifications": { "shape": "Notifications" },
2535-
"workspaceContext": { "shape": "WorkspaceContext" }
2593+
"workspaceContext": { "shape": "WorkspaceContext" },
2594+
"mcpConfiguration": { "shape": "MCPConfiguration" }
25362595
}
25372596
},
25382597
"OptOutPreference": {
@@ -2682,6 +2741,30 @@
26822741
"min": 1,
26832742
"pattern": "[\\sa-zA-Z0-9_-]*"
26842743
},
2744+
"ProfileInfo": {
2745+
"type": "structure",
2746+
"required": ["arn"],
2747+
"members": {
2748+
"arn": {
2749+
"shape": "ProfileArn"
2750+
},
2751+
"profileName": {
2752+
"shape": "ProfileName"
2753+
},
2754+
"description": {
2755+
"shape": "ProfileDescription"
2756+
},
2757+
"status": {
2758+
"shape": "ProfileStatus"
2759+
},
2760+
"profileType": {
2761+
"shape": "ProfileType"
2762+
},
2763+
"optInFeatures": {
2764+
"shape": "OptInFeatures"
2765+
}
2766+
}
2767+
},
26852768
"ProfileList": {
26862769
"type": "list",
26872770
"member": { "shape": "Profile" }

packages/core/src/codewhisperer/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,4 @@ export * from './util/gitUtil'
8686
export * from './ui/prompters'
8787
export { UserWrittenCodeTracker } from './tracker/userWrittenCodeTracker'
8888
export { RegionProfileManager, defaultServiceConfig } from './region/regionProfileManager'
89+
export { checkMcpConfiguration } from './util/profileUtils'
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { getLogger } from '../../shared/logger/logger'
7+
import { codeWhispererClient } from '../client/codewhisperer'
8+
import { AuthUtil } from './authUtil'
9+
10+
export async function checkMcpConfiguration(): Promise<boolean> {
11+
try {
12+
if (!AuthUtil.instance.isConnected()) {
13+
return true
14+
}
15+
16+
const userClient = await codeWhispererClient.createUserSdkClient()
17+
const profileArn = AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn
18+
if (!profileArn) {
19+
return true
20+
}
21+
22+
const response = await retryWithBackoff(() => userClient.getProfile({ profileArn }).promise())
23+
const mcpConfig = response.profile?.optInFeatures?.mcpConfiguration?.toggle
24+
const isMcpEnabled = mcpConfig === 'ON'
25+
26+
getLogger().debug(`MCP configuration toggle: ${mcpConfig}, mcpAdmin flag set to: ${isMcpEnabled}`)
27+
return isMcpEnabled
28+
} catch (error) {
29+
getLogger().debug(`Failed to check MCP configuration from profile: ${error}. Setting mcpAdmin to false.`)
30+
return true
31+
}
32+
}
33+
34+
async function retryWithBackoff<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {
35+
let lastError: any
36+
37+
for (let attempt = 0; attempt < maxRetries; attempt++) {
38+
try {
39+
return await fn()
40+
} catch (error) {
41+
lastError = error
42+
43+
// Only retry on specific retryable exceptions
44+
const errorCode = (error as any).code || (error as any).name
45+
const statusCode = (error as any).statusCode
46+
47+
// Don't retry on client errors (4xx) except ThrottlingException
48+
if (statusCode >= 400 && statusCode < 500 && errorCode !== 'ThrottlingException') {
49+
throw error
50+
}
51+
52+
// Only retry on retryable exceptions
53+
const retryableExceptions = [
54+
'ThrottlingException',
55+
'InternalServerException',
56+
'ServiceUnavailableException',
57+
]
58+
if (!retryableExceptions.includes(errorCode) && statusCode !== 500 && statusCode !== 503) {
59+
throw error
60+
}
61+
62+
if (attempt < maxRetries - 1) {
63+
const delay = Math.min(1000 * Math.pow(2, attempt), 3000) // Cap at 3s
64+
getLogger().debug(`GetProfile attempt ${attempt + 1} failed, retrying in ${delay}ms: ${error}`)
65+
await new Promise((resolve) => setTimeout(resolve, delay))
66+
}
67+
}
68+
}
69+
70+
throw lastError
71+
}

0 commit comments

Comments
 (0)