概述
本教程将带您构建一个功能完整的MCP(Model Context Protocol)图片处理工具,重点展示:
- 动态工具发现机制
- 与Cursor IDE的无缝集成
- 完整的交互流程
- TypeScript快速上手指南
项目结构
csharp
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
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
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 - 参数验证
typescript
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 - 基础工具类
typescript
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 - 调整大小工具
typescript
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 - 滤镜工具
typescript
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 - 裁剪工具
typescript
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 - 动态发现机制
typescript
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 - 主服务器文件
typescript
#!/usr/bin/env node
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 { 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服务器配置
json
{
"mcp.servers": {
"image-processing": {
"command": "node",
"args": ["/path/to/your/image-processing-mcp/dist/index.js"],
"env": {}
}
}
}
- 或者在
.cursor-settings.json
中配置
json
{
"mcp": {
"servers": {
"image-processing": {
"command": "node",
"args": ["./dist/index.js"],
"cwd": "/path/to/image-processing-mcp"
}
}
}
}
4. 完整交互流程演示
4.1 启动流程
bash
# 1. 构建项目
npm run build
# 2. 启动MCP服务器(通常由Cursor自动启动)
npm start
服务器启动日志:
erlang
🚀 启动图片处理MCP服务器...
🔧 已注册工具: resize_image - 调整图片尺寸,支持多种适配模式
🔧 已注册工具: apply_filter - 对图片应用各种滤镜效果
🔧 已注册工具: crop_image - 裁剪图片到指定区域
📊 可用工具数量: 3
🔌 等待客户端连接...
✅ MCP服务器已启动,等待请求...
4.2 工具发现过程
当Cursor连接到MCP服务器时:
1. Cursor发送工具列表请求
yaml
📋 客户端请求工具列表
📤 返回 3 个工具:
- resize_image: 调整图片尺寸,支持多种适配模式
- apply_filter: 对图片应用各种滤镜效果
- crop_image: 裁剪图片到指定区域
2. 用户在Cursor中的交互
arduino
用户: "帮我把这张图片调整为800x600像素"
3. Cursor调用工具
css
🔧 调用工具: resize_image
📥 参数: {
"path": "/path/to/image.jpg",
"width": 800,
"height": 600,
"fit": "cover"
}
4. 工具执行和结果返回
css
✅ 工具执行完成: 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中,您可以这样使用:
markdown
用户: "请帮我处理这张照片:
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 智能参数提示
typescript
// 工具定义中的inputSchema自动为Cursor提供:
// - 参数类型提示
// - 参数验证规则
// - 参数描述和示例
inputSchema = {
type: 'object',
properties: {
path: {
type: 'string',
description: '图片文件路径'
},
// ... 其他参数
}
}
5.3 扩展性设计
typescript
// 轻松添加新工具
export class WatermarkTool extends BaseTool {
name = 'add_watermark';
description = '为图片添加水印';
// 自动被发现和集成
}
// 在ToolDiscovery中注册
toolDiscovery.registerTool(new WatermarkTool());
6. 调试和监控
6.1 详细日志
每个操作都有详细的日志输出,便于调试:
typescript
console.log(`🔧 调用工具: ${name}`);
console.log(`📥 参数:`, JSON.stringify(args, null, 2));
console.log(`✅ 工具执行完成: ${name}`);
console.log(`📤 结果:`, JSON.stringify(result, null, 2));
6.2 错误处理
typescript
try {
const result = await tool.execute(args);
// 成功处理
} catch (error) {
console.log(`❌ 工具执行失败: ${name}`, error);
return {
content: [{
type: 'text',
text: `工具执行失败: ${error.message}`
}]
};
}
7. 最佳实践
7.1 参数验证
使用Zod进行严格的参数验证:
typescript
const params = this.validateArgs(args, ResizeSchema);
7.2 工具设计原则
- 单一职责:每个工具只做一件事
- 参数清晰:提供详细的参数描述
- 错误友好:返回有意义的错误信息
- 结果完整:返回详细的执行结果
7.3 性能优化
- 异步处理大文件
- 适当的错误边界
- 内存使用监控
8. 快速上手指南
8.1 5分钟快速开始
bash
# 1. 克隆项目
git clone <your-repo>
cd image-processing-mcp
# 2. 安装依赖
npm install
# 3. 构建项目
npm run build
# 4. 在Cursor中配置MCP服务器
# 5. 开始使用!
8.2 创建自定义工具
typescript
// 1. 继承BaseTool
export class YourCustomTool extends BaseTool {
name = 'your_tool_name';
description = '工具描述';
inputSchema = {
// 定义参数结构
};
async execute(args: any) {
// 实现工具逻辑
return {
success: true,
// 返回结果
};
}
}
// 2. 在ToolDiscovery中注册
8.3 测试工具
typescript
// 创建单元测试
import { YourCustomTool } from './your-tool';
const tool = new YourCustomTool();
const result = await tool.execute({
// 测试参数
});
console.log(result);
结语
这个MCP图片处理工具展示了:
- 如何实现动态工具发现
- 如何与Cursor无缝集成
- 如何构建可扩展的工具架构
- 如何提供友好的用户体验
通过这个例子,您可以快速掌握MCP开发的核心概念,并创建自己的MCP工具!