面试官:如何用 Bun + JS 实现安全的文件 MCP 工具集

40岁的中年程序员面试 AI Agent 开发:第一篇,用 Bun 实现安全的文件 MCP 工具集

面试官:为什么用 Bun 不用 Node.js?

:Bun 启动比 Node.js 快 5 倍,冷启动从 300ms 降到 60ms,文件读写性能高出 30%-50%。写工具类代码也比 Node.js 少一半样板代码。


面试官 :那 AI 编程助手要读、写、改项目里的文件,但绝对不能让它删 C:\Windows 或读 ~/.ssh/id_rsa。你怎么设计这套工具集?

:核心思路是嵌入式 MCP 工具集------不走 stdio 通信,就是一个普通 TypeScript 模块,函数调用直接执行。7 个工具,不到 300 行代码,覆盖 90% 的文件操作场景。


面试官追问:MCP 是什么?

:MCP 全称 Model Context Protocol ,Anthropic 在 2024 年推的,核心思想是把工具调用标准化

它定义了一个 JSON Schema 描述工具能力:

bash 复制代码
{
  "name": "read_file",
  "description": "读取文件内容",
  "inputSchema": {
    "type": "object",
    "properties": {
      "path": { "type": "string", "description": "文件路径" }
    },
    "required": ["path"]
  }
}

面试官追问:为什么不用现成的 MCP server?

:三个原因:

所以我的策略是:自己写一个嵌入式 MCP 工具集,注册到 AI 网关里,不走 stdio / socket,就是一个普通 TypeScript 模块。


面试官追问:安全怎么保证?

:这是整个工具集最关键的一处。做法是维护一个全局白名单根目录,所有路径必须落在它下面

bash 复制代码
let allowedRoot = "";

export function setAllowedRoot(path: string) {
  allowedRoot = path;
}

function resolvePath(inputPath: string): string {
  const resolved = inputPath.startsWith("/") || inputPath.match(/^[a-zA-Z]:\\/)
    ? inputPath
    : join(allowedRoot, inputPath);

  if (allowedRoot && !resolved.startsWith(allowedRoot)) {
    throw new Error(`权限拒绝: 路径 ${resolved} 不在项目目录 ${allowedRoot} 内`);
  }
  return resolved;
}

逻辑两步:

allowedRoot 什么时候设置?用户在前端"打开项目"时调用 setAllowedRoot。之后 AI 调任何工具,路径都被这条线拦着。

这个校验不是完美的,比如符号链接、..\ 这种边界情况,但对个人 IDE 来说够用。


面试官追问:图片文件怎么处理?

read_file 需要同时支持文本和图片。图片转 base64 返回,多模态模型可以直接识别。

bash 复制代码
read_file: async (args) => {
  const path = resolvePath(args.path as string);
  const mime = getImageMime(path);
  if (mime) {
    const buf = await readFile(path);
    const b64 = buf.toString("base64");
    return { content: [{ type: "text", text: `data:${mime};base64,${b64}` }] };
  }
  const content = await readFile(path, "utf-8");
  return { content: [{ type: "text", text: content }] };
}

getImageMime 是后缀名查表:

bash 复制代码
const IMAGE_MIME: Record<string, string> = {
  png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg",
  gif: "image/gif", webp: "image/webp", bmp: "image/bmp",
  ico: "image/x-icon", svg: "image/svg+xml", avif: "image/avif",
};

返回 data:image/png;base64,iVBORw0KG... 这种格式,OpenAI / Anthropic 的多模态接口都认。


面试官追问:write_file 和 edit_file 有什么区别?

:两个工具看起来都是"写文件",但职责完全不同,互相不替代

bash 复制代码
write_file: async (args) => {
  const path = resolvePath(args.path as string);
  await mkdir(dirname(path), { recursive: true });
  const exists = await stat(path).catch(() => null);
  if (exists) throw new Error(`文件已存在: ${path}。如需修改请用 edit_file 工具。`);
  await writeFile(path, args.content as string, "utf-8");
  return { content: [{ type: "text", text: `文件创建成功: ${path}` }] };
},

edit_file: async (args) => {
  const path = resolvePath(args.path as string);
  const exists = await stat(path).catch(() => null);
  if (!exists) throw new Error(`文件不存在: ${path}。如需新建请用 write_file 工具。`);
  const original = await readFile(path, "utf-8");
  await writeFile(path, args.content as string, "utf-8");
  return {
    content: [{
      type: "text",
      text: `文件编辑成功: ${path}\n原大小: ${original.length} 字符\n新大小: ${(args.content as string).length} 字符`
    }],
  };
}

设计意图

  • write_file 已存在就报错 → 防止 AI 误覆盖已有文件

  • edit_file 不存在也报错 → 防止 AI 写到一个错误路径

  • 错误信息里直接告诉 AI 应该用哪个工具,AI 不用猜,直接重试

write_file 自动 mkdir -p 中间目录({ recursive: true }),AI 创建新文件时经常给一个深层路径,父目录不存在就报错,这个细节很关键。


