jinzd-ai-cli 0.1.14 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/CLAUDE.md +33 -2
  2. package/dist/index.js +520 -7
  3. package/package.json +1 -1
package/CLAUDE.md CHANGED
@@ -5,6 +5,7 @@
5
5
  一个跨平台的 REPL 风格 AI 对话工具,支持多个主流 AI 提供商(Claude、Gemini、DeepSeek、智谱清言、Kimi),
6
6
  带有 **AI 工具调用(Agentic)** 能力,支持执行 bash 命令、读写文件、运行交互式程序、流式生成大文档。
7
7
  代理支持(`proxy` 配置字段 + 环境变量),Gemini 完整支持(2.5 Pro/Flash,array items schema 修复)。
8
+ **MCP 协议支持**:可接入外部 MCP 服务器,自动发现并注册工具,无缝融入 agentic 循环。
8
9
  设计上可扩展至 Electron/Tauri 桌面 GUI。
9
10
 
10
11
  ## 技术栈
@@ -44,7 +45,11 @@ src/
44
45
  │ ├── dev-state.ts # 开发状态交接(provider/model 切换时快照生成、save/load/clear)
45
46
  │ ├── setup-wizard.ts # @inquirer/prompts 首次运行交互式设置
46
47
  │ └── commands/
47
- │ └── index.ts # CommandRegistry + 15个命令(/help /about /provider /model /clear /session /system /context /status /search /undo /export /tools /config /exit)
48
+ │ └── index.ts # CommandRegistry + 17个命令(/help /about /provider /model /clear /session /system /context /status /search /undo /export /tools /plugins /mcp /config /exit)
49
+ ├── mcp/
50
+ │ ├── types.ts # MCP 协议类型定义(JSON-RPC、工具 schema、服务器配置)
51
+ │ ├── client.ts # McpClient(单个 MCP 服务器 STDIO 连接,JSON-RPC 通信)
52
+ │ └── manager.ts # McpManager(多服务器管理,工具发现与注册,MCP→Tool 转换)
48
53
  └── tools/
49
54
  ├── types.ts # ToolDefinition / ToolCall / ToolResult / DangerLevel / getDangerLevel
50
55
  ├── registry.ts # ToolRegistry(注册全部内置工具,共14个)
@@ -117,6 +122,30 @@ npm run pack:all # 同时打包所有平台
117
122
  }
118
123
  ```
119
124
 
125
+ ### MCP 服务器配置
126
+
127
+ 在 `config.json` 中声明 `mcpServers` 字段,启动时自动连接、发现工具并注册。格式兼容 Claude Desktop。
128
+
129
+ ```json
130
+ {
131
+ "mcpServers": {
132
+ "filesystem": {
133
+ "command": "npx",
134
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "D:/projects"],
135
+ "timeout": 30000
136
+ },
137
+ "github": {
138
+ "command": "node",
139
+ "args": ["path/to/github-server.js"],
140
+ "env": { "GITHUB_TOKEN": "ghp_xxx" }
141
+ }
142
+ }
143
+ }
144
+ ```
145
+
146
+ MCP 工具名格式:`mcp__<serverId>__<toolName>`,如 `mcp__filesystem__read_file`。
147
+ 所有 MCP 工具默认 `safe` 级别(用户主动配置即表示信任)。
148
+
120
149
  ## 环境变量(优先级高于配置文件)
121
150
 
122
151
  ```
@@ -176,6 +205,7 @@ AICLI_NO_STREAM 设为 1 禁用流式输出
176
205
  | `ask_user` | safe | AI 在 agentic 循环中向用户提问,等待文本回答后继续执行 |
177
206
  | `write_todos` | safe | AI 拆解复杂任务为子任务列表,终端实时渲染进度(pending/in_progress/completed) |
178
207
  | `google_search` | safe | Google Custom Search API 搜索网页,需配置 API Key + Search Engine ID (cx) |
208
+ | `mcp__*` | safe | MCP 服务器暴露的动态工具(命名格式:`mcp__<serverId>__<toolName>`) |
179
209
 
180
210
  ### 危险级别与确认机制
181
211
 
@@ -271,8 +301,9 @@ const stdinLines = Array.isArray(rawStdin) ? rawStdin.map(String)
271
301
  - [x] **`write_todos` 工具**(2026-02-23):AI 拆解复杂任务为子任务列表,终端实时渲染进度。参数采用 JSON 字符串(因 ToolParameterSchema 不支持嵌套对象数组),容错处理 AI 直接传数组。`execute()` 内直接 `console.log()` 渲染(绕过 executor 8 行截断),模块级变量保持会话内状态。
272
302
  - [x] **Google 搜索集成**(2026-02-23):`google_search` 工具通过 Google Custom Search JSON API 搜索网页。需配置 API Key(`apiKeys['google-search']` 或 `AICLI_API_KEY_GOOGLESEARCH`)和 Search Engine ID(`googleSearchEngineId` 或 `AICLI_GOOGLE_CX`)。自动走全局 proxy,15s 超时,返回 Markdown 格式结果列表。配置向导新增 `Configure Google Search` 入口。
273
303
 
