Skip to content

Commit 75ebc37

Browse files
committed
Add mcp configuration button
1 parent 0cb50b8 commit 75ebc37

File tree

8 files changed

+129
-18
lines changed

8 files changed

+129
-18
lines changed

packages/core/src/amazonq/webview/ui/main.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1011,6 +1011,11 @@ export const createMynahUI = (
10111011
feedbackOptions: feedbackOptions,
10121012
texts: uiComponentsTexts,
10131013
tabBarButtons: [
1014+
{
1015+
id: 'mcp_configuration',
1016+
icon: MynahIcons.MAGIC,
1017+
description: 'MCP configuration',
1018+
},
10141019
{
10151020
id: 'history_sheet',
10161021
icon: MynahIcons.HISTORY,

packages/core/src/codewhisperer/activation.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ import { SecurityIssueTreeViewProvider } from './service/securityIssueTreeViewPr
9090
import { setContext } from '../shared/vscode/setContext'
9191
import { syncSecurityIssueWebview } from './views/securityIssue/securityIssueWebview'
9292
import { detectCommentAboveLine } from '../shared/utilities/commentUtils'
93+
import { globalMcpConfigPath } from '../codewhispererChat/constants'
94+
import { McpManager } from '../codewhispererChat/tools/mcp/mcpManager'
95+
import globals from '../shared/extensionGlobals'
96+
import { ToolUtils } from '../codewhispererChat/tools/toolUtils'
9397

9498
let localize: nls.LocalizeFunc
9599

@@ -373,6 +377,8 @@ export async function activate(context: ExtContext): Promise<void> {
373377

374378
setSubscriptionsForCodeIssues()
375379

380+
setSubscriptionsForMcp()
381+
376382
function shouldRunAutoScan(editor: vscode.TextEditor | undefined, isScansEnabled?: boolean) {
377383
return (
378384
(isScansEnabled ?? CodeScansState.instance.isScansEnabled()) &&
@@ -499,6 +505,35 @@ export async function activate(context: ExtContext): Promise<void> {
499505
})
500506
)
501507
}
508+
509+
function setSubscriptionsForMcp() {
510+
let lastMcpContent: string | undefined
511+
const updateLastContent = (document: vscode.TextDocument) => {
512+
lastMcpContent = document.getText()
513+
}
514+
const mcpOpenListener = vscode.workspace.onDidOpenTextDocument((document: vscode.TextDocument) => {
515+
if (document.uri.fsPath === globalMcpConfigPath) {
516+
updateLastContent(document)
517+
}
518+
})
519+
context.extensionContext.subscriptions.push(mcpOpenListener)
520+
const mcpSaveListener = vscode.workspace.onDidSaveTextDocument(async (doc) => {
521+
// eslint-disable-next-line aws-toolkits/no-console-log
522+
console.log('1111')
523+
if (doc.uri.fsPath === globalMcpConfigPath) {
524+
const newContent = doc.getText()
525+
if (lastMcpContent === undefined || newContent !== lastMcpContent) {
526+
const manager = await McpManager.initMcpManager(globalMcpConfigPath)
527+
if (manager) {
528+
globals.mcpManager = manager
529+
ToolUtils.mcpManager = manager
530+
}
531+
lastMcpContent = newContent
532+
}
533+
}
534+
})
535+
context.extensionContext.subscriptions.push(mcpSaveListener)
536+
}
502537
}
503538

504539
export async function shutdown() {

packages/core/src/codewhispererChat/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,5 @@ export const ignoredDirectoriesAndFiles = [
6666
// OS specific files
6767
'.DS_Store',
6868
]
69+
70+
export const globalMcpConfigPath = path.join(process.env.HOME ?? '', '.aws', 'amazonq', 'mcp.json')

packages/core/src/codewhispererChat/controllers/chat/controller.ts

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ import {
9393
workspaceChunkMaxSize,
9494
defaultContextLengths,
9595
tools,
96+
globalMcpConfigPath,
9697
} from '../../constants'
9798
import { ChatSession } from '../../clients/chat/v0/chat'
9899
import { amazonQTabSuffix } from '../../../shared/constants'
@@ -180,7 +181,6 @@ export class ChatController {
180181
private userPromptsWatcher: vscode.FileSystemWatcher | undefined
181182
private chatHistoryDb = Database.getInstance()
182183
private cancelTokenSource: vscode.CancellationTokenSource = new vscode.CancellationTokenSource()
183-
private mcpManager: McpManager | undefined
184184

185185
public constructor(
186186
private readonly chatControllerMessageListeners: ChatControllerMessageListeners,
@@ -200,26 +200,16 @@ export class ChatController {
200200
this.userIntentRecognizer = new UserIntentRecognizer()
201201
this.tabBarController = new TabBarController(this.messenger)
202202

203-
// MCP intitialzation
204-
const mcpConfigPath = path.join(process.env.HOME ?? '', '.aws', 'amazonq', 'mcp.json')
205-
McpManager.create(mcpConfigPath)
203+
// todo: move to activate function?
204+
McpManager.initMcpManager(globalMcpConfigPath)
206205
.then((manager) => {
207-
this.mcpManager = manager
208-
ToolUtils.mcpManager = manager
209-
const discovered = manager.getAllMcpTools()
210-
for (const def of discovered) {
211-
tools.push({
212-
toolSpecification: {
213-
name: def.toolName,
214-
description: def.description,
215-
inputSchema: { json: def.inputSchema },
216-
},
217-
})
206+
if (manager) {
207+
globals.mcpManager = manager
208+
ToolUtils.mcpManager = manager
218209
}
219-
getLogger().info(`MCP: successfully discovered ${discovered.length} new tools.`)
220210
})
221211
.catch((err) => {
222-
getLogger().error(`Failed to init MCP manager: ${err}`)
212+
getLogger().error(`Failed to initialize MCP manager in controller: ${err}`)
223213
})
224214

225215
onDidChangeAmazonQVisibility((visible) => {

packages/core/src/codewhispererChat/controllers/chat/tabBarController.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import { Database } from '../../../shared/db/chatDb/chatDb'
1515
import { TabBarButtonClick, SaveChatMessage } from './model'
1616
import { Conversation, messageToChatItem, Tab } from '../../../shared/db/chatDb/util'
1717
import { DetailedListItemGroup, MynahIconsType } from '@aws/mynah-ui'
18+
import path from 'path'
19+
import { UserWrittenCodeTracker } from '../../../codewhisperer/tracker/userWrittenCodeTracker'
20+
import { globalMcpConfigPath } from '../../constants'
1821

1922
export class TabBarController {
2023
private readonly messenger: Messenger
@@ -144,11 +147,29 @@ export class TabBarController {
144147
case 'history_sheet':
145148
await this.historyButtonClicked(message)
146149
break
150+
case 'mcp_configuration':
151+
await this.mcpButtonClicked(message)
152+
break
147153
case 'export_chat':
148154
await this.exportChatButtonClicked(message)
149155
break
150156
}
151157
}
158+
private async mcpButtonClicked(message: TabBarButtonClick) {
159+
let fileExists = false
160+
try {
161+
await fs.stat(globalMcpConfigPath)
162+
fileExists = true
163+
} catch (error) {
164+
fileExists = false
165+
}
166+
if (!fileExists) {
167+
const defaultContent = JSON.stringify({ mcpServers: {} }, undefined, 2)
168+
await fs.writeFile(globalMcpConfigPath, defaultContent, { encoding: 'utf8' })
169+
}
170+
const document = await vscode.workspace.openTextDocument(globalMcpConfigPath)
171+
await vscode.window.showTextDocument(document, { preview: false })
172+
}
152173

153174
private async exportChatButtonClicked(message: TabBarButtonClick) {
154175
const defaultFileName = `q-dev-chat-${new Date().toISOString().split('T')[0]}.md`

packages/core/src/codewhispererChat/tools/mcp/mcpManager.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
88
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
99
import fs from '../../../shared/fs/fs'
1010
import { getLogger } from '../../../shared/logger/logger'
11+
import { tools } from '../../constants'
12+
import { ToolType } from '../toolUtils'
1113

1214
export interface McpToolDefinition {
1315
serverName: string
@@ -21,7 +23,7 @@ export class McpManager {
2123
private clients: Map<string, Client> = new Map() // key: serverName, val: MCP client
2224
private mcpTools: McpToolDefinition[] = []
2325

24-
private constructor(private configPath: string) {}
26+
private constructor(private readonly configPath: string) {}
2527

2628
public static async create(configPath: string): Promise<McpManager> {
2729
const instance = new McpManager(configPath)
@@ -79,6 +81,7 @@ export class McpManager {
7981
getLogger().info(`Found MCP tool [${toolDef.toolName}] from server [${serverName}]`)
8082
}
8183
} catch (err) {
84+
// Log the error for this server but allow the initialization of others to continue.
8285
getLogger().error(`Failed to init server [${serverName}]: ${(err as Error).message}`)
8386
}
8487
}
@@ -101,4 +104,53 @@ export class McpManager {
101104
public findTool(toolName: string): McpToolDefinition | undefined {
102105
return this.mcpTools.find((t) => t.toolName === toolName)
103106
}
107+
108+
public static async initMcpManager(configPath: string): Promise<McpManager | undefined> {
109+
try {
110+
const manager = await McpManager.create(configPath)
111+
const discovered = manager.getAllMcpTools()
112+
const builtInNames = new Set<string>(Object.values(ToolType))
113+
const discoveredNames = new Set(discovered.map((d) => d.toolName))
114+
115+
for (const def of discovered) {
116+
const spec = {
117+
toolSpecification: {
118+
name: def.toolName,
119+
description: def.description,
120+
inputSchema: { json: def.inputSchema },
121+
},
122+
}
123+
const idx = tools.findIndex((t) => t.toolSpecification!.name === def.toolName)
124+
if (idx >= 0) {
125+
// replace existing entry
126+
tools[idx] = spec
127+
} else {
128+
// append new entry
129+
tools.push(spec)
130+
}
131+
}
132+
133+
// Prune stale _dynamic_ tools (leave built‑ins intact)
134+
for (let i = tools.length - 1; i >= 0; --i) {
135+
const name = tools[i].toolSpecification!.name
136+
if (!name || builtInNames.has(name)) {
137+
continue
138+
}
139+
// if it wasn’t rediscovered in new MCP config, remove it
140+
if (!discoveredNames.has(name)) {
141+
tools.splice(i, 1)
142+
}
143+
}
144+
getLogger().info(`MCP: successfully discovered ${discovered.length} new tools.`)
145+
return manager
146+
} catch (err) {
147+
getLogger().error(`Failed to init MCP manager: ${(err as Error).message}`)
148+
return undefined
149+
}
150+
}
151+
152+
// public async dispose(): Promise<void> {
153+
// this.clients.clear()
154+
// this.mcpTools = []
155+
// }
104156
}

packages/core/src/dynamicResources/activation.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ import { resourceFileGlobPattern } from './awsResourceManager'
1818
import { Commands } from '../shared/vscode/commands2'
1919
import globals from '../shared/extensionGlobals'
2020
import { openUrl } from '../shared/utilities/vsCodeUtils'
21+
import { globalMcpConfigPath } from '../codewhispererChat/constants'
22+
import { McpManager } from '../codewhispererChat/tools/mcp/mcpManager'
23+
import { ToolUtils } from '../codewhispererChat/tools/toolUtils'
2124

2225
const localize = nls.loadMessageBundle()
2326

packages/core/src/shared/extensionGlobals.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { GlobalState } from './globalState'
2020
import { setContext } from './vscode/setContext'
2121
import { getLogger } from './logger/logger'
2222
import { AWSClientBuilderV3 } from './awsClientBuilderV3'
23+
import { McpManager } from '../codewhispererChat/tools/mcp/mcpManager'
2324

2425
type Clock = Pick<
2526
typeof globalThis,
@@ -154,6 +155,7 @@ export function initialize(context: ExtensionContext, isWeb: boolean = false): T
154155
globalState: new GlobalState(context.globalState),
155156
manifestPaths: {} as ToolkitGlobals['manifestPaths'],
156157
isWeb,
158+
mcpManager: undefined,
157159
})
158160
void setContext('aws.isWebExtHost', isWeb)
159161

@@ -227,4 +229,5 @@ export interface ToolkitGlobals {
227229
}
228230
/** If this extension is running in Web mode (the browser), compared to running on the desktop (node) */
229231
isWeb: boolean
232+
mcpManager?: McpManager
230233
}

0 commit comments

Comments
 (0)