前情提要
关于mcp的所有内容都可以在官网找到:
mcp官网
接下来会简要的解释mcp的用处,以及实现一个简单mcp-server。
mcp能做什么
chagpt 刚出的时候,你的朋友阿光来问你,他想压缩一个文件夹下的图片,能不能直接跟chatgpt说他想压缩某个文件夹下的所有图片,然后chatgpt 就帮他压缩。当时那当然是不太行,但是你跟他说,没事,我帮你写一个脚本,你执行一下就可以了。于是你写了一个叫yasuotupian
的脚本,只需要执行:
bash
npx yasuotupian -d /User/私人/学习资料/图片
这样就可以自动找到阿光这个文件夹下的全部图片然后进行压缩。
但是阿光不满意,觉得以后要是电脑内存不够了,要压缩 /User/私人/学习资料/视频
里的内容的话,那没准又要多记一个yasuoshipin
的命令,太麻烦了。于是你没有办法,你只好给他做了个网页,让他把图片和视频拖进去然后执行压缩,顺便趁他上传的时候偷到了他全部的学习资料
。
现在有了mcp ,阿光就可以实现他想要的功能了。mcp 做的事情一句话描述就是让模型去调用你写的这个脚本
。
步骤大致如下(我猜的):
- 阿光告诉mcp客户端(我们后面以cursor 为例,mcp 客户端的案例我们下一节再写),我要压缩
/User/私人/学习资料/图片
下的图片 - 客户端处理好这句话的内容,理解到你的需求是
压缩
, 并且路径是/User/私人/学习资料/图片
- 客户端看一眼自己的mcp-server 的配置,发现有这么个合适的server里面有一个叫做tupianyasuo 的
tool
,顺便看一眼这个tool
有一个参数path - 调用这个
tool
,并且把路径当作实参传给path
那么我们就着手编写这个mcp-server并进行配置。
实现压缩工具
为了写mcp服务,我们需要安装一些必要的工具 当然,这些基本步骤mcp官网也有写,看这里。
他这个是保姆级教程了,我们按照他的要求创建好文件夹,安装好必要的工具。由于我们不是要实现天气查询mcp,所以我们只要做到安装依赖那一步就可以收手了。
接下来在src文件夹下创建下面几个文件:

