Skip to content

Commit e1f01ef

Browse files
WitMiaoclaude
andcommitted
feat: switch exa MCP to local environment-based configuration
- Change exa MCP from remote URL-based to local exa-mcp-server package - Use environment variable (EXA_API_KEY) instead of URL parameter for API key - Update buildMcpServerConfig to support environment variable configuration - Add comprehensive tests for environment variable-based MCP services - Improve security by avoiding API key exposure in command arguments 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent b269be6 commit e1f01ef

8 files changed

Lines changed: 137 additions & 10 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Exa MCP配置格式更新计划
2+
3+
## 任务背景
4+
将exa MCP服务配置从URL参数方式改为环境变量方式,提高安全性和规范性。
5+
6+
## 目标配置格式
7+
```json
8+
{
9+
"exa": {
10+
"command": "npx",
11+
"args": ["-y", "exa-mcp-server"],
12+
"env": {
13+
"EXA_API_KEY": "your-api-key-here"
14+
}
15+
}
16+
}
17+
```
18+
19+
## 实施步骤
20+
21+
### 1. 修改MCP服务定义
22+
- 文件:`src/constants.ts`
23+
- 修改exa服务配置结构
24+
25+
### 2. 更新配置构建逻辑
26+
- 文件:`src/utils/mcp.ts`
27+
- 增强buildMcpServerConfig函数支持环境变量
28+
29+
### 3. 更新类型定义
30+
- 检查并确保类型正确
31+
32+
### 4. 测试验证
33+
- 运行类型检查和测试
34+
- 验证配置生成正确
35+
36+
## 执行时间
37+
2025-08-08

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ ZCF (Zero-Config Claude-Code Flow) is a CLI tool that automatically configures C
99
## Development Guidelines
1010

1111
- **Documentation Language**: Except for README_zh, all code comments and documentation should be written in English
12+
- When writing tests, first verify if relevant test files already exist to avoid unnecessary duplication
1213

1314
## Development Commands
1415

src/commands/init.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ export async function init(options: InitOptions = {}) {
322322
}
323323

324324
if (apiKey) {
325-
config = buildMcpServerConfig(service.config, apiKey, service.apiKeyPlaceholder);
325+
config = buildMcpServerConfig(service.config, apiKey, service.apiKeyPlaceholder, service.apiKeyEnvVar);
326326
} else {
327327
// Skip this service if no API key provided
328328
continue;

src/constants.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -467,12 +467,14 @@ export const MCP_SERVICES: McpService[] = [
467467
'zh-CN': '请输入 Exa API Key',
468468
en: 'Enter Exa API Key',
469469
},
470-
apiKeyPlaceholder: 'YOUR_EXA_API_KEY',
470+
apiKeyEnvVar: 'EXA_API_KEY',
471471
config: {
472472
type: 'stdio',
473473
command: 'npx',
474-
args: ['-y', 'mcp-remote', 'https://mcp.exa.ai/mcp?exaApiKey=YOUR_EXA_API_KEY'],
475-
env: {},
474+
args: ['-y', 'exa-mcp-server'],
475+
env: {
476+
EXA_API_KEY: 'YOUR_EXA_API_KEY',
477+
},
476478
},
477479
},
478480
];

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export interface McpService {
55
requiresApiKey: boolean
66
apiKeyPrompt?: { en: string; 'zh-CN': string }
77
apiKeyPlaceholder?: string
8+
apiKeyEnvVar?: string
89
config: McpServerConfig
910
}
1011

src/utils/features.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ export async function configureMcpFeature(scriptLang: SupportedLang) {
207207
});
208208

209209
if (apiKey) {
210-
config = buildMcpServerConfig(service.config, apiKey, service.apiKeyPlaceholder);
210+
config = buildMcpServerConfig(service.config, apiKey, service.apiKeyPlaceholder, service.apiKeyEnvVar);
211211
} else {
212212
continue;
213213
}

