TypeScript 在 Cursor AI MCP 服务中的工程化实践
写于 2025-03-10
一、引言
MCP (模型上下文协议)是什么
The Model Context Protocol allows applications to provide context for LLMs in a standardized way, separating the concerns of providing context from the actual LLM interaction.
模型上下文协议允许应用程序以标准化的方式为LLM提供上下文,从而将提供上下文的任务与实际的LLM交互分离开来。
二、问题背景
在使用 Cursor AI 调用 MCP(Managed Code Protocol) 服务时,开发者可能会遇到以下痛点:
1. 官方 SDK 示例无法直接适配 Cursor AI
- 重复性 MCP 代码开发
- 每个 MCP 服务都需要编写相似的 协议解析、错误处理、日志记录 等基础代码,增加了维护成本。
- 缺乏统一的工程化方案,不同 MCP 服务的代码风格和架构可能不一致,影响团队协作。
三、问题分析
- 官方 SDK 与 Cursor AI 的适配问题
主要原因在于一些核心的配置项如果缺失配置的话,CursorAI只会告诉你Client Closed,并不会告知关闭的原因,可能每次都要排查类似的问题。而这些核心配置项在官方SDK文档的位置也比较后面,对于初次开发的人员来说要定位问题比较麻烦。
- 重复性 MCP 代码开发
(1)每个 MCP 服务需独立实现协议解析、错误处理等基础逻辑。
(2)代码冗余,维护困难,且容易引入不一致性。
四、解决方案
对MCP SDK进行工程化封装,提供统一入口和基础模板。首个工具验证通过后,后续工具可复用相同模式快速接入,确保架构统一性。
五、实施过程-开发一个独立的MCP
第一步 安装核心的SDK与第三方库
// package.json
{
// 仅保留核心部分
"dependencies": {
"@modelcontextprotocol/sdk": "^1.6.1",
"zod": "^3.24.2"
},
"devDependencies": {
"@types/node": "^22.13.9",
"typescript": "^5.8.2"
}
}@modelcontextprotocol/sdk
MCP官方TypeScript SDK,用于搭建MCP的服务器与客户端
Github地址:https://github.com/modelcontextprotocol/typescript-sdk
npm install @modelcontextprotocol/sdk作用:
- 提供MCP(Model Context Protocol)的核心功能
- 包含服务器创建、通信协议和类型定义
- 提供与LLM通信的标准接口
主要组件:
- Server:创建MCP服务实例
- StdioServerTransport:基于标准输入/输出的通信传输层
- CallToolRequestSchema:处理工具调用的请求模式
- ListToolsRequestSchema:处理工具列表查询的请求模式
示例用法:
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";zod
npm install zod作用:
- 提供TypeScript优先的数据验证库
- 用于定义和验证工具输入参数的结构
- 提供丰富的验证规则和错误处理
主要功能:
- 类型定义与验证
- 自动类型推断
- 错误处理与格式化
- 模式组合与转换
示例用法:
import { z } from "zod";
const UserSchema = z.object({
username: z.string().min(3).max(20),
email: z.string().email(),
age: z.number().int().positive().optional(),
role: z.enum(["admin", "user", "guest"]).default("user"),
});
// 验证数据
try {
const userData = UserSchema.parse({
username: "zhang",
email: "zhang@example.com",
age: 30
});
// userData 类型已被推断为 { username: string; email: string; age?: number; role: "admin" | "user" | "guest" }
} catch (error) {
if (error instanceof z.ZodError) {
console.error("验证错误:", error.errors);
}
}第二步 通过SDK创建Server示例
const server = new Server(
{
name: "your-service-name", // 服务名称
version: "1.0.0", // 服务版本
},
{
capabilities: {
tools: {} // 启用工具功能,必须
}
}
);第三步 通过ZOD定义你期望的输入参数
const YourToolArgumentsSchema = z.object({
param1: z.string().describe("参数1的描述"),
param2: z.number().optional().describe("可选参数2的描述"),
param3: z.enum(["option1", "option2"]).describe("枚举参数的描述"),
param4: z.boolean().default(false).describe("布尔参数的描述"),
});第四步 定义支持的工具列表
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "your_tool_name", // 工具名称(使用下划线,cursor会默认转为下划线的调用)
description: "工具的详细描述", // 工具描述
// inputSchema是对第三步的Object补充
inputSchema: {
type: "object",
properties: {
param1: {
type: "string",
description: "参数1的描述",
},
param2: {
type: "number",
description: "参数2的描述",
},
param3: {
type: "string",
enum: ["option1", "option2"],
description: "枚举参数的描述",
},
},
required: ["param1", "param3"], // 必需参数列表
},
},
// 可以定义多个工具
],
};
});参数说明:
- name:工具名称,使用下划线命名法(如your_tool_name)
- description:工具的详细描述,会展示给LLM
- inputSchema:输入参数的JSON Schema定义
- type:通常为"object"
- properties:定义各个参数的类型和描述
- 每个参数包含type和description
- 可以使用enum定义枚举值
- required:必需参数的名称数组
第五步 定义工具调用的处理逻辑
server.setRequestHandler(CallToolRequestSchema, async (request: any) => {
const { name, arguments: args } = request.params;
const toolName = name.replace(/_/g, '-'); // 将下划线转换为连字符
// 由于cursor调用的时候会将事件名称改为 xxx_xxx 的下划线格式,我们这里的 === 需要注意
// 为了避免定义的事件名与调用不一致导致的问题,一种可行的方法是将参数的 "_" 都转为 "-" 处理
if (toolName === "your-tool-name") {
try {
// 1. 验证参数
const { param1, param2, param3 } = YourToolArgumentsSchema.parse(args);
// 2. 实现工具逻辑
// ...执行具体操作...
// 在这里调用你的自定义函数:fun1()
// 3. 返回成功结果
return {
content: [
{
type: "text",
text: "操作成功!\n结果: ...",
},
// 可以返回多种类型的内容
{
type: "image",
url: "图片URL",
alt: "图片描述"
}
],
};
} catch (error) {
// 4. 错误处理
if (error instanceof z.ZodError) {
// 参数验证错误
throw new Error(
`参数无效: ${error.errors
.map((e) => `${e.path.join(".")}: ${e.message}`)
.join(", ")}`
);
}
// 其他错误
throw new Error(`操作失败: ${error}`);
}
} else {
// 未知工具
throw new Error(`未知工具: ${name}`);
}
});第六步 开启MCP工具服务
目录结构如下:
async function main() {
// 1. 创建传输层
const transport = new StdioServerTransport();
// 2. 连接服务
await server.connect(transport);
// 3. 记录服务启动信息
console.log("Your Service MCP Server running on stdio");
}
// 4. 启动服务并处理错误
main().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});六、实施过程-工程化MCP工具开发实践(stdio类型)
目标:开发一个主要处理将PSD文件中的图层按组的层次结构进行解析与导出,文字部分将会被清空的MCP,并处理为一个工程化MCP模板。
your-mcp-service/
├── src/
│ ├── index.ts # 主入口文件
│ ├── tools/ # 工具实现目录
│ │ ├── tool1.ts # 工具1实现
│ │ └── tool2.ts # 工具2实现
│ ├── schemas/ # 数据模式定义
│ │ └── input.ts # 输入参数模式
│ ├── utils/ # 工具函数
│ │ ├── logger.ts # 日志工具
│ │ └── helpers.ts # 辅助函数
│ └── config.ts # 配置文件
├── tests/ # 测试目录
│ └── tools.test.ts # 工具测试
├── .env # 环境变量
├── .gitignore # Git忽略文件
├── package.json # 项目配置
├── tsconfig.json # TypeScript配置
└── README.md # 项目说明核心实现流程如下所示。
第一步 定义配置文件
**文件位置:**src/config.ts
/**
* 应用配置
*/
// 服务配置
export const SERVICE_CONFIG = {
name: "mcp-tools",
version: "1.0.0",
};
// 工具配置
export const TOOLS_CONFIG = {
// PSD切片工具
psdSlice: {
name: "slice_psd",
description: "Slice a PSD file into separate PNG layers",
},
};
// 日志配置
export const LOG_CONFIG = {
level: process.env.LOG_LEVEL || "INFO",
};
export default {
SERVICE_CONFIG,
TOOLS_CONFIG,
LOG_CONFIG,
};第二步 定义PSD切图工具的主要逻辑
**文件位置:**src/tools/psd-slice.ts
import PSD from 'psd';
import fs from 'fs';
import path from 'path';
// 用于处理文件名中的非法字符
const sanitizeFileName = (name: string): string => {
return name.replace(/[\\/:*?"<>|]/g, '_').trim();
};
// 类型声明
interface PSDLayer {
name: string;
_children?: PSDLayer[];
layer?: {
image?: {
saveAsPng: (path: string) => void;
};
};
export: () => { text?: any };
}
// 递归处理PSD图层
export const parseChildren = (
children: any[],
outputDir: string,
currentPath: string = ''
): PSDLayer[] => {
const flatChildren: PSDLayer[] = [];
for (const child of children) {
const layerName = sanitizeFileName(child.name || 'unnamed');
const currentLayerPath = currentPath ? `${currentPath}/${layerName}` : layerName;
if (child._children && child._children.length > 0) {
const groupDir = path.join(outputDir, currentLayerPath);
if (!fs.existsSync(groupDir)) {
fs.mkdirSync(groupDir, { recursive: true });
}
const innerChildren = parseChildren(child._children, outputDir, currentLayerPath);
flatChildren.push(...innerChildren);
} else {
if (!child.export().text) {
flatChildren.push(child);
const layerDir = path.join(outputDir, currentPath);
if (!fs.existsSync(layerDir)) {
fs.mkdirSync(layerDir, { recursive: true });
}
const imagePath = path.join(outputDir, currentPath, `${layerName}.png`);
try {
child.layer?.image?.saveAsPng(imagePath);
// console.log(`已保存图层: ${imagePath}`);
} catch (err) {
// console.error(`保存图层失败 ${layerName}:`, err);
}
}
}
}
return flatChildren;
};
export async function slicePSD(inputPath: string, outputDir: string = path.join(process.cwd(), 'output')) {
// 检查文件是否存在
if (!fs.existsSync(inputPath)) {
throw new Error(`文件不存在: ${inputPath}`);
}
// 创建输出目录
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// 解析PSD文件
const psd = PSD.fromFile(inputPath);
await psd.parse();
const data = await psd.tree();
const children = await data.children();
// 保存预览图
if (psd.image) {
await psd.image.saveAsPng(path.join(outputDir, "preview.png"));
}
// 处理所有图层
const flatChildren = parseChildren(children, outputDir);
return {
width: psd.header?.width || 'unknown',
height: psd.header?.height || 'unknown',
totalLayers: children.length,
processedLayers: flatChildren.length,
outputDir
};
}第三步 输入内容参数数据结构定义
**文件位置:**src/schemas/input.ts
import { z } from "zod";
// PSD切片工具的输入参数验证模式
export const SlicePSDArgumentsSchema = z.object({
inputPath: z.string().describe("PSD文件在当前目录的绝对路径"),
outputDir: z.string().optional().describe("输出目录在当前目录的绝对路径"),
});第四步 定义一些可能会用到的工具函数
**文件位置:**src/utils/helpers.ts
/**
* 通用辅助函数
*/
import { z } from 'zod';
/**
* 格式化Zod验证错误
* @param error Zod错误对象
* @returns 格式化后的错误消息
*/
export function formatZodError(error: z.ZodError): string {
return error.errors
.map((e) => `${e.path.join(".")}: ${e.message}`)
.join(", ");
}
/**
* 将下划线命名转换为连字符命名
* @param name 下划线命名的字符串
* @returns 连字符命名的字符串
*/
export function underscoreToDash(name: string): string {
return name.replace(/_/g, '-');
}
/**
* 格式化工具响应内容
* @param text 响应文本
* @returns 格式化后的响应内容
*/
export function formatToolResponse(text: string) {
return {
content: [
{
type: "text",
text,
},
],
};
}
/**
* 安全执行异步函数
* @param fn 要执行的异步函数
* @param errorMessage 错误消息前缀
* @returns 函数执行结果或错误
*/
export async function safeExecute<T>(
fn: () => Promise<T>,
errorMessage: string = "操作失败"
): Promise<T> {
try {
return await fn();
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`参数无效: ${formatZodError(error)}`);
}
throw new Error(`${errorMessage}: ${error}`);
}
}第五步 定义MCP工具服务的主入口
**文件位置:**src/index.ts
/**
* MCP工具服务主入口
*/
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
// 导入配置
import { SERVICE_CONFIG, TOOLS_CONFIG } from './config.js';
// 导入工具实现
import { slicePSD } from './tools/psd-slice.js';
// 导入参数验证模式
import { SlicePSDArgumentsSchema, CreateVueAppArgumentsSchema } from './schemas/input.js';
// 导入辅助函数
import { underscoreToDash, formatToolResponse, safeExecute } from './utils/helpers.js';
import logger from './utils/logger.js';
// 创建MCP服务器实例
const server = new Server(
{
name: SERVICE_CONFIG.name,
version: SERVICE_CONFIG.version,
},
{
capabilities: {
tools: {}
}
}
);
// 注册可用工具列表
server.setRequestHandler(ListToolsRequestSchema, async () => {
logger.info("收到工具列表请求");
return {
tools: [
{
name: TOOLS_CONFIG.psdSlice.name,
description: TOOLS_CONFIG.psdSlice.description,
inputSchema: {
type: "object",
properties: {
inputPath: {
type: "string",
description: "PSD文件在当前目录的绝对路径",
},
outputDir: {
type: "string",
description: "输出目录在当前目录的绝对路径",
},
},
required: ["inputPath"],
},
},
],
};
});
// 处理工具调用
server.setRequestHandler(CallToolRequestSchema, async (request: any) => {
const { name, arguments: args } = request.params;
logger.info(`收到工具调用请求: ${name}`);
const toolName = underscoreToDash(name);
// 处理PSD切片工具
if (toolName === underscoreToDash(TOOLS_CONFIG.psdSlice.name)) {
return await safeExecute(async () => {
const { inputPath, outputDir } = SlicePSDArgumentsSchema.parse(args);
logger.info(`开始切片PSD文件: ${inputPath}`);
const result = await slicePSD(inputPath, outputDir);
return formatToolResponse(`PSD切片完成!\n
文件信息:
- 宽度: ${result.width}px
- 高度: ${result.height}px
- 总图层数: ${result.totalLayers}
- 处理完成的图层数: ${result.processedLayers}
输出目录: ${result.outputDir}`);
}, "PSD切片失败");
}
// 未知工具
else {
throw new Error(`未知工具: ${name}`);
}
});
// 启动服务器
async function main() {
try {
logger.info("正在启动MCP服务...");
const transport = new StdioServerTransport();
await server.connect(transport);
logger.info(`${SERVICE_CONFIG.name} MCP服务已启动`);
} catch (error) {
logger.error("服务启动失败:", error);
process.exit(1);
}
}
// 处理未捕获的异常
process.on('uncaughtException', (error) => {
logger.error("未捕获的异常:", error);
});
process.on('unhandledRejection', (reason) => {
logger.error("未处理的Promise拒绝:", reason);
});
// 启动服务
main().catch((error) => {
logger.error("Fatal error in main():", error);
process.exit(1);
});第六步 定义构建的脚本
{
// ...
"scripts": {
"build": "mkdir -p build && tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
},
"files": [
"build"
],
}执行构建命令 npm run build 即可完成脚本的构建,构建产物在项目根目录的 build 文件夹下。
第七步 在Cursor中开启使用
新增一个MCP的服务器

