概述
本教程将带您构建一个功能完整的MCP(Model Context Protocol)图片处理工具,重点展示:
- 动态工具发现机制与Cursor IDE的无缝集成完整的交互流程TypeScript快速上手指南
项目结构
image-processing-mcp/├── package.json├── tsconfig.json├── src/│ ├── index.ts # 主服务器文件│ ├── tools/ # 工具实现│ │ ├── base.ts # 基础工具类│ │ ├── resize.ts # 调整大小工具│ │ ├── filter.ts # 滤镜工具│ │ └── crop.ts # 裁剪工具│ └── utils/│ ├── discovery.ts # 动态发现逻辑│ └── validation.ts # 参数验证├── dist/ # 编译输出└── README.md
1. 项目初始化
package.json
{ "name": "image-processing-mcp", "version": "1.0.0", "description": "MCP图片处理工具,支持动态发现", "main": "dist/index.js", "bin": { "image-processing-mcp": "dist/index.js" }, "scripts": { "build": "tsc", "start": "node dist/index.js", "dev": "ts-node src/index.ts" }, "dependencies": { "@modelcontextprotocol/sdk": "^0.4.0", "sharp": "^0.33.0", "zod": "^3.22.0" }, "devDependencies": { "@types/node": "^20.0.0", "typescript": "^5.0.0", "ts-node": "^10.9.0" }}
tsconfig.json
{ "compilerOptions": { "target": "ES2022", "module": "commonjs", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"]}
2. 核心实现
src/utils/validation.ts - 参数验证
import { z } from 'zod';// 基础图片参数验证export const ImagePathSchema = z.object({ path: z.string().min(1, '图片路径不能为空')});// 调整大小参数验证export const ResizeSchema = z.object({ path: z.string().min(1, '图片路径不能为空'), width: z.number().int().min(1, '宽度必须大于0').optional(), height: z.number().int().min(1, '高度必须大于0').optional(), fit: z.enum(['cover', 'contain', 'fill', 'inside', 'outside']).default('cover')});// 滤镜参数验证export const FilterSchema = z.object({ path: z.string().min(1, '图片路径不能为空'), type: z.enum(['blur', 'sharpen', 'grayscale', 'sepia']), intensity: z.number().min(0).max(100).default(50)});// 裁剪参数验证export const CropSchema = z.object({ path: z.string().min(1, '图片路径不能为空'), x: z.number().int().min(0), y: z.number().int().min(0), width: z.number().int().min(1), height: z.number().int().min(1)});export type ResizeParams = z.infer<typeof ResizeSchema>;export type FilterParams = z.infer<typeof FilterSchema>;export type CropParams = z.infer<typeof CropSchema>;
src/tools/base.ts - 基础工具类
import { Tool } from '@modelcontextprotocol/sdk/types.js';export abstract class BaseTool { abstract name: string; abstract description: string; abstract inputSchema: any; // 获取工具定义(用于动态发现) getToolDefinition(): Tool { return { name: this.name, description: this.description, inputSchema: this.inputSchema }; } // 执行工具逻辑 abstract execute(args: any): Promise<any>; // 验证参数 protected validateArgs(args: any, schema: any): any { try { return schema.parse(args); } catch (error) { throw new Error(`参数验证失败: ${error.message}`); } }}
src/tools/resize.ts - 调整大小工具
import sharp from 'sharp';import path from 'path';import { BaseTool } from './base.js';import { ResizeSchema, ResizeParams } from '../utils/validation.js';export class ResizeTool extends BaseTool { name = 'resize_image'; description = '调整图片尺寸,支持多种适配模式'; inputSchema = { type: 'object', properties: { path: { type: 'string', description: '图片文件路径' }, width: { type: 'number', description: '目标宽度(像素)' }, height: { type: 'number', description: '目标高度(像素)' }, fit: { type: 'string', enum: ['cover', 'contain', 'fill', 'inside', 'outside'], description: '适配模式', default: 'cover' } }, required: ['path'] }; async execute(args: any) { const params = this.validateArgs(args, ResizeSchema) as ResizeParams; try { const inputPath = params.path; const outputPath = this.generateOutputPath(inputPath, 'resized'); let sharpInstance = sharp(inputPath); // 获取原始图片信息 const metadata = await sharpInstance.metadata(); // 如果没有指定尺寸,返回原始尺寸信息 if (!params.width && !params.height) { return { success: false, message: '必须指定至少一个维度(宽度或高度)', originalSize: { width: metadata.width, height: metadata.height } }; } // 应用调整大小 sharpInstance = sharpInstance.resize({ width: params.width, height: params.height, fit: params.fit as any }); await sharpInstance.toFile(outputPath); // 获取输出图片信息 const outputMetadata = await sharp(outputPath).metadata(); return { success: true, message: '图片尺寸调整完成', inputPath, outputPath, originalSize: { width: metadata.width, height: metadata.height }, newSize: { width: outputMetadata.width, height: outputMetadata.height }, fitMode: params.fit }; } catch (error) { return { success: false, message: `调整尺寸失败: ${error.message}` }; } } private generateOutputPath(inputPath: string, suffix: string): string { const ext = path.extname(inputPath); const name = path.basename(inputPath, ext); const dir = path.dirname(inputPath); return path.join(dir, `${name}_${suffix}${ext}`); }}
src/tools/filter.ts - 滤镜工具
import sharp from 'sharp';import path from 'path';import { BaseTool } from './base.js';import { FilterSchema, FilterParams } from '../utils/validation.js';export class FilterTool extends BaseTool { name = 'apply_filter'; description = '对图片应用各种滤镜效果'; inputSchema = { type: 'object', properties: { path: { type: 'string', description: '图片文件路径' }, type: { type: 'string', enum: ['blur', 'sharpen', 'grayscale', 'sepia'], description: '滤镜类型' }, intensity: { type: 'number', minimum: 0, maximum: 100, description: '滤镜强度 (0-100)', default: 50 } }, required: ['path', 'type'] }; async execute(args: any) { const params = this.validateArgs(args, FilterSchema) as FilterParams; try { const inputPath = params.path; const outputPath = this.generateOutputPath(inputPath, `${params.type}_${params.intensity}`); let sharpInstance = sharp(inputPath); // 应用不同类型的滤镜 switch (params.type) { case 'blur': sharpInstance = sharpInstance.blur(params.intensity / 10); break; case 'sharpen': sharpInstance = sharpInstance.sharpen(params.intensity / 50); break; case 'grayscale': sharpInstance = sharpInstance.grayscale(); break; case 'sepia': // 使用色调调整实现棕褐色效果 sharpInstance = sharpInstance.tint({ r: 255, g: 235, b: 205 }); break; } await sharpInstance.toFile(outputPath); return { success: true, message: `${params.type}滤镜应用完成`, inputPath, outputPath, filterType: params.type, intensity: params.intensity }; } catch (error) { return { success: false, message: `滤镜应用失败: ${error.message}` }; } } private generateOutputPath(inputPath: string, suffix: string): string { const ext = path.extname(inputPath); const name = path.basename(inputPath, ext); const dir = path.dirname(inputPath); return path.join(dir, `${name}_${suffix}${ext}`); }}
src/tools/crop.ts - 裁剪工具
import sharp from 'sharp';import path from 'path';import { BaseTool } from './base.js';import { CropSchema, CropParams } from '../utils/validation.js';export class CropTool extends BaseTool { name = 'crop_image'; description = '裁剪图片到指定区域'; inputSchema = { type: 'object', properties: { path: { type: 'string', description: '图片文件路径' }, x: { type: 'number', description: '裁剪区域左上角X坐标' }, y: { type: 'number', description: '裁剪区域左上角Y坐标' }, width: { type: 'number', description: '裁剪区域宽度' }, height: { type: 'number', description: '裁剪区域高度' } }, required: ['path', 'x', 'y', 'width', 'height'] }; async execute(args: any) { const params = this.validateArgs(args, CropParams) as CropParams; try { const inputPath = params.path; const outputPath = this.generateOutputPath(inputPath, `crop_${params.x}_${params.y}_${params.width}_${params.height}`); // 先检查原图尺寸 const metadata = await sharp(inputPath).metadata(); if (params.x + params.width > metadata.width! || params.y + params.height > metadata.height!) { return { success: false, message: '裁剪区域超出图片边界', imageSize: { width: metadata.width, height: metadata.height }, requestedCrop: { x: params.x, y: params.y, width: params.width, height: params.height } }; } await sharp(inputPath) .extract({ left: params.x, top: params.y, width: params.width, height: params.height }) .toFile(outputPath); return { success: true, message: '图片裁剪完成', inputPath, outputPath, originalSize: { width: metadata.width, height: metadata.height }, cropArea: { x: params.x, y: params.y, width: params.width, height: params.height } }; } catch (error) { return { success: false, message: `裁剪失败: ${error.message}` }; } } private generateOutputPath(inputPath: string, suffix: string): string { const ext = path.extname(inputPath); const name = path.basename(inputPath, ext); const dir = path.dirname(inputPath); return path.join(dir, `${name}_${suffix}${ext}`); }}
src/utils/discovery.ts - 动态发现机制
import { BaseTool } from '../tools/base.js';import { ResizeTool } from '../tools/resize.js';import { FilterTool } from '../tools/filter.js';import { CropTool } from '../tools/crop.js';export class ToolDiscovery { private tools: Map<string, BaseTool> = new Map(); constructor() { this.registerTools(); } // 注册所有工具 private registerTools() { const toolInstances = [ new ResizeTool(), new FilterTool(), new CropTool() ]; toolInstances.forEach(tool => { this.tools.set(tool.name, tool); console.log(`🔧 已注册工具: ${tool.name} - ${tool.description}`); }); } // 获取所有工具定义(用于MCP协议) getAllToolDefinitions() { return Array.from(this.tools.values()).map(tool => tool.getToolDefinition()); } // 获取特定工具 getTool(name: string): BaseTool | undefined { return this.tools.get(name); } // 获取工具列表 getToolNames(): string[] { return Array.from(this.tools.keys()); } // 动态添加工具(支持插件式扩展) registerTool(tool: BaseTool) { this.tools.set(tool.name, tool); console.log(`🔧 动态添加工具: ${tool.name}`); } // 移除工具 unregisterTool(name: string) { if (this.tools.delete(name)) { console.log(`🗑️ 移除工具: ${name}`); return true; } return false; }}
src/index.ts - 主服务器文件
#!/usr/bin/env nodeimport { Server } from '@modelcontextprotocol/sdk/server/index.js';import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';import { CallToolRequestSchema, ListToolsRequestSchema,} from '@modelcontextprotocol/sdk/types.js';import { ToolDiscovery } from './utils/discovery.js';class ImageProcessingMCPServer { private server: Server; private toolDiscovery: ToolDiscovery; constructor() { this.server = new Server( { name: 'image-processing-mcp', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); this.toolDiscovery = new ToolDiscovery(); this.setupHandlers(); } private setupHandlers() { // 处理工具列表请求 - 动态发现的核心 this.server.setRequestHandler(ListToolsRequestSchema, async () => { console.log('📋 客户端请求工具列表'); const tools = this.toolDiscovery.getAllToolDefinitions(); console.log(`📤 返回 ${tools.length} 个工具:`); tools.forEach(tool => { console.log(` - ${tool.name}: ${tool.description}`); }); return { tools }; }); // 处理工具调用请求 this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; console.log(`🔧 调用工具: ${name}`); console.log(`📥 参数:`, JSON.stringify(args, null, 2)); const tool = this.toolDiscovery.getTool(name); if (!tool) { console.log(`❌ 工具不存在: ${name}`); return { content: [ { type: 'text', text: `错误:未找到工具 "${name}"` } ] }; } try { const result = await tool.execute(args); console.log(`✅ 工具执行完成: ${name}`); console.log(`📤 结果:`, JSON.stringify(result, null, 2)); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2) } ] }; } catch (error) { console.log(`❌ 工具执行失败: ${name}`, error); return { content: [ { type: 'text', text: `工具执行失败: ${error.message}` } ] }; } }); } async run() { console.log('🚀 启动图片处理MCP服务器...'); console.log(`📊 可用工具数量: ${this.toolDiscovery.getToolNames().length}`); console.log('🔌 等待客户端连接...'); const transport = new StdioServerTransport(); await this.server.connect(transport); console.log('✅ MCP服务器已启动,等待请求...'); }}// 启动服务器const server = new ImageProcessingMCPServer();server.run().catch(console.error);
3. Cursor集成配置
在Cursor中配置MCP
打开Cursor设置
- 按
Cmd/Ctrl + ,
打开设置搜索 "MCP" 或 "Model Context Protocol"添加MCP服务器配置
{ "mcp.servers": { "image-processing": { "command": "node", "args": ["/path/to/your/image-processing-mcp/dist/index.js"], "env": {} } }}
- 或者在
.cursor-settings.json
中配置{ "mcp": { "servers": { "image-processing": { "command": "node", "args": ["./dist/index.js"], "cwd": "/path/to/image-processing-mcp" } } }}
4. 完整交互流程演示
4.1 启动流程
# 1. 构建项目npm run build# 2. 启动MCP服务器(通常由Cursor自动启动)npm start
服务器启动日志:
🚀 启动图片处理MCP服务器...🔧 已注册工具: resize_image - 调整图片尺寸,支持多种适配模式🔧 已注册工具: apply_filter - 对图片应用各种滤镜效果🔧 已注册工具: crop_image - 裁剪图片到指定区域📊 可用工具数量: 3🔌 等待客户端连接...✅ MCP服务器已启动,等待请求...
4.2 工具发现过程
当Cursor连接到MCP服务器时:
1. Cursor发送工具列表请求
📋 客户端请求工具列表📤 返回 3 个工具: - resize_image: 调整图片尺寸,支持多种适配模式 - apply_filter: 对图片应用各种滤镜效果 - crop_image: 裁剪图片到指定区域
2. 用户在Cursor中的交互
用户: "帮我把这张图片调整为800x600像素"
3. Cursor调用工具
🔧 调用工具: resize_image📥 参数: { "path": "/path/to/image.jpg", "width": 800, "height": 600, "fit": "cover"}
4. 工具执行和结果返回
✅ 工具执行完成: resize_image📤 结果: { "success": true, "message": "图片尺寸调整完成", "inputPath": "/path/to/image.jpg", "outputPath": "/path/to/image_resized.jpg", "originalSize": { "width": 1920, "height": 1080 }, "newSize": { "width": 800, "height": 600 }, "fitMode": "cover"}
4.3 复杂工作流示例
在Cursor中,您可以这样使用:
用户: "请帮我处理这张照片:1. 先调整为1200x800像素2. 然后应用轻微的锐化效果3. 最后裁剪中心区域1000x600"
Cursor会自动:
- 调用
resize_image
工具调用 apply_filter
工具(type: sharpen, intensity: 30)调用 crop_image
工具(x: 100, y: 100, width: 1000, height: 600)5. 动态发现的优势
5.1 自动工具识别
- Cursor自动发现所有可用工具无需手动配置工具清单支持工具的热插拔
5.2 智能参数提示
// 工具定义中的inputSchema自动为Cursor提供:// - 参数类型提示// - 参数验证规则// - 参数描述和示例inputSchema = { type: 'object', properties: { path: { type: 'string', description: '图片文件路径' }, // ... 其他参数 }}
5.3 扩展性设计
// 轻松添加新工具export class WatermarkTool extends BaseTool { name = 'add_watermark'; description = '为图片添加水印'; // 自动被发现和集成}// 在ToolDiscovery中注册toolDiscovery.registerTool(new WatermarkTool());
6. 调试和监控
6.1 详细日志
每个操作都有详细的日志输出,便于调试:
console.log(`🔧 调用工具: ${name}`);console.log(`📥 参数:`, JSON.stringify(args, null, 2));console.log(`✅ 工具执行完成: ${name}`);console.log(`📤 结果:`, JSON.stringify(result, null, 2));
6.2 错误处理
try { const result = await tool.execute(args); // 成功处理} catch (error) { console.log(`❌ 工具执行失败: ${name}`, error); return { content: [{ type: 'text', text: `工具执行失败: ${error.message}` }] };}
7. 最佳实践
7.1 参数验证
使用Zod进行严格的参数验证:
const params = this.validateArgs(args, ResizeSchema);
7.2 工具设计原则
- 单一职责:每个工具只做一件事参数清晰:提供详细的参数描述错误友好:返回有意义的错误信息结果完整:返回详细的执行结果
7.3 性能优化
- 异步处理大文件适当的错误边界内存使用监控
8. 快速上手指南
8.1 5分钟快速开始
# 1. 克隆项目git clone <your-repo>cd image-processing-mcp# 2. 安装依赖npm install# 3. 构建项目npm run build# 4. 在Cursor中配置MCP服务器# 5. 开始使用!
8.2 创建自定义工具
// 1. 继承BaseToolexport class YourCustomTool extends BaseTool { name = 'your_tool_name'; description = '工具描述'; inputSchema = { // 定义参数结构 }; async execute(args: any) { // 实现工具逻辑 return { success: true, // 返回结果 }; }}// 2. 在ToolDiscovery中注册
8.3 测试工具
// 创建单元测试import { YourCustomTool } from './your-tool';const tool = new YourCustomTool();const result = await tool.execute({ // 测试参数});console.log(result);
结语
这个MCP图片处理工具展示了:
- 如何实现动态工具发现如何与Cursor无缝集成如何构建可扩展的工具架构如何提供友好的用户体验
通过这个例子,您可以快速掌握MCP开发的核心概念,并创建自己的MCP工具!