Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

[Deep Code](https://github.com/lessweb/deepcode-cli) 是专为 `deepseek-v4` 模型优化的终端 AI 编码助手,支持深度思考、推理强度控制以及 Agent Skills。

## 🚀 新增功能(本 Fork)

### `/mcp` Skill 与 MCP 实现

本 Fork 新增了 `/mcp` 命令和 MCP(Model Context Protocol)集成,让 Deep Code CLI 能够连接外部工具和服务:

- **`/mcp` Skill**:一键管理 MCP 服务器连接,支持添加、移除、列出已配置的 MCP 服务。
- **MCP 协议实现**:支持与 GitHub、文件系统、数据库等多种外部服务的标准化集成,大幅扩展 AI 助手的操作能力。

通过 MCP,你现在可以让 Deep Code 直接操作 GitHub 仓库、读取文件、查询数据库等,而无需离开终端。

📖 **详细配置指南:** [docs/mcp.md](docs/mcp.md)

## 安装

```bash
Expand Down Expand Up @@ -98,6 +111,10 @@ Deep Code自带免费的、且大部分情况够用的Web Search工具。如果
}
```

### 如何配置 MCP?

Deep Code CLI 支持 MCP(Model Context Protocol),可以连接 GitHub、浏览器、文件系统等外部服务。配置方法请查看:[docs/mcp.md](docs/mcp.md)

## 获取帮助

- 在 GitHub Issues 上报告错误或请求功能 (https://github.com/lessweb/deepcode-cli/issues)
Expand Down
205 changes: 205 additions & 0 deletions docs/mcp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
# Deep Code CLI MCP 配置指南

Deep Code CLI 支持 MCP(Model Context Protocol),让 AI 助手能够连接外部工具和服务,如 GitHub、浏览器、数据库等。

## 概述

配置 MCP 后,Deep Code 可以:

- 操作 GitHub 仓库(查看 Issues、创建 PR、搜索代码等)
- 操控浏览器(截图、点击、填表单等)
- 访问文件系统
- 连接数据库和 API
- ...以及任何兼容 MCP 协议的外部服务

MCP 工具在 Deep Code 中的命名格式为 `mcp__<服务名>__<工具名>`,例如 `mcp__github__search_code`。

## 配置 MCP 服务器

编辑 `~/.deepcode/settings.json`,添加 `mcpServers` 字段:

```json
{
"env": {
"MODEL": "deepseek-v4-pro",
"BASE_URL": "https://api.deepseek.com",
"API_KEY": "sk-..."
},
"thinkingEnabled": true,
"reasoningEffort": "max",
"mcpServers": {
"<服务名称>": {
"command": "<可执行文件>",
"args": ["<参数1>", "<参数2>"],
"env": {
"<环境变量>": "<值>"
}
}
}
}
```

### 配置项说明

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `command` | string | 是 | MCP 服务器的可执行文件路径或命令(如 `npx`、`node`、`python`) |
| `args` | string[] | 否 | 传递给命令的参数列表 |
| `env` | object | 否 | 传递给 MCP 服务器进程的环境变量(如 API Key) |

## 常用 MCP 示例

### GitHub MCP

让 Deep Code 直接操作 GitHub 仓库(搜索代码、管理 Issue/PR、读写文件等):

```json
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxxxxxxxxxx"
}
}
}
}
```

> GitHub Personal Access Token 可在 [GitHub Settings > Developer settings > Personal access tokens](https://github.com/settings/tokens) 生成。

### 浏览器控制(Playwright)

让 Deep Code 操控浏览器进行截图、页面操作等:

```json
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest"]
}
}
}
```

### 文件系统

让 Deep Code 在指定目录中读写文件:

```json
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed/dir"]
}
}
}
```

### 自定义 Python MCP

```json
{
"mcpServers": {
"my-tool": {
"command": "python",
"args": ["-m", "my_mcp_server"],
"env": {
"API_KEY": "xxx"
}
}
}
}
```

## 完整配置示例

以下是一个配置了 GitHub 和 Playwright 两个 MCP 服务器的完整 `~/.deepcode/settings.json`:

```json
{
"env": {
"MODEL": "deepseek-v4-pro",
"BASE_URL": "https://api.deepseek.com",
"API_KEY": "sk-xxxxxxxxxxxx"
},
"thinkingEnabled": true,
"reasoningEffort": "max",
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxxxxxxxxxx"
}
},
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest"]
}
}
}
```

## 使用 MCP

配置完成后,启动 `deepcode`,使用 `/mcp` 命令管理 MCP 连接:

- `/mcp` — 查看已配置的 MCP 服务器状态
- `/mcp add` — 添加新的 MCP 服务器
- `/mcp remove` — 移除 MCP 服务器
- `/mcp list` — 列出所有已连接的 MCP 服务器及其工具

在对话中直接使用 MCP 工具名称即可调用,例如:

```
帮我搜索 GitHub 上 deepcode-cli 仓库的 issues
```

AI 会自动调用 `mcp__github__search_issues` 工具完成操作。

## 工具命名规则

MCP 工具名称由三部分组成:`mcp__<服务名>__<工具名>`

| 服务名 | 工具名 | 完整调用名 |
|--------|--------|-----------|
| github | search_code | `mcp__github__search_code` |
| github | create_pull_request | `mcp__github__create_pull_request` |
| playwright | browser_navigate | `mcp__playwright__browser_navigate` |
| playwright | browser_take_screenshot | `mcp__playwright__browser_take_screenshot` |

你可以通过 `/mcp list` 查看每个服务器提供的具体工具列表。

## 故障排查

### 启动失败

如果 MCP 服务器无法启动,检查:

1. `command` 是否已安装(如 `npx` 需要 Node.js)
2. `env` 中的环境变量是否正确(如 `GITHUB_PERSONAL_ACCESS_TOKEN`)
3. 运行 `deepcode` 的终端是否有网络访问权限

### 工具不显示

1. 确认 `settings.json` 中的 `mcpServers` 字段格式正确
2. 启动 deepcode 后使用 `/mcp` 查看服务器状态
3. 如果服务器状态显示错误,根据错误信息排查

### Windows 用户

在 Windows 上,Deep Code CLI 会自动为 `.cmd` 命令添加 shell 支持。如果你的 MCP 命令是批处理脚本,确保文件名以 `.cmd` 结尾。

## 编写你自己的 MCP 服务器

MCP 服务器遵循 [Model Context Protocol](https://modelcontextprotocol.io/) 规范,使用 JSON-RPC 2.0 通信。你可以用任何语言编写 MCP 服务器,只要实现以下协议即可:

1. `initialize` — 握手和协议协商
2. `tools/list` — 返回可用工具列表
3. `tools/call` — 执行工具调用

更多参考:[MCP 官方文档](https://modelcontextprotocol.io/)
6 changes: 5 additions & 1 deletion src/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ export type ToolDefinition = {
};
};

export function getTools(_options: PromptToolOptions = {}): ToolDefinition[] {
export function getTools(_options: PromptToolOptions = {}, externalTools: ToolDefinition[] = []): ToolDefinition[] {
const tools: ToolDefinition[] = [
{
type: "function",
Expand Down Expand Up @@ -610,5 +610,9 @@ export function getTools(_options: PromptToolOptions = {}): ToolDefinition[] {
},
});

for (const tool of externalTools) {
tools.push(tool);
}

return tools;
}
20 changes: 17 additions & 3 deletions src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import type { ChatCompletionMessageParam, ChatCompletionContentPart } from "open
import { launchNotifyScript } from "./notify";
import { buildThinkingRequestOptions } from "./openai-thinking";
import { DEEPSEEK_V4_MODELS } from "./model-capabilities";
import { getCompactPrompt, getSystemPrompt, getTools, AGENT_DRIFT_GUARD_SKILL } from "./prompt";
import { getCompactPrompt, getSystemPrompt, getTools, AGENT_DRIFT_GUARD_SKILL, type ToolDefinition } from "./prompt";
import { ToolExecutor, type CreateOpenAIClient } from "./tools/executor";
import { McpManager } from "./tools/mcp-manager";
import { logApiError } from "./error-logger";
import { logOpenAIChatCompletionDebug, normalizeDebugError } from "./debug-logger";

Expand Down Expand Up @@ -180,6 +181,8 @@ export class SessionManager {
private activePromptController: AbortController | null = null;
private readonly sessionControllers = new Map<string, AbortController>();
private readonly toolExecutor: ToolExecutor;
private readonly mcpManager = new McpManager();
private mcpToolDefinitions: ToolDefinition[] = [];

constructor(options: SessionManagerOptions) {
this.projectRoot = options.projectRoot;
Expand All @@ -188,7 +191,18 @@ export class SessionManager {
this.onAssistantMessage = options.onAssistantMessage;
this.onSessionEntryUpdated = options.onSessionEntryUpdated;
this.onLlmStreamProgress = options.onLlmStreamProgress;
this.toolExecutor = new ToolExecutor(this.projectRoot, this.createOpenAIClient);
this.toolExecutor = new ToolExecutor(this.projectRoot, this.createOpenAIClient, this.mcpManager);
}

async initMcpServers(
servers?: Record<string, { command: string; args?: string[]; env?: Record<string, string> }>
): Promise<void> {
await this.mcpManager.initialize(servers);
this.mcpToolDefinitions = this.mcpManager.getMcpToolDefinitions();
}

getMcpStatus() {
return this.mcpManager.getStatus();
}

private estimateStreamTokens(text: string): number {
Expand Down Expand Up @@ -999,7 +1013,7 @@ ${skillMd}
{
model,
messages,
tools: getTools(this.getPromptToolOptions()),
tools: getTools(this.getPromptToolOptions(), this.mcpToolDefinitions),
...thinkingOptions,
},
{ signal: sessionController.signal },
Expand Down
11 changes: 11 additions & 0 deletions src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ export type DeepcodingEnv = {

export type ReasoningEffort = "high" | "max";

export type McpServerConfig = {
command: string;
args?: string[];
env?: Record<string, string>;
};

export type DeepcodingSettings = {
env?: DeepcodingEnv;
model?: string;
Expand All @@ -17,6 +23,7 @@ export type DeepcodingSettings = {
debugLogEnabled?: boolean;
notify?: string;
webSearchTool?: string;
mcpServers?: Record<string, McpServerConfig>;
};

export type ResolvedDeepcodingSettings = {
Expand All @@ -28,6 +35,7 @@ export type ResolvedDeepcodingSettings = {
debugLogEnabled: boolean;
notify?: string;
webSearchTool?: string;
mcpServers?: Record<string, McpServerConfig>;
};

export type ModelConfigSelection = {
Expand Down Expand Up @@ -63,6 +71,8 @@ export function resolveSettings(
const notify = typeof settings?.notify === "string" ? settings.notify.trim() : "";
const webSearchTool = typeof settings?.webSearchTool === "string" ? settings.webSearchTool.trim() : "";

const mcpServers = settings?.mcpServers;

return {
apiKey: env.API_KEY?.trim(),
baseURL: env.BASE_URL?.trim() || defaults.baseURL,
Expand All @@ -72,6 +82,7 @@ export function resolveSettings(
debugLogEnabled: settings?.debugLogEnabled === true,
notify: notify || undefined,
webSearchTool: webSearchTool || undefined,
mcpServers,
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/tests/slashCommands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ test("buildSlashCommands prefixes skills before built-ins", () => {
assert.equal(items[0].kind, "skill");
assert.equal(items[0].name, "skill-writer");
const builtinNames = items.filter((i) => i.kind !== "skill").map((i) => i.name);
assert.deepEqual(builtinNames, ["skills", "model", "new", "init", "resume", "exit"]);
assert.deepEqual(builtinNames, ["skills", "model", "new", "init", "resume", "mcp", "exit"]);
});

test("filterSlashCommands matches partial prefixes", () => {
Expand Down
11 changes: 10 additions & 1 deletion src/tools/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { handleEditTool } from "./edit-handler";
import { handleReadTool } from "./read-handler";
import { handleWebSearchTool } from "./web-search-handler";
import { handleWriteTool } from "./write-handler";
import type { McpManager } from "./mcp-manager";

export type CreateOpenAIClient = () => {
client: OpenAI | null;
Expand Down Expand Up @@ -73,11 +74,13 @@ export type ToolCallExecution = {
export class ToolExecutor {
private readonly projectRoot: string;
private readonly createOpenAIClient?: CreateOpenAIClient;
private readonly mcpManager?: McpManager;
private readonly toolHandlers = new Map<string, ToolHandler>();

constructor(projectRoot: string, createOpenAIClient?: CreateOpenAIClient) {
constructor(projectRoot: string, createOpenAIClient?: CreateOpenAIClient, mcpManager?: McpManager) {
this.projectRoot = projectRoot;
this.createOpenAIClient = createOpenAIClient;
this.mcpManager = mcpManager;
this.registerToolHandlers();
}

Expand Down Expand Up @@ -161,6 +164,12 @@ export class ToolExecutor {
const toolName = toolCall.function.name;
const handler = this.toolHandlers.get(toolName);
if (!handler) {
// Try MCP tools
if (toolName.startsWith("mcp__") && this.mcpManager) {
const parsedArgs = this.parseToolArguments(toolCall.function.arguments);
const args = parsedArgs.ok ? parsedArgs.args : {};
return this.mcpManager.executeMcpTool(toolName, args);
}
return {
ok: false,
name: toolName,
Expand Down
Loading