Skip to content

Commit 5fd7d5b

Browse files
committed
Add mcp configuration button
1 parent 0cb50b8 commit 5fd7d5b

File tree

10 files changed

+140
-29
lines changed

10 files changed

+140
-29
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: 32 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,32 @@ 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+
if (doc.uri.fsPath === globalMcpConfigPath) {
522+
const newContent = doc.getText()
523+
if (lastMcpContent === undefined || newContent !== lastMcpContent) {
524+
const manager = await McpManager.initMcpManager(globalMcpConfigPath)
525+
if (manager) {
526+
globals.mcpManager = manager
527+
}
528+
lastMcpContent = newContent
529+
}
530+
}
531+
})
532+
context.extensionContext.subscriptions.push(mcpSaveListener)
533+
}
502534
}
503535

504536
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: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ import {
9292
additionalContentInnerContextLimit,
9393
workspaceChunkMaxSize,
9494
defaultContextLengths,
95-
tools,
95+
globalMcpConfigPath,
9696
} from '../../constants'
9797
import { ChatSession } from '../../clients/chat/v0/chat'
9898
import { amazonQTabSuffix } from '../../../shared/constants'
@@ -180,7 +180,6 @@ export class ChatController {
180180
private userPromptsWatcher: vscode.FileSystemWatcher | undefined
181181
private chatHistoryDb = Database.getInstance()
182182
private cancelTokenSource: vscode.CancellationTokenSource = new vscode.CancellationTokenSource()
183-
private mcpManager: McpManager | undefined
184183

185184
public constructor(
186185
private readonly chatControllerMessageListeners: ChatControllerMessageListeners,
@@ -200,27 +199,16 @@ export class ChatController {
200199
this.userIntentRecognizer = new UserIntentRecognizer()
201200
this.tabBarController = new TabBarController(this.messenger)
202201

203-
// MCP intitialzation
204-
const mcpConfigPath = path.join(process.env.HOME ?? '', '.aws', 'amazonq', 'mcp.json')
205-
McpManager.create(mcpConfigPath)
206-
.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-
})
218-
}
219-
getLogger().info(`MCP: successfully discovered ${discovered.length} new tools.`)
220-
})
221-
.catch((err) => {
222-
getLogger().error(`Failed to init MCP manager: ${err}`)
223-
})
202+
// todo: move to activate function?
203+
// McpManager.initMcpManager(globalMcpConfigPath)
204+
// .then((manager) => {
205+
// if (manager) {
206+
// globals.mcpManager = manager
207+
// }
208+
// })
209+
// .catch((err) => {
210+
// getLogger().error(`Failed to initialize MCP manager in controller: ${err}`)
211+
// })
224212

225213
onDidChangeAmazonQVisibility((visible) => {
226214
if (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/codewhispererChat/tools/mcp/mcpTool.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Writable } from 'stream'
66
import { getLogger } from '../../../shared/logger/logger'
77
import { ToolUtils } from '../toolUtils'
88
import { CommandValidation, InvokeOutput, OutputKind } from '../toolShared'
9+
import globals from '../../../shared/extensionGlobals'
910

1011
export interface McpToolParams {
1112
serverName: string
@@ -38,7 +39,8 @@ export class McpTool {
3839

3940
public async invoke(updates?: Writable): Promise<InvokeOutput> {
4041
try {
41-
const result = await ToolUtils.mcpManager!.callTool(this.serverName, this.toolName, this.input)
42+
const mcpManager = globals.mcpManager
43+
const result = await mcpManager?.callTool(this.serverName, this.toolName, this.input)
4244
const content = typeof result === 'object' ? JSON.stringify(result) : String(result)
4345

4446
return {

packages/core/src/codewhispererChat/tools/toolUtils.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
} from './toolShared'
1717
import { ListDirectory, ListDirectoryParams } from './listDirectory'
1818
import { McpTool } from './mcp/mcpTool'
19-
import { McpManager } from './mcp/mcpManager'
19+
import globals from '../../shared/extensionGlobals'
2020

2121
export enum ToolType {
2222
FsRead = 'fsRead',
@@ -34,8 +34,6 @@ export type Tool =
3434
| { type: ToolType.Mcp; tool: McpTool }
3535

3636
export class ToolUtils {
37-
static mcpManager?: McpManager
38-
3937
static displayName(tool: Tool): string {
4038
switch (tool.type) {
4139
case ToolType.FsRead:
@@ -172,7 +170,8 @@ export class ToolUtils {
172170
tool: new ListDirectory(value.input as unknown as ListDirectoryParams),
173171
}
174172
default: {
175-
const mcpToolDef = ToolUtils.mcpManager?.findTool(value.name as string)
173+
const mcpMgr = globals.mcpManager
174+
const mcpToolDef = mcpMgr?.findTool(value.name as string)
176175
if (mcpToolDef) {
177176
return {
178177
type: ToolType.Mcp,

packages/core/src/extension.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ import { setupUninstallHandler } from './shared/handleUninstall'
5555
import { maybeShowMinVscodeWarning } from './shared/extensionStartup'
5656
import { getLogger } from './shared/logger/logger'
5757
import { setContext } from './shared/vscode/setContext'
58+
import { McpManager } from './codewhispererChat/tools/mcp/mcpManager'
59+
import { globalMcpConfigPath } from './codewhispererChat/constants'
5860

5961
disableAwsSdkWarning()
6062

@@ -116,6 +118,12 @@ export async function activateCommon(
116118
)
117119
globals.regionProvider = RegionProvider.fromEndpointsProvider(makeEndpointsProvider())
118120

121+
/**
122+
* MCP client initialization
123+
*/
124+
const mgr = await McpManager.initMcpManager(globalMcpConfigPath)
125+
globals.mcpManager = mgr
126+
119127
// telemetry
120128
await activateTelemetry(context, globals.awsContext, Settings.instance, 'AWS Toolkit For VS Code')
121129

packages/core/src/shared/extensionGlobals.ts

Lines changed: 2 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,
@@ -227,4 +228,5 @@ export interface ToolkitGlobals {
227228
}
228229
/** If this extension is running in Web mode (the browser), compared to running on the desktop (node) */
229230
isWeb: boolean
231+
mcpManager?: McpManager
230232
}

0 commit comments

Comments
 (0)