304
+ - [x] **MCP 协议支持**(2026-02-23):轻量级 MCP 客户端(不依赖 SDK),STDIO 传输 + JSON-RPC 2.0 通信。`src/mcp/` 模块:`types.ts`(协议类型)、`client.ts`(单服务器连接)、`manager.ts`(多服务器管理 + Tool 转换)。配置兼容 Claude Desktop(`config.json` 的 `mcpServers` 字段)。启动自动连接、发现工具并注册到 ToolRegistry,工具名格式 `mcp__<serverId>__<toolName>`。新增 `/mcp` REPL 命令查看连接状态和工具列表。
305
+
274
306
  ### 下一步待实现
275
- - [ ] **MCP 协议支持**:Model Context Protocol 扩展生态,接入外部工具
276
307
  - [ ] **Agent Skills 系统**:可复用的专业技能包(`.aicli/skills/`)
277
308
 
278
309
  ## 已知待改进项
package/dist/index.js CHANGED
@@ -74,6 +74,16 @@ var ConfigSchema = z.object({
74
74
  // API Key 通过 apiKeys['google-search'] 或 AICLI_API_KEY_GOOGLESEARCH 环境变量配置
75
75
  // CX 也可通过 AICLI_GOOGLE_CX 环境变量覆盖
76
76
  googleSearchEngineId: z.string().optional(),
77
+ // MCP (Model Context Protocol) 服务器配置
78
+ // 声明外部 MCP 服务器,启动时自动连接、发现工具并注册
79
+ // 配置格式兼容 Claude Desktop(command + args + env)
80
+ // 示例:{ "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path"] } }
81
+ mcpServers: z.record(z.object({
82
+ command: z.string(),
83
+ args: z.array(z.string()).default([]),
84
+ env: z.record(z.string()).optional(),
85
+ timeout: z.number().default(3e4)
86
+ })).default({}),
77
87
  // 插件加载开关(安全控制)
78
88
  // 默认 false:不自动加载 ~/.aicli/plugins/ 中的插件文件。
79
89
  // 插件以完整 Node.js 权限在主进程中执行(可读写文件、访问网络、执行命令),
@@ -119,7 +129,8 @@ var EnvLoader = class {
119
129
  };
120
130
 
121
131
  // src/core/constants.ts
122
- var VERSION = "0.1.13";
132
+ var VERSION = "0.1.16";
133
+ var APP_NAME = "ai-cli";
123
134
  var CONFIG_DIR_NAME = ".aicli";
124
135
  var CONFIG_FILE_NAME = "config.json";
125
136
  var HISTORY_DIR_NAME = "history";
@@ -129,6 +140,10 @@ var MEMORY_FILE_NAME = "memory.md";
129
140
  var MEMORY_MAX_CHARS = 1e4;
130
141
  var DEV_STATE_FILE_NAME = "dev-state.md";
131
142
  var DEFAULT_MAX_TOKENS = 8192;
143
+ var MCP_TOOL_PREFIX = "mcp__";
144
+ var MCP_CONNECT_TIMEOUT = 3e4;
145
+ var MCP_CALL_TIMEOUT = 6e4;
146
+ var MCP_PROTOCOL_VERSION = "2024-11-05";
132
147
  var AUTHOR = "\u664B\u6B63\u4E1C";
133
148
  var AUTHOR_EMAIL = "zhengdong.jin@gmail.com";
134
149
  var DESCRIPTION = "\u8DE8\u5E73\u53F0 REPL \u98CE\u683C AI \u5BF9\u8BDD\u5DE5\u5177\uFF0C\u652F\u6301\u591A Provider \u4E0E Agentic \u5DE5\u5177\u8C03\u7528";
@@ -1417,7 +1432,7 @@ var Renderer = class {
1417
1432
  console.log(chalk.gray(" Commands : ") + chalk.dim("/help \xB7 /about \xB7 Ctrl+C to exit"));
1418
1433
  console.log();
1419
1434
  }
1420
- printAbout(pluginCount = 0) {
1435
+ printAbout(pluginCount = 0, mcpInfo) {
1421
1436
  const HR = chalk.dim(" " + "\u2500".repeat(56));
1422
1437
  const label = (s) => chalk.gray(` ${s.padEnd(6)}`);
1423
1438
  const tool = (name, desc) => chalk.cyan(` ${name.padEnd(22)}`) + chalk.dim(desc);
@@ -1434,8 +1449,12 @@ var Renderer = class {
1434
1449
  console.log(chalk.dim(" DeepSeek \xB7 Kimi (Moonshot) \xB7 Claude (Anthropic)"));
1435
1450
  console.log(chalk.dim(" Gemini (Google) \xB7 \u667A\u8C31\u6E05\u8A00 \xB7 \u81EA\u5B9A\u4E49 OpenAI \u517C\u5BB9"));
1436
1451
  console.log(HR);
1437
- const toolTotal = 14 + pluginCount;
1438
- const toolLabel = pluginCount > 0 ? `\uFF0C\u542B ${pluginCount} \u4E2A\u63D2\u4EF6` : "\uFF0C\u63D2\u4EF6\u53EF\u6269\u5C55";
1452
+ const mcpToolCount = mcpInfo?.tools ?? 0;
1453
+ const toolTotal = 14 + pluginCount + mcpToolCount;
1454
+ const extras = [];
1455
+ if (pluginCount > 0) extras.push(`${pluginCount} \u4E2A\u63D2\u4EF6`);
1456
+ if (mcpToolCount > 0) extras.push(`${mcpToolCount} \u4E2A MCP`);
1457
+ const toolLabel = extras.length > 0 ? `\uFF0C\u542B ${extras.join(" + ")}` : "\uFF0C\u63D2\u4EF6/MCP \u53EF\u6269\u5C55";
1439
1458
  console.log(chalk.gray(` Agentic \u5DE5\u5177\uFF08${toolTotal}\u4E2A${toolLabel}\uFF09\uFF1A`));
1440
1459
  console.log(tool("bash", "\u6267\u884C Shell \u547D\u4EE4\uFF08PowerShell/bash\uFF0CWindows \u5F3A\u5236 UTF-8\uFF09"));
1441
1460
  console.log(tool("read_file", "\u8BFB\u53D6\u6587\u4EF6\u5185\u5BB9"));
@@ -1452,10 +1471,10 @@ var Renderer = class {
1452
1471
  console.log(tool("ask_user", "\u5411\u7528\u6237\u63D0\u95EE\u5E76\u7B49\u5F85\u56DE\u7B54\uFF08agentic \u5FAA\u73AF\u4E2D\u8BF7\u6C42\u6F84\u6E05\uFF09"));
1453
1472
  console.log(tool("write_todos", "\u62C6\u89E3\u4EFB\u52A1\u4E3A\u5B50\u4EFB\u52A1\u5217\u8868\uFF0C\u5B9E\u65F6\u663E\u793A\u8FDB\u5EA6"));
1454
1473
  console.log(HR);
1455
- console.log(chalk.gray(" REPL \u547D\u4EE4\uFF0816\u4E2A\uFF09\uFF1A"));
1474
+ console.log(chalk.gray(" REPL \u547D\u4EE4\uFF0817\u4E2A\uFF09\uFF1A"));
1456
1475
  console.log(chalk.dim(" /help /about /provider /model /clear /session"));
1457
1476
  console.log(chalk.dim(" /system /context /status /search /undo /export"));
1458
- console.log(chalk.dim(" /tools /plugins /config /exit"));
1477
+ console.log(chalk.dim(" /tools /plugins /mcp /config /exit"));
1459
1478
  console.log(HR);
1460
1479
  console.log(chalk.gray(" \u4E3B\u8981\u7279\u6027\uFF1A"));
1461
1480
  console.log(feat("Agentic \u5FAA\u73AF\uFF08\u6700\u591A 20 \u8F6E\u5DE5\u5177\u8C03\u7528\uFF0C\u6700\u7EC8\u56DE\u7B54\u6D41\u5F0F\u8F93\u51FA\uFF09"));
@@ -1466,6 +1485,7 @@ var Renderer = class {
1466
1485
  console.log(feat("\u6587\u4EF6\u64CD\u4F5C\u64A4\u9500\uFF08/undo\uFF0C\u652F\u6301 write_file / edit_file\uFF09"));
1467
1486
  console.log(feat("Thinking \u6A21\u5F0F\u6298\u53E0\uFF08<think> \u5757\u81EA\u52A8\u6298\u53E0\uFF0CGLM-5 \u7B49\uFF09"));
1468
1487
  console.log(feat("Token \u7528\u91CF\u8FFD\u8E2A\uFF08\u6BCF\u6B21\u56DE\u590D + session \u7D2F\u8BA1\uFF0C\u652F\u6301 Gemini/Claude/DeepSeek \u7B49\uFF09"));
1488
+ console.log(feat("MCP \u534F\u8BAE\u652F\u6301\uFF1A\u63A5\u5165\u5916\u90E8 MCP \u670D\u52A1\u5668\u5DE5\u5177\uFF08config.json mcpServers \u914D\u7F6E\uFF09"));
1469
1489
  console.log(feat("\u63D2\u4EF6\u7CFB\u7EDF\uFF1A~/.aicli/plugins/*.js \u81EA\u5B9A\u4E49\u5DE5\u5177\uFF08\u9700 allowPlugins:true \u542F\u7528\uFF0C\u9ED8\u8BA4\u5173\u95ED\uFF09"));
1470
1490
  console.log(feat("\u72EC\u7ACB\u53EF\u6267\u884C\u6587\u4EF6\u6253\u5305\uFF08~56MB\uFF0C\u65E0\u9700 Node.js \u73AF\u5883\uFF09"));
1471
1491
  console.log();
@@ -1725,6 +1745,7 @@ function createDefaultCommands() {
1725
1745
  " /export [md|json] [file] - Export session to file (default: auto-named .md)",
1726
1746
  " /tools - List all AI tools available",
1727
1747
  " /plugins - Show plugin directory and loaded plugins",
1748
+ " /mcp - Show MCP server connections and tools",
1728
1749
  " /config - Open configuration wizard (API keys, proxy, etc.)",
1729
1750
  " /exit - Exit"
1730
1751
  ] : [];
@@ -1738,7 +1759,9 @@ function createDefaultCommands() {
1738
1759
  description: "Show information about ai-cli and its author",
1739
1760
  usage: "/about",
1740
1761
  execute(_args, ctx) {
1741
- ctx.renderer.printAbout(ctx.tools.listPluginTools().length);
1762
+ const manager = ctx.getMcpManager();
1763
+ const mcpInfo = manager ? { servers: manager.getConnectedCount(), tools: manager.getTotalToolCount() } : void 0;
1764
+ ctx.renderer.printAbout(ctx.tools.listPluginTools().length, mcpInfo);
1742
1765
  }
1743
1766
  },
1744
1767
  {
@@ -2146,6 +2169,55 @@ ${text}
2146
2169
  console.log();
2147
2170
  }
2148
2171
  },
2172
+ {
2173
+ name: "mcp",
2174
+ description: "Show MCP server connections and tools",
2175
+ usage: "/mcp [reconnect]",
2176
+ execute(args, ctx) {
2177
+ const manager = ctx.getMcpManager();
2178
+ if (!manager) {
2179
+ console.log();
2180
+ console.log(chalk2.dim(" No MCP servers configured."));
2181
+ console.log(chalk2.dim(' Add "mcpServers" to ~/.aicli/config.json to connect MCP servers.'));
2182
+ console.log(chalk2.dim(" Example:"));
2183
+ console.log(chalk2.dim(' "mcpServers": {'));
2184
+ console.log(chalk2.dim(' "filesystem": {'));
2185
+ console.log(chalk2.dim(' "command": "npx",'));
2186
+ console.log(chalk2.dim(' "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path"]'));
2187
+ console.log(chalk2.dim(" }"));
2188
+ console.log(chalk2.dim(" }"));
2189
+ console.log();
2190
+ return;
2191
+ }
2192
+ const statuses = manager.getStatus();
2193
+ console.log();
2194
+ console.log(chalk2.bold(" \u{1F50C} MCP Servers:"));
2195
+ console.log();
2196
+ if (statuses.length === 0) {
2197
+ console.log(chalk2.dim(" No MCP servers configured."));
2198
+ console.log();
2199
+ return;
2200
+ }
2201
+ for (const s of statuses) {
2202
+ const statusBadge = s.connected ? chalk2.green("connected") : chalk2.red("disconnected");
2203
+ const toolCountStr = s.connected ? chalk2.dim(` \u2014 ${s.toolCount} tool(s)`) : "";
2204
+ const errorStr = s.error ? chalk2.red(`
2205
+ Error: ${s.error}`) : "";
2206
+ const serverLabel = s.serverName !== s.serverId ? ` (${s.serverName})` : "";
2207
+ console.log(` ${chalk2.cyan(s.serverId)}${chalk2.dim(serverLabel)} [${statusBadge}]${toolCountStr}${errorStr}`);
2208
+ if (s.connected) {
2209
+ const mcpTools = ctx.tools.listMcpTools().filter(
2210
+ (t) => t.definition.name.startsWith(`mcp__${s.serverId}__`)
2211
+ );
2212
+ for (const t of mcpTools) {
2213
+ const shortName = t.definition.name.replace(`mcp__${s.serverId}__`, "");
2214
+ console.log(chalk2.dim(` - ${shortName}`));
2215
+ }
2216
+ }
2217
+ }
2218
+ console.log();
2219
+ }
2220
+ },
2149
2221
  {
2150
2222
  name: "config",
2151
2223
  description: "Open configuration wizard (API keys, proxy, default provider)",
@@ -3798,6 +3870,7 @@ import { join as join7 } from "path";
3798
3870
  var ToolRegistry = class {
3799
3871
  tools = /* @__PURE__ */ new Map();
3800
3872
  pluginToolNames = /* @__PURE__ */ new Set();
3873
+ mcpToolNames = /* @__PURE__ */ new Set();
3801
3874
  constructor() {
3802
3875
  this.register(bashTool);
3803
3876
  this.register(readFileTool);
@@ -3831,6 +3904,22 @@ var ToolRegistry = class {
3831
3904
  listPluginTools() {
3832
3905
  return [...this.tools.values()].filter((t) => this.pluginToolNames.has(t.definition.name));
3833
3906
  }
3907
+ /** 注册一个 MCP 工具(名称以 mcp__ 开头) */
3908
+ registerMcpTool(tool) {
3909
+ this.tools.set(tool.definition.name, tool);
3910
+ this.mcpToolNames.add(tool.definition.name);
3911
+ }
3912
+ /** 返回所有 MCP 工具 */
3913
+ listMcpTools() {
3914
+ return [...this.tools.values()].filter((t) => this.mcpToolNames.has(t.definition.name));
3915
+ }
3916
+ /** 清除所有已注册的 MCP 工具(重连时先清除再重新注册) */
3917
+ unregisterMcpTools() {
3918
+ for (const name of this.mcpToolNames) {
3919
+ this.tools.delete(name);
3920
+ }
3921
+ this.mcpToolNames.clear();
3922
+ }
3834
3923
  /**
3835
3924
  * Dynamically loads .js plugin files from pluginsDir.
3836
3925
  *
@@ -3908,6 +3997,7 @@ import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
3908
3997
 
3909
3998
  // src/tools/types.ts
3910
3999
  function getDangerLevel(toolName, args) {
4000
+ if (toolName.startsWith("mcp__")) return "safe";
3911
4001
  if (toolName === "bash") {
3912
4002
  const cmd = String(args["command"] ?? "");
3913
4003
  if (/\brm\s+[^\n]*(?:-\w*[rRfF]\w*|--recursive|--force)\b/.test(cmd)) return "destructive";
@@ -4737,6 +4827,407 @@ function formatGitContextForPrompt(ctx) {
4737
4827
  return lines.join("\n");
4738
4828
  }
4739
4829
 
4830
+ // src/mcp/client.ts
4831
+ import { spawn as spawn2 } from "child_process";
4832
+ var McpClient = class {
4833
+ serverId;
4834
+ config;
4835
+ process = null;
4836
+ nextId = 1;
4837
+ connected = false;
4838
+ serverInfo = null;
4839
+ /** stderr 收集(最多保留最后 2KB,用于错误报告) */
4840
+ stderrBuffer = "";
4841
+ /** 缓存已发现的工具列表 */
4842
+ cachedTools = [];
4843
+ /** 错误信息(连接失败时设置) */
4844
+ errorMessage = null;
4845
+ // ── JSON-RPC 请求/响应匹配 ──────────────────────────────────────
4846
+ pendingRequests = /* @__PURE__ */ new Map();
4847
+ /** stdout 残余缓冲区(处理不完整的 JSON 行) */
4848
+ stdoutBuffer = "";
4849
+ constructor(serverId, config) {
4850
+ this.serverId = serverId;
4851
+ this.config = config;
4852
+ }
4853
+ get isConnected() {
4854
+ return this.connected;
4855
+ }
4856
+ get serverName() {
4857
+ return this.serverInfo?.name ?? this.serverId;
4858
+ }
4859
+ get tools() {
4860
+ return this.cachedTools;
4861
+ }
4862
+ // ══════════════════════════════════════════════════════════════════
4863
+ // 连接与初始化
4864
+ // ══════════════════════════════════════════════════════════════════
4865
+ async connect() {
4866
+ const timeout = this.config.timeout ?? MCP_CONNECT_TIMEOUT;
4867
+ try {
4868
+ this.process = spawn2(this.config.command, this.config.args ?? [], {
4869
+ stdio: ["pipe", "pipe", "pipe"],
4870
+ env: { ...process.env, ...this.config.env },
4871
+ // Windows 上 npx 等是 .cmd 脚本,需要 shell 模式
4872
+ shell: process.platform === "win32",
4873
+ // 不让子进程阻止父进程退出
4874
+ detached: false
4875
+ });
4876
+ this.process.on("error", (err) => {
4877
+ this.errorMessage = err.message;
4878
+ this.connected = false;
4879
+ this.rejectAllPending(new Error(`MCP server [${this.serverId}] process error: ${err.message}`));
4880
+ });
4881
+ this.process.on("exit", (code, signal) => {
4882
+ this.connected = false;
4883
+ const reason = signal ? `signal ${signal}` : `code ${code}`;
4884
+ this.rejectAllPending(new Error(`MCP server [${this.serverId}] exited: ${reason}`));
4885
+ });
4886
+ this.process.stdout.setEncoding("utf-8");
4887
+ this.process.stdout.on("data", (chunk) => this.handleStdoutData(chunk));
4888
+ this.process.stderr.setEncoding("utf-8");
4889
+ this.process.stderr.on("data", (chunk) => {
4890
+ this.stderrBuffer += chunk;
4891
+ if (this.stderrBuffer.length > 2048) {
4892
+ this.stderrBuffer = this.stderrBuffer.slice(-2048);
4893
+ }
4894
+ });
4895
+ const initResult = await this.withTimeout(
4896
+ this.sendRequest("initialize", {
4897
+ protocolVersion: MCP_PROTOCOL_VERSION,
4898
+ capabilities: {},
4899
+ clientInfo: { name: APP_NAME, version: VERSION }
4900
+ }),
4901
+ timeout,
4902
+ "initialize handshake"
4903
+ );
4904
+ this.serverInfo = initResult.serverInfo;
4905
+ this.sendNotification("notifications/initialized");
4906
+ this.connected = true;
4907
+ await this.refreshTools();
4908
+ } catch (err) {
4909
+ this.errorMessage = err instanceof Error ? err.message : String(err);
4910
+ this.connected = false;
4911
+ this.killProcess();
4912
+ throw err;
4913
+ }
4914
+ }
4915
+ // ══════════════════════════════════════════════════════════════════
4916
+ // 工具操作
4917
+ // ══════════════════════════════════════════════════════════════════
4918
+ /** 刷新工具列表(tools/list) */
4919
+ async refreshTools() {
4920
+ this.ensureConnected();
4921
+ const result = await this.withTimeout(
4922
+ this.sendRequest("tools/list", {}),
4923
+ MCP_CALL_TIMEOUT,
4924
+ "tools/list"
4925
+ );
4926
+ this.cachedTools = result.tools ?? [];
4927
+ return this.cachedTools;
4928
+ }
4929
+ /** 调用工具(tools/call) */
4930
+ async callTool(name, args) {
4931
+ this.ensureConnected();
4932
+ return this.withTimeout(
4933
+ this.sendRequest("tools/call", { name, arguments: args }),
4934
+ MCP_CALL_TIMEOUT,
4935
+ `tools/call(${name})`
4936
+ );
4937
+ }
4938
+ // ══════════════════════════════════════════════════════════════════
4939
+ // 关闭连接
4940
+ // ══════════════════════════════════════════════════════════════════
4941
+ async close() {
4942
+ this.connected = false;
4943
+ this.rejectAllPending(new Error("Client closing"));
4944
+ this.killProcess();
4945
+ }
4946
+ // ══════════════════════════════════════════════════════════════════
4947
+ // 内部方法:JSON-RPC 通信
4948
+ // ══════════════════════════════════════════════════════════════════
4949
+ sendRequest(method, params) {
4950
+ return new Promise((resolve5, reject) => {
4951
+ if (!this.process?.stdin?.writable) {
4952
+ return reject(new Error(`MCP server [${this.serverId}] stdin not writable`));
4953
+ }
4954
+ const id = this.nextId++;
4955
+ const request = {
4956
+ jsonrpc: "2.0",
4957
+ id,
4958
+ method,
4959
+ ...params !== void 0 ? { params } : {}
4960
+ };
4961
+ const timer = setTimeout(() => {
4962
+ this.pendingRequests.delete(id);
4963
+ reject(new Error(`MCP request [${method}] timed out (internal)`));
4964
+ }, MCP_CALL_TIMEOUT * 2);
4965
+ this.pendingRequests.set(id, {
4966
+ resolve: resolve5,
4967
+ reject,
4968
+ timer
4969
+ });
4970
+ const json = JSON.stringify(request) + "\n";
4971
+ this.process.stdin.write(json, (err) => {
4972
+ if (err) {
4973
+ this.pendingRequests.delete(id);
4974
+ clearTimeout(timer);
4975
+ reject(new Error(`MCP write error: ${err.message}`));
4976
+ }
4977
+ });
4978
+ });
4979
+ }
4980
+ sendNotification(method, params) {
4981
+ if (!this.process?.stdin?.writable) return;
4982
+ const notification = {
4983
+ jsonrpc: "2.0",
4984
+ method,
4985
+ ...params !== void 0 ? { params } : {}
4986
+ };
4987
+ this.process.stdin.write(JSON.stringify(notification) + "\n");
4988
+ }
4989
+ /**
4990
+ * 处理 stdout 数据:按行分割,每行解析为 JSON-RPC 响应。
4991
+ * 处理不完整行:残余数据保留在 stdoutBuffer 中。
4992
+ */
4993
+ handleStdoutData(chunk) {
4994
+ this.stdoutBuffer += chunk;
4995
+ const lines = this.stdoutBuffer.split("\n");
4996
+ this.stdoutBuffer = lines.pop() ?? "";
4997
+ for (const line of lines) {
4998
+ const trimmed = line.trim();
4999
+ if (!trimmed) continue;
5000
+ try {
5001
+ const msg = JSON.parse(trimmed);
5002
+ this.handleMessage(msg);
5003
+ } catch {
5004
+ }
5005
+ }
5006
+ }
5007
+ /** 处理收到的 JSON-RPC 消息 */
5008
+ handleMessage(msg) {
5009
+ if ("id" in msg && typeof msg.id === "number") {
5010
+ const pending = this.pendingRequests.get(msg.id);
5011
+ if (!pending) return;
5012
+ this.pendingRequests.delete(msg.id);
5013
+ clearTimeout(pending.timer);
5014
+ const response = msg;
5015
+ if (response.error) {
5016
+ pending.reject(new Error(
5017
+ `MCP error [${response.error.code}]: ${response.error.message}`
5018
+ ));
5019
+ } else {
5020
+ pending.resolve(response.result);
5021
+ }
5022
+ }
5023
+ }
5024
+ // ══════════════════════════════════════════════════════════════════
5025
+ // 辅助方法
5026
+ // ══════════════════════════════════════════════════════════════════
5027
+ ensureConnected() {
5028
+ if (!this.connected) {
5029
+ throw new Error(`MCP server [${this.serverId}] is not connected`);
5030
+ }
5031
+ }
5032
+ /** Promise 超时包装 */
5033
+ withTimeout(promise, ms, label) {
5034
+ return new Promise((resolve5, reject) => {
5035
+ const timer = setTimeout(() => {
5036
+ reject(new Error(`MCP [${this.serverId}] ${label} timed out after ${ms}ms`));
5037
+ }, ms);
5038
+ promise.then((val) => {
5039
+ clearTimeout(timer);
5040
+ resolve5(val);
5041
+ }).catch((err) => {
5042
+ clearTimeout(timer);
5043
+ reject(err);
5044
+ });
5045
+ });
5046
+ }
5047
+ /** 拒绝所有挂起的请求 */
5048
+ rejectAllPending(error) {
5049
+ for (const [id, pending] of this.pendingRequests) {
5050
+ clearTimeout(pending.timer);
5051
+ pending.reject(error);
5052
+ }
5053
+ this.pendingRequests.clear();
5054
+ }
5055
+ /** 杀掉子进程 */
5056
+ killProcess() {
5057
+ if (this.process) {
5058
+ try {
5059
+ this.process.stdin?.end();
5060
+ this.process.kill();
5061
+ } catch {
5062
+ }
5063
+ this.process = null;
5064
+ }
5065
+ }
5066
+ };
5067
+
5068
+ // src/mcp/manager.ts
5069
+ var McpManager = class {
5070
+ clients = /* @__PURE__ */ new Map();
5071
+ /**
5072
+ * 连接所有配置的 MCP 服务器(并发连接,单个失败不阻塞其他)。
5073
+ * 连接结果通过 getStatus() 查看。
5074
+ */
5075
+ async connectAll(servers) {
5076
+ const entries = Object.entries(servers);
5077
+ if (entries.length === 0) return;
5078
+ const promises = entries.map(async ([serverId, config]) => {
5079
+ const client = new McpClient(serverId, config);
5080
+ this.clients.set(serverId, client);
5081
+ try {
5082
+ await client.connect();
5083
+ process.stderr.write(`[mcp] \u2713 ${serverId}: connected (${client.serverName}, ${client.tools.length} tools)
5084
+ `);
5085
+ } catch (err) {
5086
+ const msg = err instanceof Error ? err.message : String(err);
5087
+ process.stderr.write(`[mcp] \u2717 ${serverId}: ${msg}
5088
+ `);
5089
+ }
5090
+ });
5091
+ await Promise.allSettled(promises);
5092
+ }
5093
+ /**
5094
+ * 获取所有已连接服务器的工具,转换为 ai-cli 的 Tool 接口。
5095
+ * 工具名格式:mcp__<serverId>__<toolName>
5096
+ */
5097
+ getAllTools() {
5098
+ const tools = [];
5099
+ for (const [serverId, client] of this.clients) {
5100
+ if (!client.isConnected) continue;
5101
+ for (const mcpTool of client.tools) {
5102
+ tools.push(this.wrapMcpTool(serverId, client, mcpTool));
5103
+ }
5104
+ }
5105
+ return tools;
5106
+ }
5107
+ /**
5108
+ * 获取连接状态摘要。
5109
+ */
5110
+ getStatus() {
5111
+ const statuses = [];
5112
+ for (const [serverId, client] of this.clients) {
5113
+ statuses.push({
5114
+ serverId,
5115
+ serverName: client.serverName,
5116
+ toolCount: client.tools.length,
5117
+ connected: client.isConnected,
5118
+ error: client.errorMessage ?? void 0
5119
+ });
5120
+ }
5121
+ return statuses;
5122
+ }
5123
+ /**
5124
+ * 获取已连接服务器数量。
5125
+ */
5126
+ getConnectedCount() {
5127
+ let count = 0;
5128
+ for (const client of this.clients.values()) {
5129
+ if (client.isConnected) count++;
5130
+ }
5131
+ return count;
5132
+ }
5133
+ /**
5134
+ * 获取所有 MCP 工具的总数。
5135
+ */
5136
+ getTotalToolCount() {
5137
+ let count = 0;
5138
+ for (const client of this.clients.values()) {
5139
+ if (client.isConnected) count += client.tools.length;
5140
+ }
5141
+ return count;
5142
+ }
5143
+ /**
5144
+ * 关闭所有 MCP 服务器连接。
5145
+ */
5146
+ async closeAll() {
5147
+ const promises = [...this.clients.values()].map((c) => c.close().catch(() => {
5148
+ }));
5149
+ await Promise.allSettled(promises);
5150
+ this.clients.clear();
5151
+ }
5152
+ // ══════════════════════════════════════════════════════════════════
5153
+ // 内部方法
5154
+ // ══════════════════════════════════════════════════════════════════
5155
+ /**
5156
+ * 将 MCP 工具包装为 ai-cli 的 Tool 接口。
5157
+ * execute() 方法委托给对应 McpClient.callTool()。
5158
+ */
5159
+ wrapMcpTool(serverId, client, mcpTool) {
5160
+ const toolName = `${MCP_TOOL_PREFIX}${serverId}__${mcpTool.name}`;
5161
+ const definition = this.convertDefinition(toolName, serverId, mcpTool);
5162
+ return {
5163
+ definition,
5164
+ execute: async (args) => {
5165
+ try {
5166
+ const result = await client.callTool(mcpTool.name, args);
5167
+ if (result.isError) {
5168
+ const errorText = this.extractText(result.content);
5169
+ throw new Error(errorText || "MCP tool returned error with no message");
5170
+ }
5171
+ return this.extractText(result.content) || "(no output)";
5172
+ } catch (err) {
5173
+ throw new Error(`MCP [${serverId}/${mcpTool.name}]: ${err instanceof Error ? err.message : String(err)}`);
5174
+ }
5175
+ }
5176
+ };
5177
+ }
5178
+ /**
5179
+ * 转换 MCP 工具定义为 ai-cli 的 ToolDefinition。
5180
+ * MCP 使用 JSON Schema 的 inputSchema,ai-cli 使用扁平的 Record<string, ToolParameterSchema>。
5181
+ */
5182
+ convertDefinition(toolName, serverId, mcpTool) {
5183
+ const parameters = {};
5184
+ const props = mcpTool.inputSchema?.properties ?? {};
5185
+ const required = new Set(mcpTool.inputSchema?.required ?? []);
5186
+ for (const [key, schema] of Object.entries(props)) {
5187
+ parameters[key] = {
5188
+ type: this.normalizeType(schema.type),
5189
+ description: schema.description ?? "",
5190
+ ...schema.enum ? { enum: schema.enum } : {},
5191
+ ...schema.items ? { items: { type: this.normalizeType(schema.items.type) } } : {},
5192
+ ...required.has(key) ? { required: true } : {}
5193
+ };
5194
+ }
5195
+ return {
5196
+ name: toolName,
5197
+ description: (mcpTool.description ?? mcpTool.name) + ` [MCP: ${serverId}]`,
5198
+ parameters
5199
+ };
5200
+ }
5201
+ /**
5202
+ * 将 JSON Schema 类型映射到 ai-cli 的 ToolParameterType。
5203
+ * integer → number(ai-cli 不区分整数/浮点)
5204
+ */
5205
+ normalizeType(jsonSchemaType) {
5206
+ switch (jsonSchemaType) {
5207
+ case "string":
5208
+ return "string";
5209
+ case "number":
5210
+ case "integer":
5211
+ return "number";
5212
+ case "boolean":
5213
+ return "boolean";
5214
+ case "array":
5215
+ return "array";
5216
+ case "object":
5217
+ return "object";
5218
+ default:
5219
+ return "string";
5220
+ }
5221
+ }
5222
+ /**
5223
+ * 从 MCP 响应的 content blocks 中提取纯文本。
5224
+ * 只处理 type="text" 的块,其他类型(image 等)跳过。
5225
+ */
5226
+ extractText(content) {
5227
+ return content.filter((block) => block.type === "text" && block.text).map((block) => block.text).join("\n");
5228
+ }
5229
+ };
5230
+
4740
5231
  // src/repl/repl.ts
4741
5232
  var IMAGE_MIME = {
4742
5233
  ".png": "image/png",
@@ -4832,6 +5323,8 @@ var Repl = class {
4832
5323
  sessionTokenUsage = { inputTokens: 0, outputTokens: 0 };
4833
5324
  /** 启动时检测到的 Git 分支(无 git 仓库时为 null) */
4834
5325
  gitBranch = null;
5326
+ /** MCP 多服务器管理器(无 MCP 配置时为 null) */
5327
+ mcpManager = null;
4835
5328
  /**
4836
5329
  * 交互式列表选择器进行中标志。
4837
5330
  * 与 toolExecutor.confirming 类似:主循环 line handler 在此为 true 时忽略 line 事件,
@@ -5107,6 +5600,23 @@ ${memory.content}`);
5107
5600
  process.stdout.write(chalk9.dim(` \u{1F50C} Plugins loaded: ${pluginCount} tool(s) from plugins/
5108
5601
  `));
5109
5602
  }
5603
+ const mcpServers = this.config.get("mcpServers");
5604
+ if (mcpServers && Object.keys(mcpServers).length > 0) {
5605
+ this.mcpManager = new McpManager();
5606
+ await this.mcpManager.connectAll(mcpServers);
5607
+ const mcpTools = this.mcpManager.getAllTools();
5608
+ for (const tool of mcpTools) {
5609
+ this.toolRegistry.registerMcpTool(tool);
5610
+ }
5611
+ const connectedCount = this.mcpManager.getConnectedCount();
5612
+ const totalTools = this.mcpManager.getTotalToolCount();
5613
+ if (connectedCount > 0) {
5614
+ process.stdout.write(
5615
+ chalk9.dim(` \u{1F50C} MCP: ${connectedCount} server(s), ${totalTools} tool(s)
5616
+ `)
5617
+ );
5618
+ }
5619
+ }
5110
5620
  this.rl.on("SIGINT", () => {
5111
5621
  if (this.toolExecutor.confirming) {
5112
5622
  this.toolExecutor.cancelConfirm();
@@ -5536,6 +6046,7 @@ ${memory.content}`);
5536
6046
  },
5537
6047
  generateDevStateSnapshot: () => this.generateDevStateSnapshot(),
5538
6048
  clearDevState: () => clearDevState(),
6049
+ getMcpManager: () => this.mcpManager,
5539
6050
  exit: () => this.handleExit()
5540
6051
  };
5541
6052
  await cmd.execute(args, ctx);
@@ -5546,6 +6057,8 @@ ${memory.content}`);
5546
6057
  if (sessionId) {
5547
6058
  this.events.emit("session.end", { sessionId });
5548
6059
  }
6060
+ this.mcpManager?.closeAll().catch(() => {
6061
+ });
5549
6062
  this.rl.close();
5550
6063
  console.log(chalk9.gray("\nGoodbye!"));
5551
6064
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jinzd-ai-cli",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "Cross-platform REPL-style AI CLI with multi-provider support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",