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

发布时间:2026/9/19 2:58:21

使用 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/9/19 2:58:21

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

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

2026/9/19 2:58:21

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

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

2026/9/19 4:03:23

Altium Designer死铜清理全攻略:从判定逻辑到实战排查

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

2026/9/19 4:03:23

Qwen3.8本地部署全攻略:显存估算、量化选型与避坑指南

折腾了大概一周,中间翻车翻到怀疑人生,才把Qwen3.8在本地部署这件事彻底跑通。这期间踩过的坑五花八门:下载到损坏的模型权重、因为显存估算错误导致推理直接卡死、模型文件和服务端版本对不上、上下文稍微一长就开始吞字……如果你正准备把Q…

2026/9/19 4:03:23

LLVM项目深度解析:编译器基础设施核心架构与工程实践

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

2026/9/19 4:03:23

自然语言驱动开发:vibe coding与工具选型实战指南

1. “vibe coding”不是玄学,是自然语言驱动开发的实践范式演进最近在几个技术社区里频繁看到“vibe coding”这个词被反复提起——不是作为营销话术,而是真实出现在工程师的日常协作记录、内部分享PPT甚至代码评审备注里。它不像“低代码”那样强调拖拽…

2026/9/18 14:13:01

拯救者Y7000黑屏故障排查与维修实战指南

1. 项目概述:一台黑屏的拯救者Y7000,到底卡在哪一步? 联想拯救者Y7000系列笔记本,从2018年第一代搭载i5-8300H开始,到后来的i7-9750H、i7-10750H、i5-11400H,再到2023年款的R7-7840HS,它始终是学…

2026/9/19 0:03:10

验证 OpenSpec 兼容性,Cursor 的 Token 从 TaoToken 出

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

2026/9/19 0:03:10

书桌角落的 Mac mini,OpenClaw 通过 TaoToken 跑任务。

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

2026/9/19 0:03:10

oh-my-hermes:打造跨工具的命令编排与插件化工作流

1. 项目概述与设计初衷1.1 它到底是什么先说结论:oh-my-hermes 是一个面向开发者日常终端操作的效率工具套件,核心定位是“把分散在各类命令行工具里的高频操作,统一收拢成一套插件化、可编排的工作流”。项目灵感来源很明显——oh-my-zsh 重…

2026/9/18 14:13:03

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/18 14:13:02

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/18 14:13:02

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

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

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

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