使用 Turborepo 构建多 MCP Server Monorepo:以 with-mcp-servers 为例的架构与实战

发布时间:2026/11/10 11:44:49

使用 Turborepo 构建多 MCP Server Monorepo:以 with-mcp-servers 为例的架构与实战 使用 Turborepo 构建多 MCP Server Monorepo以 with-mcp-servers 为例的架构与实战【免费下载链接】turboBuild system optimized for JavaScript and TypeScript, written in Rust项目地址: https://gitcode.com/gh_mirrors/tu/turbo本文围绕 Turborepo 官方示例with-mcp-servers讲解如何在一个 pnpm monorepo 中把多个 Model Context Protocol (MCP) 服务器组织成相互隔离的 workspace 包并借助 Turbo 的^build依赖让编译顺序正确、演示客户端按需连接。读完本文你将掌握 MCP Server 的createServer()工厂 cli可执行入口双导出结构、stdio 传输的客户端接入方式、基于 zod 的运行时参数校验以及针对不可信模型输入的路径穿越防护与集成测试写法。示例概览仓库里到底有什么with-mcp-servers是一个社区维护的 Turborepo 示例位于仓库的 examples/with-mcp-servers 目录其元信息meta.json描述为 Turborepo monorepo with multiple MCP tool servers as isolated workspace packages。整个 monorepo 包含Appsmcp-client一个 Node.js CLI通过 stdio 传输同时连接两个 MCP 服务器并运行若干演示用工具调用。Packagesrepo/mcp-calculator一个 MCP 服务器暴露四个算术工具 ——add、subtract、multiply、divide。repo/mcp-file-reader一个 MCP 服务器暴露两个只读文件系统工具 ——read_file、list_directory。它以允许的根目录作为第一个 CLI 参数拒绝任何逃逸出该目录的路径包括通过符号链接逃逸因为 MCP 的工具参数属于不可信输入。repo/eslint-config共享 ESLint 配置包含eslint与typescript-eslint。repo/typescript-config贯穿整个 monorepo 的共享tsconfig.json基座。工作区声明在 examples/with-mcp-servers/pnpm-workspace.yaml采用apps/*与packages/*的经典布局。根目录 package.json 指定packageManager: pnpm12.4.1并要求node 24四个关键脚本都通过turbo run转发。每个 MCP 服务器都是独立的 Node.js ESM 包用tsc构建使用官方modelcontextprotocol/sdk的 stdio 传输。每个服务器包都暴露一个无副作用的createServer()工厂供测试使用和一个独立的可执行入口供客户端使用通过包的exports字段发布。从零开始使用此示例运行下面的命令即可创建项目npx create-turbolatest --example with-mcp-servers生成后的目录结构与仓库中 examples/with-mcp-servers 完全一致with-mcp-servers/ ├── apps/ │ └── mcp-client/ # Node.js CLIstdio 连接两个 MCP Server ├── packages/ │ ├── eslint-config/ # 共享 ESLint 配置 │ ├── mcp-calculator/ # 算术工具 MCP Serveradd/subtract/multiply/divide │ ├── mcp-file-reader/ # 只读文件系统 MCP Serverread_file/list_directory │ └── typescript-config/ # 共享 tsconfig 基座 ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── turbo.json核心设计双导出exports与无副作用工厂每个 MCP 服务器包都遵循同样的包结构以repo/mcp-calculator的 package.json 为例{ name: repo/mcp-calculator, version: 1.0.0, private: true, type: module, exports: { .: ./dist/server.js, ./cli: ./dist/index.js }, scripts: { build: tsc, dev: tsc --watch, lint: eslint src/, check-types: tsc --noEmit, test: node --test dist/server.test.js }, dependencies: { modelcontextprotocol/sdk: 1.30.0, zod: 4.6.2 }, devDependencies: { repo/eslint-config: workspace:*, repo/typescript-config: workspace:*, types/node: 22.20.2, eslint: 10.10.0, typescript: 7.0.2 } }关键点exports字段区分两种消费方式.库入口指向dist/server.js即无副作用的createServer()工厂与./cli可执行入口指向dist/index.js带#!/usr/bin/env nodeshebang通过StdioServerTransport启动服务。客户端通过./cli子路径解析可执行文件测试则直接导入库入口。repo/typescript-config与repo/eslint-config以workspace:*引用这正是 pnpm workspace 的本地依赖写法。两个服务器包依赖相同的modelcontextprotocol/sdk1.30.0与zod4.6.2版本由 lockfile 统一锁定。从源码结构看这种库入口无副作用、CLI 入口负责接线的拆分见 calculator 的 index.ts 与 file-reader 的 index.ts让同一份服务器代码既能被测试进程内加载也能被客户端作为子进程启动避免重复实现。构建用^build保证依赖顺序整个 monorepo 的构建由 turbo.json 驱动{ $schema: https://turborepo.dev/schema.json, ui: tui, tasks: { build: { dependsOn: [^build], outputs: [dist/**] }, dev: { cache: false, persistent: true }, lint: { dependsOn: [^lint] }, check-types: { dependsOn: [^check-types] }, test: { dependsOn: [build] }, start: { dependsOn: [build], cache: false } } }构建全部包pnpm buildbuild任务中的dependsOn: [^build]是 Turbo 的拓扑依赖语法^表示所有依赖项的任务。它保证repo/mcp-calculator、repo/mcp-file-reader先于mcp-client编译因为客户端依赖它们。outputs: [dist/**]声明了产物目录Turbo 会据此做任务缓存——依赖未变时下游包直接从缓存恢复无需重新编译。test与start任务都dependsOn: [build]意味着运行测试或启动客户端前会先补齐过期构建start还设置了cache: false避免被缓存命中后不真正执行。运行演示客户端如何拉起两个服务器pnpm startstart通过 Turbo 先构建任何过期内容再启动客户端。客户端的核心逻辑在 apps/mcp-client/src/index.ts使用createRequirerequire.resolve(repo/mcp-calculator/cli)解析服务器包的编译产物路径。注释明确说明通过包管理器解析而非硬编码相对路径可以保证无论包安装在哪里都能工作而 Turbo 的^build依赖保证编译产物一定存在。通过StdioClientTransport以当前 Node 进程process.execPath作为命令把解析出的 CLI 文件作为参数将每个服务器作为子进程拉起。文件读取服务器额外传入exampleRoot示例根目录作为第一个参数作为它允许读取的根目录。串行连接两个服务器逐个连接便于清理生产宿主通常会并发连接执行add、multiply、list_directory三次工具调用并打印结果。在finally中调用client.close()关闭所有已连接客户端确保派生的服务器子进程退出——即使连接或工具调用失败也会执行清理。connectServer的实现展示了 MCP 客户端与 stdio 服务器对接的最小范式const transport new StdioClientTransport({ command: process.execPath, args: [require.resolve(cliSpecifier), ...args], }); const client new Client( { name: mcp-client, version: 1.0.0 }, { capabilities: {} }, ); await client.connect(transport);测试基于内存传输的集成测试两个服务器包都带有集成测试通过 MCP 的内存传输InMemoryTransport在单进程内端到端演练工具pnpm test以 calculator 的 server.test.ts 为例测试先InMemoryTransport.createLinkedPair()创建一对配对传输然后并行完成createServer().connect(serverTransport)与client.connect(clientTransport)即可像真实客户端一样调用工具。它验证了五类行为四个算术运算的返回值正确538、5-32、6×742、10÷42.5除零返回isError: true的结果而非抛出异常非数字参数如字符串5、3返回Invalid arguments错误——因为 SDK 会按注册的 zod schema 在运行时校验参数handler 永远不会看到坏输入不会发生字符串拼接或缺失参数产生 NaN缺失参数同样返回Invalid arguments未知工具包括toString这类继承属性名返回isError。pnpm testfile-reader 的 server.test.ts 则聚焦安全边界稍后详述。安全实践把 MCP 工具参数当作不可信输入MCP 工具的参数来自模型LLM本质上是不可信输入。file-reader 服务器把这一原则贯彻到了实现中见 packages/mcp-file-reader/src/server.ts。根目录逃逸防护含符号链接createServer(rootDir)首先resolve(rootDir)得到规范根目录然后所有路径都经过resolveWithinRoot校验const realRoot await realpath(root); const resolved await realpath(resolve(realRoot, path)); const relativePath relative(realRoot, resolved); if (relativePath.startsWith(..) || isAbsolute(relativePath)) { throw new Error(Path is outside the allowed root: ${path}); } return resolved;这段代码的巧妙之处在于用realpath同时解析根目录与目标路径的符号链接因此根目录内部的符号链接不可能把读取指向根目录之外。随后用relative计算相对路径若以..开头或是绝对路径则判定为逃逸。测试用例明确覆盖了三种逃逸方式相对路径穿越../secret.txt被拒根目录外的绝对路径被拒根目录内的符号链接escape.txt指向外部secret.txt被拒——测试在 fixture 中预先创建了这个恶意符号链接。资源上限与错误信息脱敏文件大小上限MAX_FILE_SIZE_BYTES 1024 * 10241MBread_file先stat再读超限返回File is too large to read的isError结果避免超大文件撑爆工具响应。目录条目上限MAX_DIRECTORY_ENTRIES 1000list_directory对条目切片并返回truncated布尔标记防止巨型目录产生巨型响应。错误信息脱敏describeError用调用方提供的path代替原始错误信息——原始错误消息包含宿主机解析后的绝对路径直接透传会泄漏主机目录结构。测试断言缺失文件返回ENOENT: missing.txt而非真实绝对路径。isError语义让模型看得到失败原因两个服务器的工具实现都遵循同一约定执行失败返回isError: true的结果而不是抛出异常。代码注释给出了理由抛出的异常会变成不透明的 JSON-RPC 协议错误而isError结果对模型可见模型可以据此理解发生了什么并做出反应。divide的除零分支正是典型示范if (b 0) { return { content: [{ type: text, text: Division by zero }], isError: true, }; }开发循环并行监听与热重编译pnpm devdev任务在turbo.json中声明为cache: false, persistent: true会在每个包中启动tsc --watch改动即重新编译但不会运行任何东西。之后在另一个终端重新执行pnpm start即可验证你的改动。由于start依赖build它总是先补齐增量编译产物再启动客户端形成改代码 → dev 重编译 → 再 start 验证的快速迭代闭环。从示例到生产可借鉴的要点基于该示例的源码结构可以总结出几条直接可复用的设计经验每个 MCP 服务器一个独立 workspace 包用exports区分离线测试用的库入口无副作用createServer()与客户端用的./cli可执行入口。用 Turbo 的^build拓扑依赖表达先编服务器、再编客户端的顺序outputs: [dist/**]让未变化的包走缓存。为每个工具注册 zod schema由 SDK 在运行时校验参数handler 内永远拿到类型正确的值。所有工具失败一律返回isError结果让模型可见可响应而不是抛出协议级错误。安全边界三件套根目录realpath规范化 相对路径逃逸检测、资源大小/数量上限、错误信息脱敏不泄漏绝对路径。用InMemoryTransport写单进程集成测试无需拉起真实子进程即可覆盖正常、异常与攻击路径。相关链接MCP TypeScript SDKTurborepo docsRemote Caching【免费下载链接】turboBuild system optimized for JavaScript and TypeScript, written in Rust项目地址: https://gitcode.com/gh_mirrors/tu/turbo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
延伸阅读

