mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-14 20:10:36 -07:00
317 lines
9.3 KiB
TypeScript
317 lines
9.3 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
// File for 'gemini mcp add' command
|
|
import type { CommandModule } from 'yargs';
|
|
import { loadSettings, SettingScope } from '../../config/settings.js';
|
|
import {
|
|
debugLogger,
|
|
type MCPServerConfig,
|
|
type McpContext,
|
|
createTransport,
|
|
createSandboxManager,
|
|
} from '@google/gemini-cli-core';
|
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
import { exitCli } from '../utils.js';
|
|
|
|
async function addMcpServer(
|
|
name: string,
|
|
commandOrUrl: string,
|
|
args: Array<string | number> | undefined,
|
|
options: {
|
|
scope: string;
|
|
transport: string;
|
|
env: string[] | undefined;
|
|
header: string[] | undefined;
|
|
timeout?: number;
|
|
trust?: boolean;
|
|
description?: string;
|
|
includeTools?: string[];
|
|
excludeTools?: string[];
|
|
},
|
|
) {
|
|
const {
|
|
scope,
|
|
transport,
|
|
env,
|
|
header,
|
|
timeout,
|
|
trust,
|
|
description,
|
|
includeTools,
|
|
excludeTools,
|
|
} = options;
|
|
|
|
const settings = loadSettings(process.cwd());
|
|
const inHome = settings.workspace.path === settings.user.path;
|
|
|
|
if (scope === 'project' && inHome) {
|
|
debugLogger.error(
|
|
'Error: Please use --scope user to edit settings in the home directory.',
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const settingsScope =
|
|
scope === 'user' ? SettingScope.User : SettingScope.Workspace;
|
|
|
|
let newServer: Partial<MCPServerConfig> = {};
|
|
|
|
const headers = header?.reduce(
|
|
(acc, curr) => {
|
|
const [key, ...valueParts] = curr.split(':');
|
|
const value = valueParts.join(':').trim();
|
|
if (key.trim() && value) {
|
|
acc[key.trim()] = value;
|
|
}
|
|
return acc;
|
|
},
|
|
{} as Record<string, string>,
|
|
);
|
|
|
|
switch (transport) {
|
|
case 'sse':
|
|
newServer = {
|
|
url: commandOrUrl,
|
|
type: 'sse',
|
|
headers,
|
|
timeout,
|
|
trust,
|
|
description,
|
|
includeTools,
|
|
excludeTools,
|
|
};
|
|
break;
|
|
case 'http':
|
|
newServer = {
|
|
url: commandOrUrl,
|
|
type: 'http',
|
|
headers,
|
|
timeout,
|
|
trust,
|
|
description,
|
|
includeTools,
|
|
excludeTools,
|
|
};
|
|
break;
|
|
case 'stdio':
|
|
default:
|
|
newServer = {
|
|
command: commandOrUrl,
|
|
args: args?.map(String),
|
|
env: env?.reduce(
|
|
(acc, curr) => {
|
|
const [key, value] = curr.split('=');
|
|
if (key && value) {
|
|
acc[key] = value;
|
|
}
|
|
return acc;
|
|
},
|
|
{} as Record<string, string>,
|
|
),
|
|
timeout,
|
|
trust,
|
|
description,
|
|
includeTools,
|
|
excludeTools,
|
|
};
|
|
break;
|
|
}
|
|
|
|
const existingSettings = settings.forScope(settingsScope).settings;
|
|
const mcpServers = existingSettings.mcpServers || {};
|
|
|
|
const isExistingServer = !!mcpServers[name];
|
|
if (isExistingServer) {
|
|
debugLogger.log(
|
|
`MCP server "${name}" is already configured within ${scope} settings.`,
|
|
);
|
|
}
|
|
|
|
mcpServers[name] = newServer as MCPServerConfig;
|
|
|
|
settings.setValue(settingsScope, 'mcpServers', mcpServers);
|
|
|
|
if (isExistingServer) {
|
|
debugLogger.log(`MCP server "${name}" updated in ${scope} settings.`);
|
|
} else {
|
|
debugLogger.log(
|
|
`MCP server "${name}" added to ${scope} settings. (${transport})`,
|
|
);
|
|
}
|
|
|
|
const isSandboxEnabled =
|
|
typeof settings.merged.tools?.sandbox === 'object'
|
|
? settings.merged.tools.sandbox.enabled
|
|
: !!settings.merged.tools?.sandbox;
|
|
|
|
if (isSandboxEnabled) {
|
|
const mcpContext: McpContext = {
|
|
sanitizationConfig: {
|
|
enableEnvironmentVariableRedaction: true,
|
|
allowedEnvironmentVariables: [],
|
|
blockedEnvironmentVariables:
|
|
settings.merged.advanced?.excludedEnvVars ?? [],
|
|
},
|
|
emitMcpDiagnostic: () => {},
|
|
isTrustedFolder: () => true,
|
|
isSandboxEnabled: () => true,
|
|
sandboxManager: createSandboxManager(
|
|
{ enabled: true },
|
|
{ workspace: process.cwd(), modeConfig: { readonly: true } },
|
|
),
|
|
};
|
|
|
|
try {
|
|
const serverTransport = await createTransport(
|
|
name,
|
|
newServer as MCPServerConfig,
|
|
false,
|
|
mcpContext,
|
|
);
|
|
const client = new Client(
|
|
{ name: 'gemini-cli-discovery', version: '1.0.0' },
|
|
{ capabilities: {} },
|
|
);
|
|
await client.connect(serverTransport);
|
|
const result = await client.listTools();
|
|
const readOnlyTools = result.tools
|
|
.filter(
|
|
(t: { name: string; annotations?: { readOnlyHint?: boolean } }) =>
|
|
t.annotations?.readOnlyHint === true,
|
|
)
|
|
.map((t) => t.name);
|
|
|
|
if (readOnlyTools.length > 0) {
|
|
debugLogger.warn(
|
|
`Warning: With sandboxing enabled, the following tools are marked as read-only and will AUTO-EXECUTE without confirmation:\n${readOnlyTools
|
|
.map((t) => ` * ${t}`)
|
|
.join('\n')}`,
|
|
);
|
|
}
|
|
await client.close();
|
|
} catch {
|
|
debugLogger.warn(
|
|
'Warning: With sandboxing enabled, any read-only tools provided by this server will AUTO-EXECUTE without confirmation.',
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
export const addCommand: CommandModule = {
|
|
command: 'add <name> <commandOrUrl> [args...]',
|
|
describe: 'Add a server',
|
|
builder: (yargs) =>
|
|
yargs
|
|
.usage('Usage: gemini mcp add [options] <name> <commandOrUrl> [args...]')
|
|
.parserConfiguration({
|
|
'unknown-options-as-args': true, // Pass unknown options as server args
|
|
'populate--': true, // Populate server args after -- separator
|
|
})
|
|
.positional('name', {
|
|
describe: 'Name of the server',
|
|
type: 'string',
|
|
demandOption: true,
|
|
})
|
|
.positional('commandOrUrl', {
|
|
describe: 'Command (stdio) or URL (sse, http)',
|
|
type: 'string',
|
|
demandOption: true,
|
|
})
|
|
.option('scope', {
|
|
alias: 's',
|
|
describe: 'Configuration scope (user or project)',
|
|
type: 'string',
|
|
default: 'project',
|
|
choices: ['user', 'project'],
|
|
})
|
|
.option('transport', {
|
|
alias: ['t', 'type'],
|
|
describe: 'Transport type (stdio, sse, http)',
|
|
type: 'string',
|
|
default: 'stdio',
|
|
choices: ['stdio', 'sse', 'http'],
|
|
})
|
|
.option('env', {
|
|
alias: 'e',
|
|
describe: 'Set environment variables (e.g. -e KEY=value)',
|
|
type: 'array',
|
|
string: true,
|
|
nargs: 1,
|
|
})
|
|
.option('header', {
|
|
alias: 'H',
|
|
describe:
|
|
'Set HTTP headers for SSE and HTTP transports (e.g. -H "X-Api-Key: abc123" -H "Authorization: Bearer abc123")',
|
|
type: 'array',
|
|
string: true,
|
|
nargs: 1,
|
|
})
|
|
.option('timeout', {
|
|
describe: 'Set connection timeout in milliseconds',
|
|
type: 'number',
|
|
})
|
|
.option('trust', {
|
|
describe:
|
|
'Trust the server (bypass all tool call confirmation prompts)',
|
|
type: 'boolean',
|
|
})
|
|
.option('description', {
|
|
describe: 'Set the description for the server',
|
|
type: 'string',
|
|
})
|
|
.option('include-tools', {
|
|
describe: 'A comma-separated list of tools to include',
|
|
type: 'array',
|
|
string: true,
|
|
})
|
|
.option('exclude-tools', {
|
|
describe: 'A comma-separated list of tools to exclude',
|
|
type: 'array',
|
|
string: true,
|
|
})
|
|
.middleware((argv) => {
|
|
// Handle -- separator args as server args if present
|
|
if (argv['--']) {
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
const existingArgs = (argv['args'] as Array<string | number>) || [];
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
argv['args'] = [...existingArgs, ...(argv['--'] as string[])];
|
|
}
|
|
}),
|
|
handler: async (argv) => {
|
|
await addMcpServer(
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
argv['name'] as string,
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
argv['commandOrUrl'] as string,
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
argv['args'] as Array<string | number>,
|
|
{
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
scope: argv['scope'] as string,
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
transport: argv['transport'] as string,
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
env: argv['env'] as string[],
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
header: argv['header'] as string[],
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
timeout: argv['timeout'] as number | undefined,
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
trust: argv['trust'] as boolean | undefined,
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
description: argv['description'] as string | undefined,
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
includeTools: argv['includeTools'] as string[] | undefined,
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
excludeTools: argv['excludeTools'] as string[] | undefined,
|
|
},
|
|
);
|
|
await exitCli();
|
|
},
|
|
};
|