功能简介
本文基于ModelContextProtocol
的服务端工具,实现了两个功能。
- 实现一个工具
get-svg
: 根据用户提供的SVG名称svgName
, 从IconFont
网站获取 SVG 图标。 - 实现一个工具
change-svg
: 当使用get-svg
获取的SVG图标不合适的时候,根据svgName
重新获取一个新的SVG图标
本文基于和参考了官方文档获取天气的示例, 建议先学习和了解 对于服务器开发人员 - 模型上下文协议 --- For Server Developers - Model Context Protocol
代码仓库
mcp_svg_search: MCP服务,提供两个工具让Cursor可以去iconfont找SVG图标,不满意时可以再更换。
想法来源
最近 MCP
挺火,那天在官网看完快速入门, 提供的样例是一个查询天气的工具。 写完之后觉得挺有意思,但是好像有点不实用。
突然想到我们做前端的, 经常需要去iconfont
找SVG图标, 我能不能让 Cursor
支持这个功能。 而且在实现上来说和查询天气没有什么本质上的区别。 说干就干,开写。
模拟请求
首先,和获取天气一样,我们要有一个查询获取SVG图标的接口, 扒了一下 iconfont
搜索图标的接口, 把请求头和请求参数给灵码
和 Cursor
, 让他们帮我用 node-fetch
模拟一下搜索的请求。( 灵码
也是阿里系的好像有点手足相残了。)
注意: 技术用于服务自身,分享用于学习交流, 请不要用于非法用途。
测试一下获取一个名为 apple
的SVG图标。
js 代码解读复制代码import fetch from 'node-fetch';
// icon font 网站的cookie
const cookie = ``
// icon font 网站的token
const ctoken = ``
// 根据svgName获取svg
async function getSVGData(svgName){
try {
// 请求头
const headers = {
accept: 'application/json, text/javascript, */*; q=0.01',
'accept-encoding': 'gzip, deflate, br, zstd',
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
'bx-v': '2.5.28',
'cache-control': 'no-cache',
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
cookie: cookie,
origin: 'https://www.iconfont.cn',
pragma: 'no-cache',
referer: `https://www.iconfont.cn/search/index?searchType=icon&q=${svgName}&page=1&fromCollection=-1`,
'sec-ch-ua': '"Microsoft Edge";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0',
'x-csrf-token': ctoken,
'x-requested-with': 'XMLHttpRequest'
};
// 表单数据
const formData = new URLSearchParams({
q: `${svgName}`,
sortType: 'updated_at',
page: '1',
pageSize: '54',
sType: '',
fromCollection: '-1',
fills: '',
t: `${(new Date()).getTime()}`,
ctoken: ctoken
}).toString();
// 发送请求
const response = await fetch('https://www.iconfont.cn/api/icon/search.json', {
method: 'POST',
headers: headers,
body: formData
})
const resJSON = await response.json() ;
console.log(resJSON);
if(resJSON?.code === 200) {
return resJSON.icons?.[0]
}
else {
throw new Error(`Failed to fetch svg data: ${response.statusText}`);
}
} catch (error) {
console.error(error);
return null;
}
}
getSVGData('apple')
此处的cookies和ctoken, 请使用你自己的,不知道的话F12
看一下就知道了。
看了一下控制台, 输出成功。模拟请求已成功。
我们再改造一下,符合实际的业务需求, 以下是最终版本。
js 代码解读复制代码/**
* @description 根据svg图标名称获取svg
* @param svgName 获取的svgName
* @param ids 已经获取过的id组合, 以 - 分隔
* @returns 返回svg的信息
*/
async function getSvgIcon(svgName, ids = undefined) {
try {
// 模拟请求,如果请求不行,请自行修改
const headers = {
accept: 'application/json, text/javascript, */*; q=0.01',
'accept-encoding': 'gzip, deflate, br, zstd',
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
'bx-v': '2.5.28',
'cache-control': 'no-cache',
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
cookie: cookie,
origin: 'https://www.iconfont.cn',
pragma: 'no-cache',
referer: `https://www.iconfont.cn/search/index?searchType=icon&q=${svgName}&page=1&fromCollection=-1`,
'sec-ch-ua': '"Microsoft Edge";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0',
'x-csrf-token': ctoken,
'x-requested-with': 'XMLHttpRequest'
};
// 表单数据
const formData = new URLSearchParams({
q: `${svgName}`,
sortType: 'updated_at',
page: '1',
pageSize: '54',
sType: '',
fromCollection: '-1',
fills: '',
t: `${(new Date()).getTime()}`,
ctoken: ctoken
}).toString();
// 发送请求
const response = await fetch('https://www.iconfont.cn/api/icon/search.json', {
method: 'POST',
headers: headers,
body: formData
});
// 获取json
const resJSON = await response.json();
if (resJSON?.code === 200) {
// 如果ids不存在,就是纯粹获取
if (!ids) {
return resJSON?.data.icons?.[0];
}
// 当ids存在,就是换一个图标
else {
// 分割ids获取id数组
const _ids = ids.split('-').map(id => {
return parseInt(id);
});
// 返回第一个id不包含在_ids中的icon
return resJSON?.data?.icons?.filter((icon) => {
return !_ids.includes(icon.id);
})?.[0];
}
}
else {
throw new Error(`Failed to fetch svg data: ${response.statusText}`);
}
}
catch (error) {
console.error(error);
return null;
}
}
这里默认在获取的时候,返回列表的第一个页面。 增加了一个ids
的参数用于对获取的SVG图标不满意的时候,调取更换svg的工具。 这个后面再讲解。
注册一个 get-svg
工具
这个工具实现了根据用户提供的SVG名称svgName
, 从 IconFont
网站获取 SVG 图标。
js 代码解读复制代码// 根据名称获取svg图标的tool
server.tool(
'get-svg',
'根据svg图标名称获取svg',
{
svgName: z.string().describe('svg图标名称'),
},
async ({ svgName }) => {
const svg = await getSvgIcon<{
id: number;
name: string;
status: number;
is_private: number;
category_id: string;
slug: string;
unicode: string;
width: number;
height: number;
defs: null | string; // 假设defs可以为null或字符串
path_attributes: string;
fills: number;
font_class: string;
user_id: number;
repositorie_id: number;
created_at: string;
updated_at: string;
svg_hash: string;
svg_fill_hash: string;
fork_from: null | number; // 假设fork_from可以为null或数字
deleted_at: null | string; // 假设deleted_at可以为null或字符串
show_svg: string;
}>(svgName);
if (!svg) {
return {
content: [{
type: 'text',
text: '获取svg失败,请检查svg名称是否正确'
}]
}
}
const formattedSvg = `
svg图标名称: ${svg.name},
show_svg: ${svg.show_svg},
path_attributes: ${svg.path_attributes},
id: ${svg.id},
ids: ${svg.id}
`;
/*
// 目前cursor不支持type: image的消息, 后续支持可以传预览图
const buffer = await sharp(Buffer.from(svg.show_svg)).resize(128, 128) // 设置宽度和高度为128px
.png().toBuffer();
const base64 = buffer.toString('base64') */
return {
content: [{
type: 'text',
text: formattedSvg
},
/* {
"type": "image",
"data": base64,
"mimeType": "image/png"
} */
]
}
}
)
看了一下查询接口返回的数据格式
可以看到show_svg
就是我们要的svg内容, 复制一下请求返回值,然后让灵码
帮我写了一下TS类型。 返回给Cursor
的内容只取了svg图标名称
、 show_svg(svg元素)
、 path_attributes(路径参数)
、 id
、 ids(已获取过的svg图标id集合字符串,以 “-” 拼接,用于后续重新获取功能)
。需要其他参数的可以自行增加。
改造完后就可以试一下了,运行一下打包命令pnpm build
, 再配置一下Cursor
的MCP
(此处在官方文档有详解,大概就是编译成JS文件
,然后配置MCP
指向该文件)
此处由于我环境安装了fnm
, 所以命令比较不一样。 正常情况下, 直接以下命令应该即可:
js 代码解读复制代码node D:\\MyProject\\svgSearch\\build\\index.js
// 或者
npx D:\\MyProject\\svgSearch\\build\\index.js
此处的路径置换为你系统内的打包路径。
配置保存好后,看到 MCP
服务显示正常,tools
显示正常(此处显示两个, 有一个稍后实现)。
我们试一下获取一个 名为banana
的 SVG图标:
看到已经触发了MCP
的 tool
, 我们点击 Run tool
调用成功! 目前已经实现了第一个功能: 根据提供的SVG名称到iconfont
找, 然后返回第一个图标。
第二个工具 change-svg
: 重新获取一个新的SVG图标
当使用get-svg
获取的SVG图标不合适的时候怎么办?重新调用get-svg
? 但是逻辑上已经写死返回列表的第一个, 重新调用也是获取同一个。 因此我们再写一个工具去重新获取一个不同的SVG图标: change-svg
js 代码解读复制代码// 根据ids和svg图标名称去重新获取一个新的svg
server.tool('change-svg', '根据id和svg图标名称去重新获取一个新的svg', {
svgName: z.string().describe('svg图标名称'),
id: z.string().describe('之前获取的相同名称的svg图标的id'),
ids: z.string().describe('之前获取的相同名称的svg图标的ids')
}, async ({ svgName, id, ids }) => {
const svg = await getSvgIcon(svgName, ids) as any;
if (!svg) {
return {
content: [{
type: 'text',
text: '获取svg失败,请检查svg名称是否正确'
}]
};
}
const formattedSvg = `
svg图标名称: ${svg.name},
show_svg: ${svg.show_svg},
path_attributes: ${svg.path_attributes},
id: ${svg.id},
ids: ${ids}-${svg.id}
`;
return {
content: [{
type: 'text',
text: formattedSvg
}]
};
})
其实和get-svg
没有太大的区别,关键在于使用了一个ids
变量记录了所有获取过的SVG图标
的id
, 在返回给Cursor
的时候进行了一个拼接更新。
再在调用getSVGData
获取到iconfont
的返回数据时候, 如果存在ids
则去做一个去重判断, 返回第一个不重复的SVG图标
js 代码解读复制代码 // 如果ids不存在,就是纯粹获取
if (!ids) {
return resJSON?.data.icons?.[0];
}
// 当ids存在,就是换一个图标
else {
// 分割ids获取id数组
const _ids = ids.split('-').map(id => {
return parseInt(id);
});
// 返回第一个id不包含在_ids中的icon
return resJSON?.data?.icons?.filter((icon) => {
return !_ids.includes(icon.id);
})?.[0];
}
我们增加完这个工具后,再编译一下, 在Cursor
刷新一下MCP
工具。然后试一下,让他帮我更换一个图标。
可以看到已经触发了change-svg
。我们再跑一下。
可以看到调用成功,我们可以保存一下SVG的代码看一下, 正对的应该就是下面第一第二个元素。
总结拓展
本文只是对MCP
应用,官方示例的一个拓展和探索。把搜索天气更改成了搜索SVG, 体验一下工具的魅力。可能有人会说:哎呀,我叫Cursor
帮我写一个SVG图标就好了。 你也有你的道理的,我们做人不要那么执着。

拓展方面,我试过能不能在工具返回的时候顺便返回SVG图标的预览图, 因为看MCP
服务的文档是支持图片消息的
Tools - Model Context Protocol
但是我经过尝试后发现Cursor
好像不支持, 那我们就不要这么执着了,静待未来吧。
另外一个拓展可能是把列表返回Cursor
, 再结合用户提示去筛选可能会更好,有兴趣的可以自行实现一下。我也是刚学习,只是个想法,欢迎各位指导学习。
评论记录:
回复评论: