掘金 人工智能 22小时前
MCP图片处理工具完整教程 - 动态发现与Cursor集成
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文档详细介绍了如何使用TypeScript构建一个基于MCP(Model Context Protocol)的图片处理工具。该工具支持动态工具发现、与Cursor IDE的无缝集成,并提供了完整的交互流程。文章通过项目结构、代码示例和核心实现,展示了如何进行图片大小调整、滤镜应用和裁剪操作,并提供了TypeScript的快速上手指南。

🖼️ **项目结构与初始化**: 项目结构清晰,包含`package.json`、`tsconfig.json`等配置文件,以及`src`目录下的核心代码。`package.json`定义了项目的依赖,包括`@modelcontextprotocol/sdk`、`sharp`等,`tsconfig.json`配置了TypeScript编译选项。

🔍 **参数验证**: 使用`zod`库进行参数验证,定义了`ResizeSchema`、`FilterSchema`和`CropSchema`等,确保输入参数的有效性。例如,`ResizeSchema`验证图片路径、宽度、高度和适配模式,确保输入的参数符合预期,避免错误。

🛠️ **工具实现**: 提供了`ResizeTool`、`FilterTool`和`CropTool`三个工具类,分别用于调整图片大小、应用滤镜和裁剪图片。每个工具类都继承自`BaseTool`,实现了`getToolDefinition`和`execute`方法,并使用`sharp`库进行图片处理。

🔄 **动态工具发现**: `BaseTool`类中的`getToolDefinition`方法用于获取工具的定义,这为动态发现工具提供了基础。通过定义`name`、`description`和`inputSchema`,使得工具能够被动态地注册和使用,方便扩展和维护。

概述

本教程将带您构建一个功能完整的MCP(Model Context Protocol)图片处理工具,重点展示:

项目结构

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 自动工具识别

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图片处理工具展示了:

通过这个例子,您可以快速掌握MCP开发的核心概念,并创建自己的MCP工具!

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

MCP 图片处理 TypeScript
相关文章