更多相关文章

2026/11/10 11:44:49

中文字体子集化实战:从3.2MB到200KB的压缩指南

上个月给个人博客换一套中文字体时,正正经经被 3.2MB 的字体文件卡了一次。首屏加载从原先一秒出头直接飙到四五秒,移动端更是肉眼可见的白屏转圈。研究了一圈解决方案,最后用 fontTools 做了字体子集化,把整套字体从 3.2MB 压到 …

2026/11/10 11:55:45

智慧校园一卡通系统架构:协议层、事件总线与GraphQL聚合

简介:本资源是一份面向高校信息化建设者、智慧校园项目实施方及物联网系统集成商的全场景一卡通解决方案PPT,聚焦数字迎新与智能控水两大核心子系统,解决迎新流程低效、水资源粗放管理等实际痛点。文件为单个37.93MB的PPTX演示文稿&#xff0…

2026/11/10 15:14:52

鸡西哪里可以办电工证报考条件与考试时间值不值得考

鸡西哪里可以办电工证报考条件与考试时间值不值得考 你是不是正被“鸡西哪里可以办电工证”这个问题折磨得头大?怕找错地方,怕被中间人套路,怕花钱还办不下来证?这种心情我太懂了,我自己在应急行业摸爬滚打了这么多年,也见过太多人因为信息不对称被坑。…