index文件如下,这个是入口文件,代码格式基本固定:
ts
import { server } from './server.js';
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP Server running on stdio");
}
main().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});
server文件如下,格式也非常固定:
ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { setupTool } from "./tool.js";
const server = new McpServer({
name: "<your-server-name>",
version: "1.0.0",
capabilities: {
resources: {},
tools: {},
},
});
setupTool(server);
export { server }
tool文件格式如下,格式也非常固定
ts
// @ts-ignore
import { z } from 'zod';
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { run } from "./run.js";
// 定义输入参数的schema
const InputSchema = {
}
// 定义返回值的schema
const OutputSchema = {
}
function setupTool(server: McpServer) {
server.tool(
"<name>",
"<description>",
InputSchema,
async (params: any) => {
const result = await run();
return {
content: [
{ type: "text", text: result }
]
};
}
);
}
export { setupTool }
那么接下来就是实现真正的执行压缩的文件了,也就是run.ts的内容:
ts
import fs from 'fs';
import path from 'path';
import sharp from 'sharp';
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'];
// 定义压缩结果接口
interface CompressResult {
success: boolean;
originalSize?: number;
compressedSize?: number;
error?: string;
}
// 定义运行结果接口
interface RunResult {
success: boolean;
message: string;
compressedCount?: number;
failedCount?: number;
originalSize?: number;
compressedSize?: number;
compressionRatio?: string;
}
// 递归遍历文件夹,返回所有图片文件
function getAllImages(folderPath: string): string[] {
const images: string[] = [];
function traverse(folderPath: string) {
const files = fs.readdirSync(folderPath);
for (const file of files) {
const filePath = path.join(folderPath, file);
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
traverse(filePath);
}
else if (imageExtensions.includes(path.extname(file).toLowerCase())) {
images.push(filePath);
}
else {
// nothing to do 不要push进入images
}
}
}
traverse(folderPath);
return images;
}
// 使用sharp压缩图片
// 图片保持原有格式和大小
// 仅做质量压缩
async function compressImage(imagePath: string, outputPath: string, quality: number): Promise<CompressResult> {
try {
// 获取原始文件大小
const stats = fs.statSync(imagePath);
const originalSize = stats.size;
// 获取文件扩展名
const ext = path.extname(imagePath).toLowerCase();
// 创建sharp实例
let sharpInstance = sharp(imagePath);
// 根据不同格式设置压缩选项
switch (ext) {
case '.jpg':
case '.jpeg':
sharpInstance = sharpInstance.jpeg({ quality });
break;
case '.png':
sharpInstance = sharpInstance.png({ quality });
break;
case '.webp':
sharpInstance = sharpInstance.webp({ quality });
break;
case '.gif':
// GIF格式不支持quality参数,但可以通过colours和dither优化
sharpInstance = sharpInstance.gif({ colours: 128, dither: 0.5 });
break;
default:
// 对于其他格式,保持原格式并尝试压缩
sharpInstance = sharpInstance.jpeg({ quality });
}
// 压缩图片
await sharpInstance.toFile(outputPath);
// 获取压缩后的文件大小
const compressedStats = fs.statSync(outputPath);
const compressedSize = compressedStats.size;
return {
success: true,
originalSize,
compressedSize,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error)
};
}
}
// 调用压缩
async function runCompress(folderPath: string, quality: number, keepOriginalName: boolean, outputPath: string): Promise<RunResult> {
try {
// 检查输入文件夹是否存在
if (!fs.existsSync(folderPath)) {
return {
success: false,
message: `文件夹不存在: ${folderPath}`
};
}
// 确定输出文件夹路径
const finalOutputPath = outputPath || `${folderPath}_compressed`;
// 创建输出文件夹(如果不存在)
if (!fs.existsSync(finalOutputPath)) {
fs.mkdirSync(finalOutputPath, { recursive: true });
}
// 获取所有图片文件
const images = getAllImages(folderPath);
if (images.length === 0) {
return {
success: false,
message: `在文件夹 ${folderPath} 中没有找到支持的图片文件`
};
}
// 压缩所有图片
let totalOriginalSize = 0;
let totalCompressedSize = 0;
let compressedCount = 0;
let failedCount = 0;
for (const imagePath of images) {
const fileName = path.basename(imagePath);
const outputFileName = keepOriginalName ? fileName : `compressed_${fileName}`;
const outputFilePath = path.join(finalOutputPath, outputFileName);
const result = await compressImage(imagePath, outputFilePath, quality);
if (result.success) {
totalOriginalSize += result.originalSize || 0;
totalCompressedSize += result.compressedSize || 0;
compressedCount++;
} else {
failedCount++;
}
}
// 计算总体压缩率
const compressionRatio = totalCompressedSize / totalOriginalSize * 100 + '%';
return {
success: true,
message: `成功压缩 ${compressedCount}/${images.length} 个图片,失败 ${failedCount} 个,压缩率: ${compressionRatio}%`,
compressedCount,
failedCount,
originalSize: totalOriginalSize,
compressedSize: totalCompressedSize,
compressionRatio: compressionRatio
};
} catch (error) {
return {
success: false,
message: `压缩过程中出错: ${error instanceof Error ? error.message : String(error)}`
};
}
}
export { runCompress }
run文件的编写可以完全交给cursor去实现,当然如果仅仅是测试tool能不能运行的话,run函数直接return一个'hello world'也可以。
cursor配置
首先找到配置项

点进去写好配置
保存之后这样就运行起来了(这个配置文件win和mac可能会有区别)
接下来就可以在curosr中开Agent模式让他帮我们压缩文件了
这样就已经调用成功了,因为我这边要传多个参数,且质量参数为number类型,模型帮我做了两次参数调整。
调试
调试的话mcp文档也有,在这里
可能会遇到的报错
目前遇到一个,当tool执行的时候(也就是run文件中的代码执行的时候),如果有console.log,会报错JSON解析失败的错,注释掉console就好了。
大概就是这么多,如果有老哥解决了这个console.log的日志的问题,麻烦教一教 。