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工具!

相关推荐
夫子3965 小时前
【深度干货】Transformer推理优化完全指南:模型压缩、推理加速与硬件调优
人工智能·llm
智泊AI7 小时前
终于有人把AI大模型训练过程讲明白了!!!
llm
大模型真好玩8 小时前
大模型Agent开发框架哪家强?12项Agent开发框架入门与选型
人工智能·agent·mcp
小小毛毛虫~8 小时前
使用Cursor遇到的问题(一):cursor使用conda虚拟环境
python·conda·cursor
数据智能老司机10 小时前
建构 AI Agent 应用——Agentic 系统的学习机制
架构·llm·agent
魁首10 小时前
MCP与ACP本质区别深度分析
claude·gemini·mcp
数据智能老司机13 小时前
建构 AI Agent 应用——编排
架构·llm·agent
董厂长1 天前
SubAgent的“指令漂移 (Instruction Drift)“困境
人工智能·agent·mcp·subagent
魁首1 天前
初识 MCP (Model Context Protocol)
claude·gemini·mcp
镰刀韭菜1 天前
【AI4S】大语言模型与化学的未来,以及整合外部工具和聊天机器人的潜力
llm·transformer·大语言模型·药物设计·分子发现·chemchat·smiles