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.toolCallCommand,
|
||||
settings.mcpServerCommand,
|
||||
settings.mcpServers,
|
||||
userAgent,
|
||||
memoryContent,
|
||||
fileCount,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,29 +172,46 @@ 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({
|
||||
const mcpClient = new Client({
|
||||
name: 'mcp-client',
|
||||
version: '0.0.1',
|
||||
});
|
||||
const transport = new StdioClientTransport({
|
||||
command: mcpServerCmd,
|
||||
...mcpServer,
|
||||
env: {
|
||||
...process.env,
|
||||
...(mcpServer.env || {}),
|
||||
} as Record<string, string>,
|
||||
stderr: 'pipe',
|
||||
});
|
||||
try {
|
||||
await this.mcpClient.connect(transport);
|
||||
await mcpClient.connect(transport);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'failed to start or connect to MCP server using ' +
|
||||
`command '${mcpServerCmd}'; \n${error}`,
|
||||
`failed to start or connect to MCP server '${mcpServerName}' ` +
|
||||
`${JSON.stringify(mcpServer)}; \n${error}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
this.mcpClient.onerror = (error) => {
|
||||
mcpClient.onerror = (error) => {
|
||||
console.error('MCP ERROR', error.toString());
|
||||
};
|
||||
if (!transport.stderr) {
|
||||
|
@ -208,14 +223,15 @@ export class ToolRegistry {
|
|||
console.log('MCP STDERR', data.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
const result = await this.mcpClient.listTools();
|
||||
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,
|
||||
),
|
||||
|
|
Loading…
Reference in New Issue