improved mcp support, including standard "mcpServers" setting with multiple named servers with command/args/env/cwd (#392)

This commit is contained in:
Olcan 2025-05-16 16:29:03 -07:00 committed by GitHub
parent e158a0d59f
commit d9bd2b0e14
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 77 additions and 42 deletions

View File

@ -402,6 +402,7 @@ export async function loadCliConfig(settings: Settings): Promise<Config> {
settings.toolDiscoveryCommand,
settings.toolCallCommand,
settings.mcpServerCommand,
settings.mcpServers,
userAgent,
memoryContent,
fileCount,

View File

@ -7,6 +7,7 @@
import * as fs from 'fs';
import * as path from 'path';
import { homedir } from 'os';
import { StdioServerParameters } from '@modelcontextprotocol/sdk/client/stdio.js';
export const SETTINGS_DIRECTORY_NAME = '.gemini';
export const USER_SETTINGS_DIR = path.join(homedir(), SETTINGS_DIRECTORY_NAME);
@ -23,6 +24,7 @@ export interface Settings {
toolDiscoveryCommand?: string;
toolCallCommand?: string;
mcpServerCommand?: string;
mcpServers?: Record<string, StdioServerParameters>;
// Add other settings here.
}
@ -67,9 +69,10 @@ export class LoadedSettings {
setValue(
scope: SettingScope,
key: keyof Settings,
value: string | undefined,
value: string | Record<string, StdioServerParameters> | undefined,
): void {
const settingsFile = this.forScope(scope);
// @ts-expect-error - value can be string | Record<string, StdioServerParameters>
settingsFile.settings[key] = value;
this._merged = this.computeMergedSettings();
saveSettings(settingsFile);

View File

@ -59,6 +59,7 @@ describe('Server Config (config.ts)', () => {
undefined, // toolDiscoveryCommand
undefined, // toolCallCommand
undefined, // mcpServerCommand
undefined, // mcpServers
USER_AGENT,
USER_MEMORY, // Pass memory here
);
@ -83,6 +84,7 @@ describe('Server Config (config.ts)', () => {
undefined,
undefined,
undefined,
undefined,
USER_AGENT,
// No userMemory argument
);
@ -102,6 +104,7 @@ describe('Server Config (config.ts)', () => {
undefined,
undefined,
undefined,
undefined,
USER_AGENT,
USER_MEMORY, // Pass memory here
);
@ -125,6 +128,7 @@ describe('Server Config (config.ts)', () => {
undefined,
undefined,
undefined,
undefined,
USER_AGENT,
// No userMemory argument
);
@ -147,6 +151,7 @@ describe('Server Config (config.ts)', () => {
undefined,
undefined,
undefined,
undefined,
USER_AGENT,
USER_MEMORY,
);

View File

@ -20,6 +20,7 @@ import { WriteFileTool } from '../tools/write-file.js';
import { WebFetchTool } from '../tools/web-fetch.js';
import { ReadManyFilesTool } from '../tools/read-many-files.js';
import { BaseTool, ToolResult } from '../tools/tools.js';
import { StdioServerParameters } from '@modelcontextprotocol/sdk/client/stdio.js';
export class Config {
private toolRegistry: ToolRegistry;
@ -35,6 +36,9 @@ export class Config {
private readonly toolDiscoveryCommand: string | undefined,
private readonly toolCallCommand: string | undefined,
private readonly mcpServerCommand: string | undefined,
private readonly mcpServers:
| Record<string, StdioServerParameters>
| undefined,
private readonly userAgent: string,
private userMemory: string = '', // Made mutable for refresh
private geminiMdFileCount: number = 0,
@ -86,6 +90,10 @@ export class Config {
return this.mcpServerCommand;
}
getMcpServers(): Record<string, StdioServerParameters> | undefined {
return this.mcpServers;
}
getUserAgent(): string {
return this.userAgent;
}
@ -146,6 +154,7 @@ export function createServerConfig(
toolDiscoveryCommand?: string,
toolCallCommand?: string,
mcpServerCommand?: string,
mcpServers?: Record<string, StdioServerParameters>,
userAgent?: string,
userMemory?: string,
geminiMdFileCount?: number,
@ -161,6 +170,7 @@ export function createServerConfig(
toolDiscoveryCommand,
toolCallCommand,
mcpServerCommand,
mcpServers,
userAgent ?? 'GeminiCLI/unknown', // Default user agent
userMemory ?? '',
geminiMdFileCount ?? 0,

View File

@ -7,6 +7,7 @@
import { FunctionDeclaration } from '@google/genai';
import { Tool, ToolResult, BaseTool } from './tools.js';
import { Config } from '../config/config.js';
import { parse } from 'shell-quote';
import { spawn, execSync } from 'node:child_process';
// TODO: remove this dependency once MCP support is built into genai SDK
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
@ -26,7 +27,7 @@ export class DiscoveredTool extends BaseTool<ToolParams, ToolResult> {
This tool was discovered from the project by executing the command \`${discoveryCmd}\` on project root.
When called, this tool will execute the command \`${callCommand} ${name}\` on project root.
Tool discovery and call commands can be configured in project settings.
Tool discovery and call commands can be configured in project or user settings.
When called, the tool call command is executed as a subprocess.
On success, tool output is returned as a json string.
@ -99,13 +100,11 @@ export class DiscoveredMCPTool extends BaseTool<ToolParams, ToolResult> {
readonly description: string,
readonly parameterSchema: Record<string, unknown>,
) {
const mcpServerCmd = config.getMcpServerCommand()!;
description += `
This MCP tool was discovered from a local MCP server using JSON RPC 2.0 over stdio transport protocol.
The MCP server was started by executing the command \`${mcpServerCmd}\` on project root.
When called, this tool will invoke the \`tools/call\` method for tool name \`${name}\`.
MCP server command can be configured in project settings.
MCP servers can be configured in project or user settings.
Returns the MCP server response as a json string.
`;
super(name, name, description, parameterSchema);
@ -125,7 +124,6 @@ Returns the MCP server response as a json string.
export class ToolRegistry {
private tools: Map<string, Tool> = new Map();
private mcpClient: Client | null = null;
constructor(private readonly config: Config) {}
@ -174,48 +172,66 @@ export class ToolRegistry {
);
}
}
// discover tools using MCP server command, if configured
const mcpServerCmd = this.config.getMcpServerCommand();
if (mcpServerCmd) {
// discover tools using MCP servers, if configured
// convert mcpServerCommand (if any) to StdioServerParameters
const mcpServers = this.config.getMcpServers() || {};
if (this.config.getMcpServerCommand()) {
const cmd = this.config.getMcpServerCommand()!;
const args = parse(cmd, process.env) as string[];
if (args.some((arg) => typeof arg !== 'string')) {
throw new Error('failed to parse mcpServerCommand: ' + cmd);
}
// use generic server name 'mcp'
mcpServers['mcp'] = {
command: args[0],
args: args.slice(1),
};
}
for (const [mcpServerName, mcpServer] of Object.entries(mcpServers)) {
(async () => {
if (!this.mcpClient) {
this.mcpClient = new Client({
name: 'mcp-client',
version: '0.0.1',
});
const transport = new StdioClientTransport({
command: mcpServerCmd,
stderr: 'pipe',
});
try {
await this.mcpClient.connect(transport);
} catch (error) {
console.error(
'failed to start or connect to MCP server using ' +
`command '${mcpServerCmd}'; \n${error}`,
);
throw error;
}
this.mcpClient.onerror = (error) => {
console.error('MCP ERROR', error.toString());
};
if (!transport.stderr) {
throw new Error('transport missing stderr stream');
}
transport.stderr.on('data', (data) => {
// filter out INFO messages logged for each request received
if (!data.toString().includes('] INFO')) {
console.log('MCP STDERR', data.toString());
}
});
const mcpClient = new Client({
name: 'mcp-client',
version: '0.0.1',
});
const transport = new StdioClientTransport({
...mcpServer,
env: {
...process.env,
...(mcpServer.env || {}),
} as Record<string, string>,
stderr: 'pipe',
});
try {
await mcpClient.connect(transport);
} catch (error) {
console.error(
`failed to start or connect to MCP server '${mcpServerName}' ` +
`${JSON.stringify(mcpServer)}; \n${error}`,
);
throw error;
}
const result = await this.mcpClient.listTools();
mcpClient.onerror = (error) => {
console.error('MCP ERROR', error.toString());
};
if (!transport.stderr) {
throw new Error('transport missing stderr stream');
}
transport.stderr.on('data', (data) => {
// filter out INFO messages logged for each request received
if (!data.toString().includes('] INFO')) {
console.log('MCP STDERR', data.toString());
}
});
const result = await mcpClient.listTools();
for (const tool of result.tools) {
this.registerTool(
new DiscoveredMCPTool(
this.mcpClient,
mcpClient,
this.config,
tool.name,
Object.keys(mcpServers).length > 1
? mcpServerName + '__' + tool.name
: tool.name,
tool.description ?? '',
tool.inputSchema,
),