ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于MCP TypeScript SDK 手搓一个 MCP Server:把本地工具接进 Cline MCP

基于MCP TypeScript SDK 手搓一个 MCP Server:把本地工具接进 Cline MCP 1. 从零手搓 MCP Server 到底解决什么问题MCP 全称 Model Context Protocol你可以把它理解成「给大模型用的 USB-C 接口」以前每接一个本地能力查数据库、读日志、跑脚本都要在客户端里写一套私有适配现在只要按协议暴露成 MCP Server任何支持 MCP 的客户端都能即插即用。Cline MCP 就是这类客户端里比较典型的一个它跑在编辑器里通过 stdio 拉起你写的本地进程把工具列表读进去再在对话里按需调用。这篇要做的是用 MCP TypeScript SDK 从零搭一个能被 Cline MCP 调用的本地 MCP Server。适合谁会一点 Node.js、想让 AI 直接操作本地文件或内部接口、又不想把数据往云端传的开发者。核心检索词就是 MCP Server、TypeScript SDK、Cline MCP 配置全文围绕这三件事展开。我试过直接拿官方示例改结果卡在 ESM 与 tsconfig 的模块解析上报了一堆ERR_MODULE_NOT_FOUND。所以下面会把项目初始化、工具注册、stdio 启动、Cline 配置、调用验证、报错排查完整走一遍配置片段都能直接复制。先说清楚 MCP Server 能暴露的三类东西这决定了你写代码时的取舍Resources 类似 GET 接口只负责把数据喂进模型上下文不该有副作用Tools 类似 POST 接口会执行计算或产生副作用是 Cline 里最常用的Prompts 是可复用模板帮模型按固定格式交互。绝大多数「把本地工具接进 Cline」的需求落在 Tools 上。一个容易忽略的点MCP Server 本身不调用大模型它只是被动响应客户端的 JSON-RPC 请求。模型什么时候调、传什么参数由客户端和模型决定你只负责把工具描述写清楚、把返回值格式写对。工具描述写得含糊模型就不会调或者传错参数这是新手最常见的坑。2. TaoToken 前置准备与 MCP TypeScript SDK 环境搭建在写 Server 之前先把模型侧的调用通道准备好。Cline 里真正发起对话、决定调用哪个工具的是背后的模型所以你需要一个能稳定访问模型的入口。我用的是 TaoToken官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 它兼容常见的 OpenAI 风格调用Cline 里填 Base URL 加 Key 就能用。先去控制台建一个 Keyhttps://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 然后在 API Keys 页面生成https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。拿到形如sk-xxxx的字符串后先放着第 4 节配 Cline 时要用。想先确认模型通不通可以直接在模型对话页试一句https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。接下来搭 Server 工程。MCP TypeScript SDK 的包名是modelcontextprotocol/sdk它同时提供 server 和 client 两套实现还内置了 stdio 与 Streamable HTTP 两种传输。本地接 Cline 用 stdio 就够了因为 Cline 会以子进程方式启动你的 Server通过标准输入输出收发 JSON-RPC 消息。初始化项目注意把type设成module否则 ESM 导入会出问题mkdir mcp-local-tools cd mcp-local-tools npm init -y npm pkg set typemodule npm install modelcontextprotocol/sdk zod npm install -D typescript tsx types/nodezod是必须的SDK 用它来定义工具的入参 schema并自动生成 JSON Schema 给客户端。tsx用来直接跑 TS省去编译步骤调试阶段很省事。然后是tsconfig.json这份配置我实测能跑通重点是module和moduleResolution都设成NodeNext让 TS 按 Node 的 ESM 规则解析.js后缀导入{ compilerOptions: { target: ES2022, module: NodeNext, moduleResolution: NodeNext, outDir: dist, rootDir: src, strict: true, esModuleInterop: true, skipLibCheck: true, resolveJsonModule: true, declaration: false, sourceMap: true }, include: [src/**/*.ts] }在package.json里补两个脚本方便编译和本地运行{ scripts: { build: tsc, dev: tsx src/server.ts, start: node dist/server.js } }这里有个关键约定在 ESM 模式下TS 源码里导入本地文件必须写.js后缀哪怕源文件是.ts。比如import { helper } from ./helper.js编译后 Node 才能找到dist/helper.js。不写后缀tsx可能能跑但node dist/server.js会直接报模块找不到这是第 5 节要重点讲的报错之一。3. 可复制的 MCP Server 入口与 Cline MCP 配置片段现在写 Server 入口。我做一个「本地工具集」包含两个工具一个读本地文本文件的行数一个做 BMI 计算覆盖「有副作用/读文件」和「纯计算」两种典型场景。文件放在src/server.tsimport { McpServer } from modelcontextprotocol/sdk/server/mcp.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { z } from zod; import { readFile } from node:fs/promises; const server new McpServer({ name: local-tools, version: 1.0.0, }); // 工具一统计本地文件行数 server.tool( count_file_lines, 读取本地文本文件并返回总行数参数为绝对路径, { filePath: z.string().describe(要统计的文件的绝对路径) }, async ({ filePath }) { try { const content await readFile(filePath, utf-8); const lines content.split(/\r?\n/).length; return { content: [{ type: text, text: 文件 ${filePath} 共 ${lines} 行 }], }; } catch (err) { return { content: [{ type: text, text: 读取失败: ${(err as Error).message} }], isError: true, }; } } ); // 工具二计算 BMI server.tool( calculate_bmi, 根据体重和身高计算 BMI 指数, { weightKg: z.number().positive().describe(体重单位千克), heightM: z.number().positive().describe(身高单位米), }, async ({ weightKg, heightM }) { const bmi weightKg / (heightM * heightM); return { content: [{ type: text, text: BMI ${bmi.toFixed(2)} }], }; } ); const transport new StdioServerTransport(); await server.connect(transport); console.error([local-tools] MCP server started on stdio);几个必须注意的点。第一server.tool的第二个参数是工具描述模型靠它判断何时调用写清楚「做什么、参数是什么」。第二返回值必须是{ content: [...] }结构type目前常用text。第三日志一定要用console.error打到 stderr因为 stdout 被 JSON-RPC 协议占用了往 stdout 打日志会污染协议流客户端直接解析失败。编译一下确认无误npm run build产物在dist/server.js。现在配 Cline MCP。在 Cline 的 MCP 配置里新增一个 serverstdio 类型命令指向 node参数指向编译产物。配置片段如下路径换成你自己的绝对路径{ mcpServers: { local-tools: { command: node, args: [/Users/you/mcp-local-tools/dist/server.js], env: {} } } }如果你还在调试阶段不想每次编译可以把 command 换成npxargs 换成[tsx, /Users/you/mcp-local-tools/src/server.ts]直接跑 TS 源码。但正式用建议编译后跑node启动更快也更稳。Cline 侧还需要配模型通道也就是第 2 节拿到的 TaoToken。在 Cline 的 API 配置里填 Base URLhttps://taotoken.net/apiKey 填你的sk-xxxxModel ID 填你在模型对话页确认可用的模型名。这三件套缺一不可Base URL 决定请求打到哪Key 决定鉴权Model ID 决定用哪个模型。填错任何一个表现都是请求失败或 401第 5 节会逐个对照。4. 验证请求与成功结果让 Cline 真正调用你的工具配置保存后Cline 会重启 MCP 连接。判断是否接上看 Cline 的 MCP 面板里local-tools是否显示为已连接并且列出了两个工具count_file_lines和calculate_bmi。如果工具列表是空的说明 Server 启动了但注册没生效回去检查server.tool是否在connect之前调用。先做一次纯计算调用最不容易受环境影响。在 Cline 对话里输入「用 calculate_bmi 算一下 70 千克、1.75 米的 BMI」。模型会发起工具调用参数是{weightKg:70,heightM:1.75}Server 返回{ content: [ { type: text, text: BMI 22.86 } ] }Cline 会把这段文本读回上下文然后组织成自然语言回复你。看到 22.86 就说明整条链路通了Cline 拉起进程 → 读取工具列表 → 模型决定调用 → Server 执行 → 结果回传。再验证读文件工具这个能确认 stdio 传输和异步 IO 都正常。先造一个测试文件printf line1\nline2\nline3\n /tmp/mcp-test.txt然后在 Cline 里说「用 count_file_lines 统计 /tmp/mcp-test.txt 的行数」。预期返回文件 /tmp/mcp-test.txt 共 4 行末尾换行会多算一行这是split的正常行为。如果返回「读取失败」多半是路径不对或权限问题不是协议问题。想脱离 Cline 单独验证 Server可以写个最小 client。SDK 自带 client 实现用StdioClientTransport拉起同一个 Serverimport { Client } from modelcontextprotocol/sdk/client/index.js; import { StdioClientTransport } from modelcontextprotocol/sdk/client/stdio.js; const transport new StdioClientTransport({ command: node, args: [dist/server.js], }); const client new Client({ name: test-client, version: 1.0.0 }); await client.connect(transport); const tools await client.listTools(); console.log(tools:, tools.tools.map((t) t.name)); const result await client.callTool({ name: calculate_bmi, arguments: { weightKg: 70, heightM: 1.75 }, }); console.log(result:, result.content);用npx tsx src/client.ts跑能看到工具名列表和BMI 22.86。这个 client 的好处是排障时能排除 Cline 的干扰直接确认 Server 本身没问题。5. 本篇常见报错排查401、local proxy failed、reading choices、OAuth排障分两层模型通道的错和 MCP 协议层的错。先看模型侧这几个报错在 Cline 里很典型。401 UnauthorizedKey 无效或没带上。检查 TaoToken 的 Key 是否复制完整、有没有多余空格Base URL 是否是https://taotoken.net/api注意结尾不要多加/v1之类除非文档明确要求。三件套里 Key 和 Base URL 必须配套换了一个另一个也要对。local proxy failedCline 请求模型时本地转发失败通常是 Base URL 写错、网络不通或端口被占。先确认 Base URL 拼写再确认本机能访问该地址。这类错和 MCP Server 无关别去改 Server 代码。reading choices 相关报错如Cannot read properties of undefined (reading choices)说明返回体不是预期的 OpenAI 风格结构常见原因是 Model ID 填错请求打到了不存在的模型返回了错误对象。去模型对话页确认可用模型名再回 Cline 改 Model ID。OAuth 相关报错某些客户端或模型通道会走 OAuth 流程如果配置里混用了鉴权方式会出现 token 获取失败。用 API Key 方式时确保没有残留的 OAuth 配置项清掉再重连。再看 MCP 协议层。ERR_MODULE_NOT_FOUNDESM 导入没写.js后缀或package.json没设type: module。对照第 2 节的 tsconfig 和导入写法改。Server 启动后工具列表为空server.tool调用在server.connect之后或者根本没执行到。确保所有注册都在 connect 之前。Cline 显示连接失败但手动node dist/server.js能跑多半是 Cline 配置里的路径不是绝对路径或用了相对路径导致工作目录不对。全部换成绝对路径。往 stdout 打了日志导致协议解析失败把console.log全改成console.error。这是最隐蔽的坑因为 Server 看起来「启动了」但客户端收不到合法消息。6. 把本地工具接进 Cline 的下一步工具跑通后扩展方向很直接。想加更多本地能力就继续用server.tool注册每个工具把描述和参数 schema 写清楚模型才知道什么时候调。需要暴露只读数据给模型参考用server.resource需要固定交互模板用server.prompt。长期在 Cline 里做编码和 Agent 任务的话模型调用量会上去可以考虑用 Coding Plan 把额度固定下来https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。接入细节和更多传输方式比如 Streamable HTTP可以查文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。如果你用 Claude Code 那套也有对应接入说明https://taotoken.net/claudecode?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite 。最后留一个实用习惯每次改完 Server先npm run build再用第 4 节的独立 client 跑一遍listTools和一次callTool确认没问题再回 Cline 测。这样能把「Server 的错」和「Cline 配置的错」分开排障效率高很多。
返回列表