diff --git a/README.md b/README.md index ea5dcde..42da1b8 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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) diff --git a/docs/mcp.md b/docs/mcp.md new file mode 100644 index 0000000..09df8cc --- /dev/null +++ b/docs/mcp.md @@ -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/) diff --git a/src/prompt.ts b/src/prompt.ts index e1def61..f2573ff 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -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", @@ -610,5 +610,9 @@ export function getTools(_options: PromptToolOptions = {}): ToolDefinition[] { }, }); + for (const tool of externalTools) { + tools.push(tool); + } + return tools; } diff --git a/src/session.ts b/src/session.ts index 0116fbd..a3d6a3e 100644 --- a/src/session.ts +++ b/src/session.ts @@ -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"; @@ -180,6 +181,8 @@ export class SessionManager { private activePromptController: AbortController | null = null; private readonly sessionControllers = new Map(); private readonly toolExecutor: ToolExecutor; + private readonly mcpManager = new McpManager(); + private mcpToolDefinitions: ToolDefinition[] = []; constructor(options: SessionManagerOptions) { this.projectRoot = options.projectRoot; @@ -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 }> + ): Promise { + await this.mcpManager.initialize(servers); + this.mcpToolDefinitions = this.mcpManager.getMcpToolDefinitions(); + } + + getMcpStatus() { + return this.mcpManager.getStatus(); } private estimateStreamTokens(text: string): number { @@ -999,7 +1013,7 @@ ${skillMd} { model, messages, - tools: getTools(this.getPromptToolOptions()), + tools: getTools(this.getPromptToolOptions(), this.mcpToolDefinitions), ...thinkingOptions, }, { signal: sessionController.signal }, diff --git a/src/settings.ts b/src/settings.ts index e1a9b09..3c046b4 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -9,6 +9,12 @@ export type DeepcodingEnv = { export type ReasoningEffort = "high" | "max"; +export type McpServerConfig = { + command: string; + args?: string[]; + env?: Record; +}; + export type DeepcodingSettings = { env?: DeepcodingEnv; model?: string; @@ -17,6 +23,7 @@ export type DeepcodingSettings = { debugLogEnabled?: boolean; notify?: string; webSearchTool?: string; + mcpServers?: Record; }; export type ResolvedDeepcodingSettings = { @@ -28,6 +35,7 @@ export type ResolvedDeepcodingSettings = { debugLogEnabled: boolean; notify?: string; webSearchTool?: string; + mcpServers?: Record; }; export type ModelConfigSelection = { @@ -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, @@ -72,6 +82,7 @@ export function resolveSettings( debugLogEnabled: settings?.debugLogEnabled === true, notify: notify || undefined, webSearchTool: webSearchTool || undefined, + mcpServers, }; } diff --git a/src/tests/slashCommands.test.ts b/src/tests/slashCommands.test.ts index 7d9510a..45b6fdf 100644 --- a/src/tests/slashCommands.test.ts +++ b/src/tests/slashCommands.test.ts @@ -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", () => { diff --git a/src/tools/executor.ts b/src/tools/executor.ts index eec0b20..d857ad7 100644 --- a/src/tools/executor.ts +++ b/src/tools/executor.ts @@ -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; @@ -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(); - constructor(projectRoot: string, createOpenAIClient?: CreateOpenAIClient) { + constructor(projectRoot: string, createOpenAIClient?: CreateOpenAIClient, mcpManager?: McpManager) { this.projectRoot = projectRoot; this.createOpenAIClient = createOpenAIClient; + this.mcpManager = mcpManager; this.registerToolHandlers(); } @@ -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, diff --git a/src/tools/mcp-client.ts b/src/tools/mcp-client.ts new file mode 100644 index 0000000..89b0c2c --- /dev/null +++ b/src/tools/mcp-client.ts @@ -0,0 +1,181 @@ +import { spawn, type ChildProcess } from "child_process"; +import { createInterface, type Interface } from "readline"; +import * as os from "os"; + +type JsonRpcRequest = { + jsonrpc: "2.0"; + id: number; + method: string; + params?: Record; +}; + +type JsonRpcResponse = { + jsonrpc: "2.0"; + id: number; + result?: unknown; + error?: { code: number; message: string; data?: unknown }; +}; + +export type McpToolDefinition = { + name: string; + description?: string; + inputSchema: { + type: "object"; + properties: Record; + required?: string[]; + }; +}; + +type ListToolsResult = { + tools: McpToolDefinition[]; +}; + +type CallToolResult = { + content: Array<{ type: string; text?: string }>; + isError?: boolean; +}; + +export class McpClient { + private process: ChildProcess | null = null; + private reader: Interface | null = null; + private nextId = 1; + private pendingRequests = new Map void; reject: (error: Error) => void }>(); + private buffer = ""; + + constructor( + private readonly serverName: string, + private readonly command: string, + private readonly args: string[] = [], + private readonly env?: Record + ) {} + + async connect(): Promise { + return new Promise((resolve, reject) => { + const childEnv = { + ...process.env, + ...this.env, + }; + + const isWindows = os.platform() === "win32"; + + if (isWindows) { + // On Windows, .cmd files require shell: true to be spawned. + // Build a single command string so cmd.exe handles quoting correctly. + const cmd = [this.command + ".cmd", ...this.args].join(" "); + this.process = spawn(cmd, [], { + stdio: ["pipe", "pipe", "pipe"], + env: childEnv, + shell: true, + windowsHide: true, + }); + } else { + this.process = spawn(this.command, this.args, { + stdio: ["pipe", "pipe", "pipe"], + env: childEnv, + }); + } + + this.process.on("error", (err) => { + reject(new Error(`Failed to start MCP server "${this.serverName}" (${this.command}): ${err.message}`)); + }); + + this.process.on("exit", (code) => { + const error = new Error(`MCP server "${this.serverName}" exited with code ${code}`); + for (const [, pending] of this.pendingRequests) { + pending.reject(error); + } + this.pendingRequests.clear(); + }); + + if (this.process.stderr) { + this.process.stderr.on("data", (data: Buffer) => { + // MCP servers log to stderr; we ignore for now + }); + } + + this.reader = createInterface({ input: this.process.stdout! }); + this.reader.on("line", (line: string) => { + this.handleLine(line); + }); + + // Send initialize request (MCP protocol handshake) + this.sendRequest("initialize", { + protocolVersion: "2024-11-05", + capabilities: {}, + clientInfo: { name: "deepcode-cli", version: "0.1.0" }, + }) + .then(() => { + // Send initialized notification + this.sendNotification("notifications/initialized"); + resolve(); + }) + .catch(reject); + }); + } + + async listTools(): Promise { + const result = (await this.sendRequest("tools/list", {})) as ListToolsResult; + return result.tools ?? []; + } + + async callTool(name: string, args: Record): Promise { + return (await this.sendRequest("tools/call", { name, arguments: args })) as CallToolResult; + } + + disconnect(): void { + if (this.reader) { + this.reader.close(); + this.reader = null; + } + if (this.process) { + this.process.kill(); + this.process = null; + } + } + + private sendRequest(method: string, params: Record): Promise { + return new Promise((resolve, reject) => { + const id = this.nextId++; + const request: JsonRpcRequest = { + jsonrpc: "2.0", + id, + method, + params, + }; + this.pendingRequests.set(id, { resolve, reject }); + this.writeLine(JSON.stringify(request)); + }); + } + + private sendNotification(method: string, params?: Record): void { + const notification = { + jsonrpc: "2.0" as const, + method, + params, + }; + this.writeLine(JSON.stringify(notification)); + } + + private writeLine(data: string): void { + if (this.process?.stdin) { + this.process.stdin.write(data + "\n"); + } + } + + private handleLine(line: string): void { + try { + const message = JSON.parse(line) as JsonRpcResponse; + if (message.id !== undefined && this.pendingRequests.has(message.id)) { + const pending = this.pendingRequests.get(message.id)!; + this.pendingRequests.delete(message.id); + if (message.error) { + pending.reject(new Error(`MCP error: ${message.error.message}`)); + } else { + pending.resolve(message.result); + } + } + } catch { + // Ignore unparseable lines + } + } +} diff --git a/src/tools/mcp-manager.ts b/src/tools/mcp-manager.ts new file mode 100644 index 0000000..4bac5e9 --- /dev/null +++ b/src/tools/mcp-manager.ts @@ -0,0 +1,145 @@ +import { McpClient, type McpToolDefinition } from "./mcp-client"; +import type { McpServerConfig } from "../settings"; + +type McpToolEntry = { + serverName: string; + originalName: string; + namespacedName: string; + definition: McpToolDefinition; + client: McpClient; +}; + +export type McpServerStatus = { + name: string; + connected: boolean; + error?: string; + toolCount: number; + tools: string[]; +}; + +export class McpManager { + private clients: McpClient[] = []; + private tools: McpToolEntry[] = []; + private initialized = false; + private serverStatuses: McpServerStatus[] = []; + + async initialize(servers?: Record): Promise { + if (this.initialized) return; + this.initialized = true; + + if (!servers || Object.keys(servers).length === 0) return; + + const entries = Object.entries(servers); + for (const [name, config] of entries) { + try { + const client = new McpClient(name, config.command, config.args ?? [], config.env); + await client.connect(); + this.clients.push(client); + + const serverTools = await client.listTools(); + const toolNames: string[] = []; + for (const tool of serverTools) { + const namespacedName = `mcp__${name}__${tool.name}`; + this.tools.push({ + serverName: name, + originalName: tool.name, + namespacedName, + definition: tool, + client, + }); + toolNames.push(tool.name); + } + this.serverStatuses.push({ + name, + connected: true, + toolCount: serverTools.length, + tools: toolNames, + }); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + process.stderr.write(`[deepcode] MCP server "${name}" failed to initialize: ${message}\n`); + this.serverStatuses.push({ + name, + connected: false, + error: message, + toolCount: 0, + tools: [], + }); + } + } + } + + getStatus(): McpServerStatus[] { + return this.serverStatuses; + } + + getMcpToolDefinitions(): Array<{ + type: "function"; + function: { + name: string; + description: string; + parameters: { + type: "object"; + properties: Record; + required?: string[]; + additionalProperties?: boolean; + }; + }; + }> { + return this.tools.map((t) => ({ + type: "function" as const, + function: { + name: t.namespacedName, + description: t.definition.description ?? `${t.serverName}: ${t.originalName}`, + parameters: { + type: "object" as const, + properties: t.definition.inputSchema.properties, + required: t.definition.inputSchema.required, + additionalProperties: false, + }, + }, + })); + } + + isMcpTool(name: string): boolean { + return name.startsWith("mcp__"); + } + + async executeMcpTool( + name: string, + args: Record + ): Promise<{ ok: boolean; name: string; output?: string; error?: string }> { + const tool = this.tools.find((t) => t.namespacedName === name); + if (!tool) { + return { ok: false, name, error: `Unknown MCP tool: ${name}` }; + } + + try { + const result = await tool.client.callTool(tool.originalName, args); + const text = result.content + .filter((c) => c.type === "text" && c.text) + .map((c) => c.text) + .join("\n"); + return { + ok: !result.isError, + name, + output: text || JSON.stringify(result.content), + }; + } catch (err) { + return { + ok: false, + name, + error: err instanceof Error ? err.message : String(err), + }; + } + } + + disconnect(): void { + for (const client of this.clients) { + client.disconnect(); + } + this.clients = []; + this.tools = []; + this.initialized = false; + } +} diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 4c13909..beab530 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -126,6 +126,11 @@ export function App({ projectRoot, version = "", onRestart }: AppProps): React.R void refreshSkills(); }, [refreshSessionsList, refreshSkills]); + useEffect(() => { + const settings = resolveCurrentSettings(); + void sessionManager.initMcpServers(settings.mcpServers); + }, [sessionManager]); + const writeRef = useRef(write); writeRef.current = write; const handlePrompt = useCallback( @@ -174,6 +179,38 @@ export function App({ projectRoot, version = "", onRestart }: AppProps): React.R setView("session-list"); return; } + if (submission.command === "mcp") { + process.stdout.write("\n"); + process.stdout.write(chalk.bold.cyan("MCP Server Status\n")); + process.stdout.write(chalk.dim("─────────────────\n")); + const statuses = sessionManager.getMcpStatus(); + if (statuses.length === 0) { + process.stdout.write(chalk.dim(" No MCP servers configured.\n")); + } else { + for (const s of statuses) { + const icon = s.connected ? chalk.green("✔") : chalk.red("✖"); + process.stdout.write(` ${icon} ${chalk.bold(s.name)}`); + if (s.connected) { + process.stdout.write(chalk.dim(` (${s.toolCount} tools)`)); + } else { + process.stdout.write(chalk.dim(` — ${s.error ?? "failed"}`)); + } + process.stdout.write("\n"); + if (s.connected && s.tools.length > 0) { + for (const tool of s.tools) { + process.stdout.write(chalk.dim(` - mcp__${s.name}__${tool}\n`)); + } + } + } + } + process.stdout.write(chalk.dim("─────────────────\n")); + process.stdout.write( + chalk.dim(` Total: ${statuses.filter((s) => s.connected).length} connected, `) + + chalk.dim(`${statuses.filter((s) => !s.connected).length} failed\n`) + ); + process.stdout.write("\n"); + return; + } const prompt: UserPromptContent = { text: submission.text, diff --git a/src/ui/PromptInput.tsx b/src/ui/PromptInput.tsx index 74c38d5..24560e2 100644 --- a/src/ui/PromptInput.tsx +++ b/src/ui/PromptInput.tsx @@ -40,7 +40,7 @@ export type PromptSubmission = { text: string; imageUrls: string[]; selectedSkills?: SkillInfo[]; - command?: "new" | "resume" | "exit"; + command?: "new" | "resume" | "mcp" | "exit"; }; type Props = { @@ -570,6 +570,14 @@ export const PromptInput = React.memo(function PromptInput({ setShowSkillsDropdown(false); return; } + if (item.kind === "mcp") { + onSubmit({ text: "/mcp", imageUrls: [], command: "mcp" }); + setBuffer(EMPTY_BUFFER); + setImageUrls([]); + setSelectedSkills([]); + setShowSkillsDropdown(false); + return; + } if (item.kind === "exit") { onSubmit({ text: "/exit", imageUrls: [], command: "exit" }); setBuffer(EMPTY_BUFFER); diff --git a/src/ui/slashCommands.ts b/src/ui/slashCommands.ts index c772124..c565606 100644 --- a/src/ui/slashCommands.ts +++ b/src/ui/slashCommands.ts @@ -1,6 +1,6 @@ import type { SkillInfo } from "../session"; -export type SlashCommandKind = "skill" | "skills" | "model" | "new" | "init" | "resume" | "exit"; +export type SlashCommandKind = "skill" | "skills" | "model" | "new" | "init" | "resume" | "mcp" | "exit"; export type SlashCommandItem = { kind: SlashCommandKind; @@ -41,6 +41,12 @@ export const BUILTIN_SLASH_COMMANDS: SlashCommandItem[] = [ label: "/resume", description: "Pick a previous conversation to continue", }, + { + kind: "mcp", + name: "mcp", + label: "/mcp", + description: "Show MCP server status and available tools", + }, { kind: "exit", name: "exit",