☰
基于MCP TypeScript SDK 手搓一个 MCP Server:把本地工具接进 Cline MCP
2026/10/1 14:51:52 网站建设 项目流程

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_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 地址是 https://taotoken.net/api ,它兼容常见的 OpenAI 风格调用,Cline 里填 Base URL 加 Key 就能用。

先去控制台建一个 Key:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,然后在 API Keys 页面生成:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。拿到形如sk-xxxx的字符串后先放着,第 4 节配 Cline 时要用。想先确认模型通不通,可以直接在模型对话页试一句:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 。

接下来搭 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 type=module npm install @modelcontextprotocol/sdk zod npm install -D typescript tsx @types/node

zod是必须的,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.ts:

import { 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 配置里新增一个 server,stdio 类型,命令指向 node,参数指向编译产物。配置片段如下(路径换成你自己的绝对路径):

{ "mcpServers": { "local-tools": { "command": "node", "args": ["/Users/you/mcp-local-tools/dist/server.js"], "env": {} } } }

如果你还在调试阶段,不想每次编译,可以把 command 换成npx,args 换成["tsx", "/Users/you/mcp-local-tools/src/server.ts"],直接跑 TS 源码。但正式用建议编译后跑node,启动更快也更稳。

Cline 侧还需要配模型通道,也就是第 2 节拿到的 TaoToken。在 Cline 的 API 配置里填 Base URLhttps://taotoken.net/api,Key 填你的sk-xxxx,Model 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拉起同一个 Server:

import { 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 Unauthorized:Key 无效或没带上。检查 TaoToken 的 Key 是否复制完整、有没有多余空格,Base URL 是否是https://taotoken.net/api(注意结尾不要多加/v1之类,除非文档明确要求)。三件套里 Key 和 Base URL 必须配套,换了一个另一个也要对。

local proxy failed:Cline 请求模型时本地转发失败,通常是 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_FOUND:ESM 导入没写.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_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。接入细节和更多传输方式(比如 Streamable HTTP)可以查文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。如果你用 Claude Code 那套,也有对应接入说明:https://taotoken.net/claudecode?utm_source=taotoken_aicg_blog_end&utm_content=claudecode&utm_campaign=rewrite 。

最后留一个实用习惯:每次改完 Server,先npm run build,再用第 4 节的独立 client 跑一遍listTools和一次callTool,确认没问题再回 Cline 测。这样能把「Server 的错」和「Cline 配置的错」分开,排障效率高很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询