面试官追问:搜索代码怎么实现的?

:不自己实现搜索,直接调 ripgrep。Bun.spawn 启动子进程,比 Node.js 简洁很多。

bash 复制代码
search_code: async (args) => {
  const root = args.path ? resolvePath(args.path as string) : allowedRoot;
  const keyword = args.keyword as string;
  const results: { file: string; line: number; content: string }[] = [];
  try {
    const proc = Bun.spawn(["rg", "--line-number", "--no-heading", "--with-filename", keyword, "./"], {
      cwd: root,
      stdout: "pipe",
      stderr: "ignore",
    });
    const text = await new Response(proc.stdout).text();
    for (const line of text.split("\n")) {
      if (!line.trim()) continue;
      const idx1 = line.indexOf(":");
      if (idx1 === -1) continue;
      const file = line.slice(0, idx1);
      const rest = line.slice(idx1 + 1);
      const idx2 = rest.indexOf(":");
      if (idx2 === -1) continue;
      const lineNum = Number(rest.slice(0, idx2));
      const content = rest.slice(idx2 + 1);
      results.push({ file: relative(root, file), line: lineNum, content });
    }
  } catch { }
  return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
}

为什么用 ripgrep?

  • • 极快(Rust 写的,比 grep/ack/ag 都快)

  • • 默认忽略 .gitignore、二进制文件、隐藏文件

  • • 输出格式稳定:file:line:content,非常好解析

Bun.spawnstdout: "pipe" 拿到 stdout 之后,直接 new Response(proc.stdout).text() 把整个流读完。这个语法只在 Bun 里有,用 Node API 还得包 readable 流。


面试官追问:工具怎么注册到 AI 网关?

:需要转换成 OpenAI tools 协议的格式,AI 网关才能识别。

bash 复制代码
export function toOpenAiTools(): unknown[] {
  return FILE_TOOLS.map((t) => ({
    type: "function",
    function: {
      name: t.name,
      description: t.description,
      parameters: { ...t.inputSchema },
    },
  }));
}

然后在 AI 网关里:

bash 复制代码
const tools = toOpenAiTools();
const body = JSON.stringify({ model, messages, tools, stream: false, tool_choice: "auto" });

AI 收到 tools 列表,就能决定调哪个、传什么参数。

工具执行入口也很简单:

bash 复制代码
export async function executeToolCall(name: string, args: Record<string, unknown>): Promise<string> {
  const tool = getTool(name);
  if (!tool) return `错误: 未知工具 "${name}"`;
  try {
    const result = await tool.handler(args);
    return result.content.map((c) => c.text).join("\n");
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    return `工具执行错误: ${msg}`;
  }
}

设计原则:


面试官追问:为什么不做通用 Shell 执行工具?

:这个问题我认真考虑过。给 AI 一个 run_command 工具,什么都能干,但那是 RCE 级别的风险。任何 AI 输出的 shell 命令,我都得人工 review 才能执行,反而失去了"自动化"的意义。

所以我只暴露"项目内文件操作",AI 想跑命令,得让用户自己在终端跑。这是 IDE 的责任边界,不能模糊。

其他几个设计决策

  • 为什么不用第三方 MCP SDK? 有现成的 @modelcontextprotocol/sdk,但它是给独立 MCP server 用的,走 stdio/websocket。我这里是嵌入式调用,同一个进程内直接调函数,强行用 SDK 反而要套一层客户端-服务端,复杂又没收益。

  • 工具集要不要更通用? 我考虑过加 git_committerminal_runhttp_request 等,结论是先不加。工具越多,AI 选错的概率越大;工具越具体,AI 调用越准确。


面试官:总结一下,文件 MCP 工具集的核心设计是什么?

:三句话概括:

整套工具集 7 个工具,不到 300 行代码,零外部依赖,跑在 Bun 里性能不是问题。MCP 协议本身很简单,难的是想清楚"哪些操作该暴露、哪些不该,错误怎么处理,安全怎么保证"。

相关推荐
小Ti客栈1 小时前
Spring Boot 整合 Swagger2 和 Knife4j实现接口文档与可视化调试
java·spring boot·后端
明月_清风1 小时前
💰 DeFi 入门完全指南:从 Uniswap 到 Aave,一文读懂去中心化金融
后端·web3
Conan在掘金1 小时前
鸿蒙报错速查:struct 里嵌套 @Component struct 就炸,Unexpected keyword 编译报错,根因 + 真解法
后端
探索前端2 小时前
Cesium图层加载及影像服务添加
前端·cesium
妙码生花2 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(三十六):多驱动上传接口
后端·go·ai编程
明月_清风2 小时前
🎨 NFT 全景解析:从 JPEG 到数字所有权革命
后端·web3
阿懂在掘金2 小时前
Vue 弹窗新范式——代码减少、复用翻倍与 AI 时代的前端基建
前端·设计模式·前端框架
千纸鹤安安2 小时前
Compose Runtime → KuiklyUI:Applier 搭起的渲染桥梁
前端
李明卫杭州2 小时前
mise 管理工具详解
前端·后端