假设构建后脚本的绝对路径是:/Users/KevinKwok/mcp/psd-slice-mcp/build/index.js。我们自定义一个服务器的名称,并且将Type设置为command类型。

开启Cursor AI的YOLO模式,以支持能够在Agent中让AI直接调用MCP工具

至此,我们已经实现了相关的自动切图MCP工具服务,我们可以直接到新版Cursor中,开启Agent模式后直接对话调用相关的逻辑即可。
简单使用如下:

执行效果如图所示(可以看到图层结构与PSD文件内一致,preview为PSD的预览图)


七、总结
在开发过程中,我们容易遇到以下的一些坑点:
1.初始化Server实例的时候,必须带上如下参数,否则MCP服务启动会失败报错。【Server does not support tools (required for tools/list)】
{
capabilities: {
tools: {}
}
}2.在定义输入与输出的参数中,如果涉及到文件路径相关的输入时,需要对参数添加需要为绝对路径的自然语言描述。否则在cursor AI调用相关工具的时候,其传入的文件路径不存在导致问题。

3.在使用已有MCP工具的时候(例如说构建好的产物),需要注意安装相关依赖,即需要确保相关内容在本地是可以直接正常执行的,否则也会使得MCP服务无法正常使用。
八、展望
期望基于该MCP工程化项目构建一个智能化的MCP开发框架,以实现以下几点功能:
1.开发者提供工具的业务逻辑描述(自然语言/已有的脚本)
2.框架直接按照 config、tools、schema 几个核心步骤生成代码到MCP工程项目内
3.构建后直接能够在MCP的客户端(如Cursor)内直接使用
