improved mcp support, including standard "mcpServers" setting with multiple named servers with command/args/env/cwd (#392)
This commit is contained in:
parent
e158a0d59f
commit
d9bd2b0e14
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,48 +172,66 @@ 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({
|
...mcpServer,
|
||||||
command: mcpServerCmd,
|
env: {
|
||||||
stderr: 'pipe',
|
...process.env,
|
||||||
});
|
...(mcpServer.env || {}),
|
||||||
try {
|
} as Record<string, string>,
|
||||||
await this.mcpClient.connect(transport);
|
stderr: 'pipe',
|
||||||
} catch (error) {
|
});
|
||||||
console.error(
|
try {
|
||||||
'failed to start or connect to MCP server using ' +
|
await mcpClient.connect(transport);
|
||||||
`command '${mcpServerCmd}'; \n${error}`,
|
} catch (error) {
|
||||||
);
|
console.error(
|
||||||
throw error;
|
`failed to start or connect to MCP server '${mcpServerName}' ` +
|
||||||
}
|
`${JSON.stringify(mcpServer)}; \n${error}`,
|
||||||
this.mcpClient.onerror = (error) => {
|
);
|
||||||
console.error('MCP ERROR', error.toString());
|
throw error;
|
||||||
};
|
|
||||||
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 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) {
|
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,
|
||||||
),
|
),
|
||||||
|
|
Loading…
Reference in New Issue