src/utils/mcp.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ function applyPlatformCommand(config: McpServerConfig): void {
5050
export function buildMcpServerConfig(
5151
baseConfig: McpServerConfig,
5252
apiKey?: string,
53-
placeholder: string = 'YOUR_EXA_API_KEY'
53+
placeholder: string = 'YOUR_EXA_API_KEY',
54+
envVarName?: string
5455
): McpServerConfig {
5556
// Deep clone the config to avoid mutation
5657
const config = deepClone(baseConfig);
@@ -62,12 +63,17 @@ export function buildMcpServerConfig(
6263
return config;
6364
}
6465

65-
// Replace API key placeholder in args if exists
66+
// New approach: If environment variable name is specified, set it directly
67+
if (envVarName && config.env) {
68+
config.env[envVarName] = apiKey;
69+
return config; // Return early for env-based configuration
70+
}
71+
72+
// Legacy approach: Replace placeholder in args and URL
6673
if (config.args) {
6774
config.args = config.args.map((arg: string) => arg.replace(placeholder, apiKey));
6875
}
6976

70-
// Replace in URL if exists
7177
if (config.url) {
7278
config.url = config.url.replace(placeholder, apiKey);
7379
}

test/unit/utils/mcp.test.ts

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
import * as jsonConfig from '../../../src/utils/json-config';
1212
import * as platform from '../../../src/utils/platform';
1313
import * as objectUtils from '../../../src/utils/object-utils';
14-
import { ClAUDE_CONFIG_FILE, CLAUDE_DIR } from '../../../src/constants';
14+
import { ClAUDE_CONFIG_FILE, CLAUDE_DIR, MCP_SERVICES } from '../../../src/constants';
1515

1616
vi.mock('../../../src/utils/json-config');
1717
vi.mock('../../../src/utils/platform');
@@ -185,6 +185,39 @@ describe('mcp utilities', () => {
185185

186186
expect(result.args).toEqual(['--key', 'api-key']);
187187
});
188+
189+
it('should set environment variable when envVarName is provided', () => {
190+
const baseConfig = {
191+
type: 'stdio' as const,
192+
command: 'npx',
193+
args: ['-y', 'exa-mcp-server'],
194+
env: { EXA_API_KEY: 'placeholder' }
195+
};
196+
vi.mocked(objectUtils.deepClone).mockReturnValue({ ...baseConfig });
197+
vi.mocked(platform.isWindows).mockReturnValue(false);
198+
199+
const result = buildMcpServerConfig(baseConfig, 'test-api-key', 'placeholder', 'EXA_API_KEY');
200+
201+
expect(result.env).toEqual({ EXA_API_KEY: 'test-api-key' });
202+
});
203+
204+
it('should handle environment variable config on Windows', () => {
205+
const baseConfig = {
206+
type: 'stdio' as const,
207+
command: 'npx',
208+
args: ['-y', 'exa-mcp-server'],
209+
env: { EXA_API_KEY: 'placeholder' }
210+
};
211+
vi.mocked(objectUtils.deepClone).mockReturnValue({ ...baseConfig });
212+
vi.mocked(platform.isWindows).mockReturnValue(true);
213+
vi.mocked(platform.getMcpCommand).mockReturnValue(['cmd', '/c', 'npx']);
214+
215+
const result = buildMcpServerConfig(baseConfig, 'test-api-key', 'placeholder', 'EXA_API_KEY');
216+
217+
expect(result.command).toBe('cmd');
218+
expect(result.args).toEqual(['/c', 'npx', '-y', 'exa-mcp-server']);
219+
expect(result.env).toEqual({ EXA_API_KEY: 'test-api-key' });
220+
});
188221
});
189222

190223
describe('fixWindowsMcpConfig', () => {
@@ -224,5 +257,52 @@ describe('mcp utilities', () => {
224257
});
225258
});
226259

227-
// Extended Tests
260+
describe('Exa MCP Service Integration', () => {
261+
beforeEach(() => {
262+
// Mock deepClone to return a proper copy
263+
vi.mocked(objectUtils.deepClone).mockImplementation(obj => JSON.parse(JSON.stringify(obj)));
264+
});
265+
266+
it('should have exa service configured with environment variable', () => {
267+
const exaService = MCP_SERVICES.find(s => s.id === 'exa');
268+
269+
expect(exaService).toBeDefined();
270+
expect(exaService!.config.command).toBe('npx');
271+
expect(exaService!.config.args).toContain('exa-mcp-server');
272+
expect(exaService!.config.env).toHaveProperty('EXA_API_KEY');
273+
expect(exaService!.apiKeyEnvVar).toBe('EXA_API_KEY');
274+
});
275+
276+
it('should build exa service config with API key in environment', () => {
277+
const exaService = MCP_SERVICES.find(s => s.id === 'exa');
278+
vi.mocked(platform.isWindows).mockReturnValue(false);
279+
280+
const config = buildMcpServerConfig(
281+
exaService!.config,
282+
'test-exa-key-123',
283+
undefined,
284+
exaService!.apiKeyEnvVar
285+
);
286+
287+
expect(config.env).toEqual({ EXA_API_KEY: 'test-exa-key-123' });
288+
expect(config.args).toEqual(['-y', 'exa-mcp-server']);
289+
});
290+
291+
it('should handle exa service on Windows platform', () => {
292+
const exaService = MCP_SERVICES.find(s => s.id === 'exa');
293+
vi.mocked(platform.isWindows).mockReturnValue(true);
294+
vi.mocked(platform.getMcpCommand).mockReturnValue(['cmd', '/c', 'npx']);
295+
296+
const config = buildMcpServerConfig(
297+
exaService!.config,
298+
'test-key',
299+
undefined,
300+
exaService!.apiKeyEnvVar
301+
);
302+
303+
expect(config.command).toBe('cmd');
304+
expect(config.args).toEqual(['/c', 'npx', '-y', 'exa-mcp-server']);
305+
expect(config.env).toEqual({ EXA_API_KEY: 'test-key' });
306+
});
307+
});
228308
});

0 commit comments

Comments
 (0)