Skip to content

Commit 2347b9a

Browse files
refactor: 将 mcp-server 解耦为独立社区包
Co-authored-by: aider (vertex_ai/gemini-2.5-pro) <aider@aider.chat>
1 parent 04b94e4 commit 2347b9a

File tree

9 files changed

+730
-61
lines changed

9 files changed

+730
-61
lines changed

packages/cli/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
"type": "module",
77
"main": "dist/index.js",
88
"exports": {
9-
".": "./dist/index.js",
10-
"./public-api": "./dist/src/public-api.js"
9+
".": "./dist/index.js"
1110
},
1211
"bin": {
1312
"gemini": "dist/index.js"

packages/mcp-server/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "@google/gemini-mcp-server",
2+
"name": "@gemini-community/gemini-mcp-server",
33
"version": "0.1.8",
44
"type": "module",
55
"main": "dist/index.js",
@@ -21,11 +21,14 @@
2121
"prepublishOnly": "node ../../scripts/prepublish.js"
2222
},
2323
"dependencies": {
24-
"@google/gemini-cli": "*",
2524
"@google/gemini-cli-core": "*",
2625
"@modelcontextprotocol/sdk": "^1.13.2",
26+
"command-exists": "^1.2.9",
27+
"dotenv": "^16.6.1",
2728
"express": "^5.1.0",
2829
"openai": "^5.8.2",
30+
"read-package-up": "^11.0.0",
31+
"strip-json-comments": "^3.1.1",
2932
"zod": "^3.23.8"
3033
},
3134
"devDependencies": {
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import {
8+
Config,
9+
loadServerHierarchicalMemory,
10+
setGeminiMdFilename as setServerGeminiMdFilename,
11+
getCurrentGeminiMdFilename,
12+
ApprovalMode,
13+
GEMINI_CONFIG_DIR as GEMINI_DIR,
14+
DEFAULT_GEMINI_MODEL,
15+
DEFAULT_GEMINI_EMBEDDING_MODEL,
16+
FileDiscoveryService,
17+
TelemetryTarget,
18+
} from '@google/gemini-cli-core';
19+
import { Settings } from './settings.js';
20+
21+
import { Extension } from './extension.js';
22+
import * as dotenv from 'dotenv';
23+
import * as fs from 'node:fs';
24+
import * as path from 'node:path';
25+
import * as os from 'node:os';
26+
import { loadSandboxConfig } from './sandboxConfig.js';
27+
28+
// Simple console logger for now - replace with actual logger if available
29+
const logger = {
30+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
31+
debug: (...args: any[]) => console.debug('[DEBUG]', ...args),
32+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
33+
warn: (...args: any[]) => console.warn('[WARN]', ...args),
34+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
35+
error: (...args: any[]) => console.error('[ERROR]', ...args),
36+
};
37+
38+
// This function is now a thin wrapper around the server's implementation.
39+
// It's kept in the CLI for now as App.tsx directly calls it for memory refresh.
40+
// TODO: Consider if App.tsx should get memory via a server call or if Config should refresh itself.
41+
export async function loadHierarchicalGeminiMemory(
42+
currentWorkingDirectory: string,
43+
debugMode: boolean,
44+
fileService: FileDiscoveryService,
45+
extensionContextFilePaths: string[] = [],
46+
): Promise<{ memoryContent: string; fileCount: number }> {
47+
if (debugMode) {
48+
logger.debug(
49+
`CLI: Delegating hierarchical memory load to server for CWD: ${currentWorkingDirectory}`,
50+
);
51+
}
52+
// Directly call the server function.
53+
// The server function will use its own homedir() for the global path.
54+
return loadServerHierarchicalMemory(
55+
currentWorkingDirectory,
56+
debugMode,
57+
fileService,
58+
extensionContextFilePaths,
59+
);
60+
}
61+
62+
export async function loadServerConfig(
63+
settings: Settings,
64+
extensions: Extension[],
65+
sessionId: string,
66+
debugMode: boolean,
67+
): Promise<Config> {
68+
loadEnvironment();
69+
70+
// Set the context filename in the server's memoryTool module BEFORE loading memory
71+
// TODO(b/343434939): This is a bit of a hack. The contextFileName should ideally be passed
72+
// directly to the Config constructor in core, and have core handle setGeminiMdFilename.
73+
// However, loadHierarchicalGeminiMemory is called *before* createServerConfig.
74+
if (settings.contextFileName) {
75+
setServerGeminiMdFilename(settings.contextFileName);
76+
} else {
77+
// Reset to default if not provided in settings.
78+
setServerGeminiMdFilename(getCurrentGeminiMdFilename());
79+
}
80+
81+
const extensionContextFilePaths = extensions.flatMap((e) => e.contextFiles);
82+
83+
const fileService = new FileDiscoveryService(process.cwd());
84+
// Call the (now wrapper) loadHierarchicalGeminiMemory which calls the server's version
85+
const { memoryContent, fileCount } = await loadHierarchicalGeminiMemory(
86+
process.cwd(),
87+
debugMode,
88+
fileService,
89+
extensionContextFilePaths,
90+
);
91+
92+
const mcpServers = mergeMcpServers(settings, extensions);
93+
94+
const sandboxConfig = await loadSandboxConfig(settings, {});
95+
96+
return new Config({
97+
sessionId,
98+
embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL,
99+
sandbox: sandboxConfig,
100+
targetDir: process.cwd(),
101+
debugMode,
102+
question: undefined,
103+
fullContext: false,
104+
coreTools: settings.coreTools || undefined,
105+
excludeTools: settings.excludeTools || undefined,
106+
toolDiscoveryCommand: settings.toolDiscoveryCommand,
107+
toolCallCommand: settings.toolCallCommand,
108+
mcpServerCommand: settings.mcpServerCommand,
109+
mcpServers,
110+
userMemory: memoryContent,
111+
geminiMdFileCount: fileCount,
112+
approvalMode: ApprovalMode.YOLO,
113+
showMemoryUsage: settings.showMemoryUsage || false,
114+
accessibility: settings.accessibility,
115+
telemetry: {
116+
enabled: settings.telemetry?.enabled,
117+
target: settings.telemetry?.target as TelemetryTarget,
118+
otlpEndpoint:
119+
process.env.OTEL_EXPORTER_OTLP_ENDPOINT ??
120+
settings.telemetry?.otlpEndpoint,
121+
logPrompts: settings.telemetry?.logPrompts,
122+
},
123+
usageStatisticsEnabled: settings.usageStatisticsEnabled ?? true,
124+
// Git-aware file filtering settings
125+
fileFiltering: {
126+
respectGitIgnore: settings.fileFiltering?.respectGitIgnore,
127+
enableRecursiveFileSearch:
128+
settings.fileFiltering?.enableRecursiveFileSearch,
129+
},
130+
checkpointing: settings.checkpointing?.enabled,
131+
proxy:
132+
process.env.HTTPS_PROXY ||
133+
process.env.https_proxy ||
134+
process.env.HTTP_PROXY ||
135+
process.env.http_proxy,
136+
cwd: process.cwd(),
137+
fileDiscoveryService: fileService,
138+
bugCommand: settings.bugCommand,
139+
model: process.env.GEMINI_MODEL || DEFAULT_GEMINI_MODEL,
140+
extensionContextFilePaths,
141+
});
142+
}
143+
144+
function mergeMcpServers(settings: Settings, extensions: Extension[]) {
145+
const mcpServers = { ...(settings.mcpServers || {}) };
146+
for (const extension of extensions) {
147+
Object.entries(extension.config.mcpServers || {}).forEach(
148+
([key, server]) => {
149+
if (mcpServers[key]) {
150+
logger.warn(
151+
`Skipping extension MCP config for server with key "${key}" as it already exists.`,
152+
);
153+
return;
154+
}
155+
mcpServers[key] = server;
156+
},
157+
);
158+
}
159+
return mcpServers;
160+
}
161+
function findEnvFile(startDir: string): string | null {
162+
let currentDir = path.resolve(startDir);
163+
while (true) {
164+
// prefer gemini-specific .env under GEMINI_DIR
165+
const geminiEnvPath = path.join(currentDir, GEMINI_DIR, '.env');
166+
if (fs.existsSync(geminiEnvPath)) {
167+
return geminiEnvPath;
168+
}
169+
const envPath = path.join(currentDir, '.env');
170+
if (fs.existsSync(envPath)) {
171+
return envPath;
172+
}
173+
const parentDir = path.dirname(currentDir);
174+
if (parentDir === currentDir || !parentDir) {
175+
// check .env under home as fallback, again preferring gemini-specific .env
176+
const homeGeminiEnvPath = path.join(os.homedir(), GEMINI_DIR, '.env');
177+
if (fs.existsSync(homeGeminiEnvPath)) {
178+
return homeGeminiEnvPath;
179+
}
180+
const homeEnvPath = path.join(os.homedir(), '.env');
181+
if (fs.existsSync(homeEnvPath)) {
182+
return homeEnvPath;
183+
}
184+
return null;
185+
}
186+
currentDir = parentDir;
187+
}
188+
}
189+
190+
export function loadEnvironment(): void {
191+
const envFilePath = findEnvFile(process.cwd());
192+
if (envFilePath) {
193+
dotenv.config({ path: envFilePath, quiet: true });
194+
}
195+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { MCPServerConfig } from '@google/gemini-cli-core';
8+
import * as fs from 'fs';
9+
import * as path from 'path';
10+
import * as os from 'os';
11+
12+
export const EXTENSIONS_DIRECTORY_NAME = path.join('.gemini', 'extensions');
13+
export const EXTENSIONS_CONFIG_FILENAME = 'gemini-extension.json';
14+
15+
export interface Extension {
16+
config: ExtensionConfig;
17+
contextFiles: string[];
18+
}
19+
20+
export interface ExtensionConfig {
21+
name: string;
22+
version: string;
23+
mcpServers?: Record<string, MCPServerConfig>;
24+
contextFileName?: string | string[];
25+
}
26+
27+
export function loadExtensions(workspaceDir: string): Extension[] {
28+
const allExtensions = [
29+
...loadExtensionsFromDir(workspaceDir),
30+
...loadExtensionsFromDir(os.homedir()),
31+
];
32+
33+
const uniqueExtensions: Extension[] = [];
34+
const seenNames = new Set<string>();
35+
for (const extension of allExtensions) {
36+
if (!seenNames.has(extension.config.name)) {
37+
console.log(
38+
`Loading extension: ${extension.config.name} (version: ${extension.config.version})`,
39+
);
40+
uniqueExtensions.push(extension);
41+
seenNames.add(extension.config.name);
42+
}
43+
}
44+
45+
return uniqueExtensions;
46+
}
47+
48+
function loadExtensionsFromDir(dir: string): Extension[] {
49+
const extensionsDir = path.join(dir, EXTENSIONS_DIRECTORY_NAME);
50+
if (!fs.existsSync(extensionsDir)) {
51+
return [];
52+
}
53+
54+
const extensions: Extension[] = [];
55+
for (const subdir of fs.readdirSync(extensionsDir)) {
56+
const extensionDir = path.join(extensionsDir, subdir);
57+
58+
const extension = loadExtension(extensionDir);
59+
if (extension != null) {
60+
extensions.push(extension);
61+
}
62+
}
63+
return extensions;
64+
}
65+
66+
function loadExtension(extensionDir: string): Extension | null {
67+
if (!fs.statSync(extensionDir).isDirectory()) {
68+
console.error(
69+
`Warning: unexpected file ${extensionDir} in extensions directory.`,
70+
);
71+
return null;
72+
}
73+
74+
const configFilePath = path.join(extensionDir, EXTENSIONS_CONFIG_FILENAME);
75+
if (!fs.existsSync(configFilePath)) {
76+
console.error(
77+
`Warning: extension directory ${extensionDir} does not contain a config file ${configFilePath}.`,
78+
);
79+
return null;
80+
}
81+
82+
try {
83+
const configContent = fs.readFileSync(configFilePath, 'utf-8');
84+
const config = JSON.parse(configContent) as ExtensionConfig;
85+
if (!config.name || !config.version) {
86+
console.error(
87+
`Invalid extension config in ${configFilePath}: missing name or version.`,
88+
);
89+
return null;
90+
}
91+
92+
const contextFiles = getContextFileNames(config)
93+
.map((contextFileName) => path.join(extensionDir, contextFileName))
94+
.filter((contextFilePath) => fs.existsSync(contextFilePath));
95+
96+
return {
97+
config,
98+
contextFiles,
99+
};
100+
} catch (e) {
101+
console.error(
102+
`Warning: error parsing extension config in ${configFilePath}: ${e}`,
103+
);
104+
return null;
105+
}
106+
}
107+
108+
function getContextFileNames(config: ExtensionConfig): string[] {
109+
if (!config.contextFileName) {
110+
return ['GEMINI.md'];
111+
} else if (!Array.isArray(config.contextFileName)) {
112+
return [config.contextFileName];
113+
}
114+
return config.contextFileName;
115+
}

0 commit comments

Comments
 (0)