2026/11/10 15:14:52

滨州电工证如何办理?官方报名入口在这,别再让证书过期了

滨州电工证如何办理?官方报名入口在这,别再让证书过期了 证书过期了不知道怎么复审延期?你不是一个人,很多特种作业人员都遇到过这种问题。尤其是像滨州地区的电工证,一旦过了有效期,不仅影响工作,还可能面临法律风险。那现在问题来了:…

2026/11/10 15:14:52

北京电工证复审简单内部消息别错过

北京电工证复审简单内部消息别错过 怕考不过白交培训费?北京电工证复审简单,但你真的了解内部消息吗?最近不少小伙伴跑来问我,电工证复审流程到底有多复杂,能不能走捷径。说实话,这些担心我都懂,尤其是你已经交了培训费,还怕费了钱没结果。今天就来聊…

2026/11/10 15:14:52

电工爸爸的手工作业考前押题怎么搞定?外省证转过来的实操经验

电工爸爸的手工作业考前押题怎么搞定?外省证转过来的实操经验 之前考的证在外省能不能转过来?这是很多电工爸爸最关心的问题,特别是那些在外地工作多年、想回老家继续干手工作业的朋友们。我之前就帮一位焦作的老电工处理过这个事,他的电工证是在江苏考的…

2026/11/10 15:14:52

