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.toolDiscoveryCommand,
settings.toolCallCommand, settings.toolCallCommand,
settings.mcpServerCommand, settings.mcpServerCommand,
settings.mcpServers,
userAgent, userAgent,
memoryContent, memoryContent,
fileCount, fileCount,

View File

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

View File

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

View File

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

View File

@ -7,6 +7,7 @@
import { FunctionDeclaration } from '@google/genai'; import { FunctionDeclaration } from '@google/genai';
import { Tool, ToolResult, BaseTool } from './tools.js'; import { Tool, ToolResult, BaseTool } from './tools.js';
import { Config } from '../config/config.js'; import { Config } from '../config/config.js';
import { parse } from 'shell-quote';
import { spawn, execSync } from 'node:child_process'; import { spawn, execSync } from 'node:child_process';
// TODO: remove this dependency once MCP support is built into genai SDK // TODO: remove this dependency once MCP support is built into genai SDK
import { Client } from '@modelcontextprotocol/sdk/client/index.js'; 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. 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. 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. When called, the tool call command is executed as a subprocess.
On success, tool output is returned as a json string. 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 description: string,
readonly parameterSchema: Record<string, unknown>, readonly parameterSchema: Record<string, unknown>,
) { ) {
const mcpServerCmd = config.getMcpServerCommand()!;
description += ` description += `
This MCP tool was discovered from a local MCP server using JSON RPC 2.0 over stdio transport protocol. 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}\`. 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. Returns the MCP server response as a json string.
`; `;
super(name, name, description, parameterSchema); super(name, name, description, parameterSchema);
@ -125,7 +124,6 @@ Returns the MCP server response as a json string.
export class ToolRegistry { export class ToolRegistry {
private tools: Map<string, Tool> = new Map(); private tools: Map<string, Tool> = new Map();
private mcpClient: Client | null = null;
constructor(private readonly config: Config) {} constructor(private readonly config: Config) {}
@ -174,29 +172,46 @@ export class ToolRegistry {
); );
} }
} }
// discover tools using MCP server command, if configured // discover tools using MCP servers, if configured
const mcpServerCmd = this.config.getMcpServerCommand(); // convert mcpServerCommand (if any) to StdioServerParameters
if (mcpServerCmd) { 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 () => { (async () => {
if (!this.mcpClient) { const mcpClient = new Client({
this.mcpClient = new Client({
name: 'mcp-client', name: 'mcp-client',
version: '0.0.1', version: '0.0.1',
}); });
const transport = new StdioClientTransport({ const transport = new StdioClientTransport({
command: mcpServerCmd, ...mcpServer,
env: {
...process.env,
...(mcpServer.env || {}),
} as Record<string, string>,
stderr: 'pipe', stderr: 'pipe',
}); });
try { try {
await this.mcpClient.connect(transport); await mcpClient.connect(transport);
} catch (error) { } catch (error) {
console.error( console.error(
'failed to start or connect to MCP server using ' + `failed to start or connect to MCP server '${mcpServerName}' ` +
`command '${mcpServerCmd}'; \n${error}`, `${JSON.stringify(mcpServer)}; \n${error}`,
); );
throw error; throw error;
} }
this.mcpClient.onerror = (error) => { mcpClient.onerror = (error) => {
console.error('MCP ERROR', error.toString()); console.error('MCP ERROR', error.toString());
}; };
if (!transport.stderr) { if (!transport.stderr) {
@ -208,14 +223,15 @@ export class ToolRegistry {
console.log('MCP STDERR', data.toString()); console.log('MCP STDERR', data.toString());
} }
}); });
} const result = await mcpClient.listTools();
const result = await this.mcpClient.listTools();
for (const tool of result.tools) { for (const tool of result.tools) {
this.registerTool( this.registerTool(
new DiscoveredMCPTool( new DiscoveredMCPTool(
this.mcpClient, mcpClient,
this.config, this.config,
tool.name, Object.keys(mcpServers).length > 1
? mcpServerName + '__' + tool.name
: tool.name,
tool.description ?? '', tool.description ?? '',
tool.inputSchema, tool.inputSchema,
), ),