Skip to content

Commit a808ee8

Browse files
committed
MCP client POC
1 parent 5a0404e commit a808ee8

File tree

9 files changed

+869
-89
lines changed

9 files changed

+869
-89
lines changed

package-lock.json

Lines changed: 630 additions & 86 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/amazonq/.vscode/launch.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
1414
"env": {
1515
"SSMDOCUMENT_LANGUAGESERVER_PORT": "6010",
16-
"WEBPACK_DEVELOPER_SERVER": "http://localhost:8080"
16+
"WEBPACK_DEVELOPER_SERVER": "http://localhost:8080",
17+
"PATH": "/Users/bywang/.toolbox/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
1718
// "__AMAZONQLSP_PATH": "${workspaceFolder}/../../../language-servers/app/aws-lsp-codewhisperer-runtimes/out/token-standalone.js",
1819
// "__AMAZONQLSP_UI": "${workspaceFolder}/../../../language-servers/chat-client/build/amazonq-ui.js"
1920
},

packages/core/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -576,7 +576,8 @@
576576
"winston": "^3.11.0",
577577
"winston-transport": "^4.6.0",
578578
"xml2js": "^0.6.1",
579-
"yaml-cfn": "^0.3.2"
579+
"yaml-cfn": "^0.3.2",
580+
"@modelcontextprotocol/sdk": "^1.9.0"
580581
},
581582
"overrides": {
582583
"webfont": {

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ import {
9292
additionalContentInnerContextLimit,
9393
workspaceChunkMaxSize,
9494
defaultContextLengths,
95+
tools,
9596
} from '../../constants'
9697
import { ChatSession } from '../../clients/chat/v0/chat'
9798
import { amazonQTabSuffix } from '../../../shared/constants'
@@ -102,6 +103,7 @@ import { tempDirPath } from '../../../shared/filesystemUtilities'
102103
import { Database } from '../../../shared/db/chatDb/chatDb'
103104
import { TabBarController } from './tabBarController'
104105
import { messageToChatMessage } from '../../../shared/db/chatDb/util'
106+
import { McpManager } from '../../tools/mcp/mcpManager'
105107

106108
export interface ChatControllerMessagePublishers {
107109
readonly processPromptChatMessage: MessagePublisher<PromptMessage>
@@ -178,6 +180,7 @@ export class ChatController {
178180
private userPromptsWatcher: vscode.FileSystemWatcher | undefined
179181
private chatHistoryDb = Database.getInstance()
180182
private cancelTokenSource: vscode.CancellationTokenSource = new vscode.CancellationTokenSource()
183+
private mcpManager: McpManager | undefined
181184

182185
public constructor(
183186
private readonly chatControllerMessageListeners: ChatControllerMessageListeners,
@@ -197,6 +200,28 @@ export class ChatController {
197200
this.userIntentRecognizer = new UserIntentRecognizer()
198201
this.tabBarController = new TabBarController(this.messenger)
199202

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+
})
224+
200225
onDidChangeAmazonQVisibility((visible) => {
201226
if (visible) {
202227
this.telemetryHelper.recordOpenChat()
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { ListToolsResponse, MCPConfig, MCPServerConfig } from './mcpTypes'
7+
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
8+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
9+
import fs from '../../../shared/fs/fs'
10+
import { getLogger } from '../../../shared/logger/logger'
11+
12+
export interface McpToolDefinition {
13+
serverName: string
14+
toolName: string
15+
description: string
16+
inputSchema: any // schema from the server
17+
}
18+
19+
export class McpManager {
20+
private mcpServers: Record<string, MCPServerConfig> = {}
21+
private clients: Map<string, Client> = new Map() // key: serverName, val: MCP client
22+
private mcpTools: McpToolDefinition[] = []
23+
24+
private constructor(private configPath: string) {}
25+
26+
public static async create(configPath: string): Promise<McpManager> {
27+
const instance = new McpManager(configPath)
28+
await instance.loadConfig()
29+
await instance.initAllServers()
30+
return instance
31+
}
32+
33+
public async loadConfig(): Promise<void> {
34+
if (!(await fs.exists(this.configPath))) {
35+
throw new Error(`Could not load the MCP config at ${this.configPath}`)
36+
}
37+
const raw = await fs.readFileText(this.configPath)
38+
const json = JSON.parse(raw) as MCPConfig
39+
if (!json.mcpServers) {
40+
throw new Error(`No "mcpServers" field found in config: ${this.configPath}`)
41+
}
42+
this.mcpServers = json.mcpServers
43+
}
44+
45+
public async initAllServers(): Promise<void> {
46+
for (const [serverName, serverConfig] of Object.entries(this.mcpServers)) {
47+
if (serverConfig.disabled) {
48+
getLogger().info(`MCP server [${serverName}] is disabled, skipping.`)
49+
continue
50+
}
51+
await this.initOneServer(serverName, serverConfig)
52+
}
53+
}
54+
55+
private async initOneServer(serverName: string, serverConfig: MCPServerConfig): Promise<void> {
56+
try {
57+
getLogger().debug(`Initializing MCP server [${serverName}] with command: ${serverConfig.command}`)
58+
const transport = new StdioClientTransport({
59+
command: serverConfig.command,
60+
args: serverConfig.args ?? [],
61+
env: process.env as Record<string, string>,
62+
})
63+
const client = new Client({
64+
name: `q-agentic-chat-mcp-client-${serverName}`,
65+
version: '1.0.0',
66+
})
67+
await client.connect(transport)
68+
this.clients.set(serverName, client)
69+
70+
const toolsResult = (await client.listTools()) as ListToolsResponse
71+
for (const toolInfo of toolsResult.tools) {
72+
const toolDef: McpToolDefinition = {
73+
serverName,
74+
toolName: toolInfo.name ?? 'unknown',
75+
description: toolInfo.description ?? '',
76+
inputSchema: toolInfo.inputSchema ?? {},
77+
}
78+
this.mcpTools.push(toolDef)
79+
getLogger().info(`Found MCP tool [${toolDef.toolName}] from server [${serverName}]`)
80+
}
81+
} catch (err) {
82+
getLogger().error(`Failed to init server [${serverName}]: ${(err as Error).message}`)
83+
}
84+
}
85+
86+
public getAllMcpTools(): McpToolDefinition[] {
87+
return [...this.mcpTools]
88+
}
89+
90+
public async callTool(serverName: string, toolName: string, args: any): Promise<any> {
91+
const client = this.clients.get(serverName)
92+
if (!client) {
93+
throw new Error(`MCP server [${serverName}] not connected or not found in clients.`)
94+
}
95+
return await client.callTool({
96+
name: toolName,
97+
arguments: args,
98+
})
99+
}
100+
101+
public findTool(toolName: string): McpToolDefinition | undefined {
102+
return this.mcpTools.find((t) => t.toolName === toolName)
103+
}
104+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
import { Writable } from 'stream'
6+
import { getLogger } from '../../../shared/logger/logger'
7+
import { ToolUtils } from '../toolUtils'
8+
import { CommandValidation, InvokeOutput, OutputKind } from '../toolShared'
9+
10+
export interface McpToolParams {
11+
serverName: string
12+
toolName: string
13+
input?: any
14+
}
15+
16+
export class McpTool {
17+
private readonly logger = getLogger('mcp')
18+
private serverName: string
19+
private toolName: string
20+
private input: any
21+
22+
public constructor(params: McpToolParams) {
23+
this.serverName = params.serverName
24+
this.toolName = params.toolName
25+
this.input = params.input
26+
}
27+
28+
public async validate(): Promise<void> {}
29+
30+
public queueDescription(updates: Writable): void {
31+
updates.write(`Invoking remote MCP tool: ${this.toolName} on server ${this.serverName}`)
32+
updates.end()
33+
}
34+
35+
public requiresAcceptance(): CommandValidation {
36+
return { requiresAcceptance: true }
37+
}
38+
39+
public async invoke(updates?: Writable): Promise<InvokeOutput> {
40+
try {
41+
const result = await ToolUtils.mcpManager!.callTool(this.serverName, this.toolName, this.input)
42+
const content = typeof result === 'object' ? JSON.stringify(result) : String(result)
43+
44+
return {
45+
output: {
46+
kind: OutputKind.Text,
47+
content,
48+
},
49+
}
50+
} catch (error: any) {
51+
this.logger.error(`Failed to invoke MCP tool: ${error.message ?? error}`)
52+
throw new Error(`Failed to invoke MCP tool: ${error.message ?? error}`)
53+
}
54+
}
55+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
export interface MCPServerConfig {
7+
command: string
8+
args?: string[]
9+
env?: Record<string, string>
10+
disabled?: boolean
11+
autoApprove?: string[]
12+
}
13+
14+
export interface MCPConfig {
15+
mcpServers: Record<string, MCPServerConfig>
16+
}
17+
18+
export interface ListToolsResponse {
19+
tools: {
20+
name?: string
21+
description?: string
22+
inputSchema?: object
23+
[key: string]: any
24+
}[]
25+
}

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

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,27 @@ import {
1515
fsReadToolResponseSize,
1616
} from './toolShared'
1717
import { ListDirectory, ListDirectoryParams } from './listDirectory'
18+
import { McpTool } from './mcp/mcpTool'
19+
import { McpManager } from './mcp/mcpManager'
1820

1921
export enum ToolType {
2022
FsRead = 'fsRead',
2123
FsWrite = 'fsWrite',
2224
ExecuteBash = 'executeBash',
2325
ListDirectory = 'listDirectory',
26+
Mcp = 'mcp',
2427
}
2528

2629
export type Tool =
2730
| { type: ToolType.FsRead; tool: FsRead }
2831
| { type: ToolType.FsWrite; tool: FsWrite }
2932
| { type: ToolType.ExecuteBash; tool: ExecuteBash }
3033
| { type: ToolType.ListDirectory; tool: ListDirectory }
34+
| { type: ToolType.Mcp; tool: McpTool }
3135

3236
export class ToolUtils {
37+
static mcpManager?: McpManager
38+
3339
static displayName(tool: Tool): string {
3440
switch (tool.type) {
3541
case ToolType.FsRead:
@@ -40,6 +46,8 @@ export class ToolUtils {
4046
return 'Execute shell command'
4147
case ToolType.ListDirectory:
4248
return 'List directory from filesystem'
49+
case ToolType.Mcp:
50+
return 'Execute MCP tool'
4351
}
4452
}
4553

@@ -53,6 +61,8 @@ export class ToolUtils {
5361
return tool.tool.requiresAcceptance()
5462
case ToolType.ListDirectory:
5563
return tool.tool.requiresAcceptance()
64+
case ToolType.Mcp:
65+
return { requiresAcceptance: false }
5666
}
5767
}
5868

@@ -75,6 +85,8 @@ export class ToolUtils {
7585
return tool.tool.invoke(updates ?? undefined, cancellationToken)
7686
case ToolType.ListDirectory:
7787
return tool.tool.invoke(updates)
88+
case ToolType.Mcp:
89+
return tool.tool.invoke(updates)
7890
}
7991
}
8092

@@ -159,7 +171,18 @@ export class ToolUtils {
159171
type: ToolType.ListDirectory,
160172
tool: new ListDirectory(value.input as unknown as ListDirectoryParams),
161173
}
162-
default:
174+
default: {
175+
const mcpToolDef = ToolUtils.mcpManager?.findTool(value.name as string)
176+
if (mcpToolDef) {
177+
return {
178+
type: ToolType.Mcp,
179+
tool: new McpTool({
180+
serverName: mcpToolDef.serverName,
181+
toolName: mcpToolDef.toolName,
182+
input: value.input, // pass LLM's JSON input
183+
}),
184+
}
185+
}
163186
return {
164187
toolUseId: value.toolUseId,
165188
content: [
@@ -169,6 +192,7 @@ export class ToolUtils {
169192
} as ToolResultContentBlock,
170193
],
171194
}
195+
}
172196
}
173197
} catch (error) {
174198
return mapErr(error)

packages/core/src/shared/logger/logger.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export type LogTopic =
2222
| 'listDirectory'
2323
| 'chatStream'
2424
| 'chatHistoryDb'
25+
| 'mcp'
2526
| 'unknown'
2627

2728
class ErrorLog {

0 commit comments

Comments
 (0)