成都办理电工证在哪考?别再被中介坑了,看这篇就够了

成都办理电工证在哪考?别再被中介坑了,看这篇就够了 你是不是也这样, 不知道去哪报名怕被中介坑 ?在网上一搜“成都办理电工证在哪考”,各种培训机构、中介、代办点信息混杂,真假难辨。其实, 电工证是国家统一颁发的特种作业操作证…

2026/11/10 15:09:52

电工证现实照片怎么弄 上岗必须要这个证吗 全国通用怎么操作

电工证现实照片怎么弄 上岗必须要这个证吗 全国通用怎么操作 怕考不过白交培训费?电工证现实照片怎么弄,直接关系到你能不能顺利报名、考试、上岗,尤其是全国通用的电工证,一旦操作不当,可能连报名资格都没了。今天咱们就从郑州金水区应届工程类毕业生…

2026/11/10 8:12:38

电工证丢失还能补吗?千万别踩坑,丢了证等于失业

电工证丢失还能补吗?千万别踩坑,丢了证等于失业 你正在找工作,但手里的电工证突然丢了,这下怎么办?别急,先别慌, 电工证丢失还能补吗 ?答案是肯定的,但 千万别踩坑 ,补办流程如果不了解,可能会耽误你上岗、甚至影响到你的收入。…

2026/11/10 0:01:28

学历不高怕报不上名?考电工证报名费+网上查询全攻略来了

学历不高怕报不上名?考电工证报名费+网上查询全攻略来了 你是不是也担心自己学历不高,考电工证报名费太贵,根本报不上名?别急,今天就来给你讲讲,怎么用最少的钱,把证拿到手,还能在工地上多挣点钱。 电工证报名费多少钱?网上查询是关键…

2026/11/10 0:01:29

绍兴考电工证培训机构怎么选?2026河南最新政策别错过

绍兴考电工证培训机构怎么选?2026河南最新政策别错过 怕考不过白交培训费?你不是一个人。2026河南最新政策出台,对电工证考试、报名、培训、复审都有明确规定。选择绍兴考电工证培训机构,别再踩坑,下面一步步教你避坑。…

2026/11/9 6:30:27

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/10 10:29:37

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