MCP图片处理工具完整教程 - 动态发现与Cursor集成

大家好,我是土豆,欢迎关注我的公众号:土豆学前端

概述

本教程将带您构建一个功能完整的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

  1. 打开Cursor设置

    • Cmd/Ctrl + , 打开设置
    • 搜索 "MCP" 或 "Model Context Protocol"
  2. 添加MCP服务器配置

json 复制代码
{
  "mcp.servers": {
    "image-processing": {
      "command": "node",
      "args": ["/path/to/your/image-processing-mcp/dist/index.js"],
      "env": {}
    }
  }
}
  1. 或者在 .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会自动:

  1. 调用 resize_image 工具
  2. 调用 apply_filter 工具(type: sharpen, intensity: 30)
  3. 调用 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工具!

相关推荐
我在北国不背锅2 小时前
基于Java开发的浏览器自动化Playwright-MCP服务器
java·playwright·mcp
银空飞羽4 小时前
总结一下最近学习到的MCP风险问题(杂谈)
安全·mcp·trae
AryaNimbus9 小时前
全网最全 Cursor 配置指南:从入门到高效工作流!建议收藏
cursor
聚客AI9 小时前
Masked LM革命:解析BERT如何用15%掩码率颠覆NLP预训练
人工智能·llm·掘金·日新计划
DeepSeek忠实粉丝10 小时前
微调篇--超长文本微调训练
人工智能·程序员·llm
AI大模型10 小时前
构建可调用外部工具的AI助手:LangChain函数调用与API集成详解
程序员·langchain·llm
oil欧哟11 小时前
🧐 如何让 AI 接入自己的 API?开发了一个将 OpenAPI 文档转为 MCP 服务的工具
前端·人工智能·mcp
我不吃饼干11 小时前
我给掘金写了一个给用户加标签的功能
前端·javascript·cursor
虎鲸不是鱼12 小时前
Spring Boot3流式访问Dify聊天助手接口
java·spring boot·后端·大模型·llm