
1. 项目缘起当“技能”需要被分享与复用最近在折腾一些AI编程辅助工具时我遇到了一个挺有意思的痛点。相信不少深度使用过GitHub Copilot、Cursor或者类似基于Codex模型工具的朋友都有过类似的体验我们常常会针对特定的开发场景精心调教出一些非常好用的“技能”Skills。比如一个能根据数据库表结构自动生成TypeScript类型定义的提示词或者一个专门用于优化React组件性能的代码片段模板。这些技能散落在各个项目的注释里、某个不起眼的文本文件中或者干脆只存在于我们自己的记忆里。当换一台新电脑、开启一个新项目或者想和团队伙伴共享时这些宝贵的“私房技巧”就变得难以迁移和复用。这让我开始思考有没有一种方式能把这些零散的、非结构化的“技能”封装起来变成一个独立的、可移植的、并且支持分享的“资产”恰好Model Context ProtocolMCP进入了我的视野。MCP本质上是一种标准协议它允许不同的应用程序客户端与AI模型或工具服务器之间进行结构化的上下文信息交换。简单来说它就像给AI工具外接了一个标准化的“外挂硬盘”可以按需读取里面的特定信息。于是一个想法诞生了为什么不把Codex里那些实用的“技能”封装成一个MCP服务器呢这样一来任何支持MCP协议的AI编程工具理论上未来会越来越多都可以通过标准的接口来调用我封装好的技能而我也能轻松地将这个技能包分享给同事或社区。说干就干经过一段时间的摸索和实现这个想法变成了现实。今天我就来详细拆解一下这个项目的设计思路、核心实现以及踩过的那些坑。2. 核心设计如何定义与封装一个“技能”要把一个模糊的“技能”概念变成MCP协议里可被调用的资源第一步也是最关键的一步就是为“技能”建立一个清晰的数据模型。这决定了整个系统的扩展性和易用性。2.1 “技能”数据模型设计我设计的技能模型主要包含以下几个核心字段它们共同描述了一个技能的完整面貌{ “id”: “generate-ts-interface-from-sql”, “name”: “根据SQL生成TS接口”, “description”: “输入CREATE TABLE语句自动生成对应的TypeScript接口定义。”, “prompt_template”: “你是一个TypeScript专家。请将以下SQL表定义语句转换为精确的TypeScript接口。注意处理字段类型映射如VARCHAR - string, INT - number, TIMESTAMP - Date等、可空字段和注释。SQL语句{{sql}}”, “trigger_patterns”: [“sql”, “create table”, “表结构”], “category”: “code-generation”, “metadata”: { “author”: “your_name”, “version”: “1.0.0”, “compatibility”: [“cursor”, “claude-desktop”] } }设计考量与解析prompt_template与变量插值这是技能的灵魂。我采用了模板字符串的设计用{{变量名}}的语法来定义动态部分。当MCP客户端调用该技能时需要传入对应的变量值。这种设计比固定文本灵活得多使得一个技能可以适应不同的具体输入。例如上面的模板中的{{sql}}会在调用时被替换为实际的SQL语句。trigger_patterns触发模式这个字段是为了实现“智能触发”而设计的。当用户在编辑器中输入的内容匹配到这些关键词或模式时集成了此MCP的客户端工具可以主动提示用户“有一个相关技能可用”。这大大提升了技能的发现性和使用便捷性让它从被动的“工具调用”变成了主动的“智能建议”。category分类与metadata元数据分类便于管理和检索。元数据则包含了作者、版本、兼容性等实用信息。特别是兼容性字段可以指明这个技能主要针对哪些AI助手优化过如Cursor的Copilot、Claude for Desktop等因为不同助手的上下文处理方式可能有细微差别。2.2 MCP服务器架构选型MCP协议本身是语言无关的它基于JSON-RPC over stdio标准输入输出或HTTP。考虑到开发效率和生态我选择了Node.js作为实现语言。为什么是Node.js生态丰富有现成的modelcontextprotocol/sdk官方SDK大大降低了协议实现的复杂度。异步友好MCP通信本质上是I/O密集型操作Node.js的异步非阻塞模型非常适合。易于分发最终打包成一个npm包或可执行文件分享和安装都非常方便。项目的核心架构非常简单清晰技能定义文件 (skills/*.json) ↓ MCP服务器 (Node.js SDK) ← JSON-RPC → AI助手客户端 (如Cursor) ↓ 技能工具调用 (Tools) / 技能资源读取 (Resources)服务器启动后会扫描指定目录下的所有技能定义文件JSON格式将它们注册为MCP的Tools工具。当客户端发起一个tools/call请求时服务器会找到对应的技能模板结合传入的参数组装出最终的提示词Prompt然后以结构化的形式返回给客户端。客户端拿到这个“加工好的提示词”后再将其送入AI模型如Codex来获取最终的代码或文本结果。3. 实现详解从零搭建一个技能MCP服务器理论讲完了我们来看具体怎么实现。我会把关键代码和配置都列出来你可以直接跟着做。3.1 初始化项目与依赖安装首先创建一个新的项目目录并初始化。mkdir codex-skills-mcp-server cd codex-skills-mcp-server npm init -y接着安装核心依赖。这里我们需要官方的MCP SDK。npm install modelcontextprotocol/sdk为了更方便地处理文件监听和技能加载我们还可以添加chokidar和ajv用于JSON模式验证。npm install chokidar ajv3.2 构建技能加载器我们需要一个模块来负责从文件系统加载和验证技能定义。在skillLoader.js中const fs require(‘fs’).promises; const path require(‘path’); const Ajv require(‘ajv’); // 定义技能JSON模式用于验证文件格式是否正确 const skillSchema { type: ‘object’, properties: { id: { type: ‘string’, pattern: ‘^[a-z0-9-]$’ }, name: { type: ‘string’ }, description: { type: ‘string’ }, prompt_template: { type: ‘string’ }, trigger_patterns: { type: ‘array’, items: { type: ‘string’ } }, category: { type: ‘string’ }, metadata: { type: ‘object’ } }, required: [‘id’, ‘name’, ‘description’, ‘prompt_template’] }; const ajv new Ajv(); const validate ajv.compile(skillSchema); class SkillLoader { constructor(skillsDir ‘./skills’) { this.skillsDir skillsDir; this.skills new Map(); // 使用Map存储key为技能ID } async loadAllSkills() { try { const files await fs.readdir(this.skillsDir); const jsonFiles files.filter(f f.endsWith(‘.json’)); for (const file of jsonFiles) { const filePath path.join(this.skillsDir, file); await this.loadSkill(filePath); } console.log(已加载 ${this.skills.size} 个技能); } catch (error) { // 如果技能目录不存在则创建它 if (error.code ‘ENOENT’) { await fs.mkdir(this.skillsDir, { recursive: true }); console.log(‘创建了技能目录:’, this.skillsDir); } else { throw error; } } } async loadSkill(filePath) { try { const data await fs.readFile(filePath, ‘utf8’); const skill JSON.parse(data); if (!validate(skill)) { console.error(技能文件 ${filePath} 格式无效:, validate.errors); return; } this.skills.set(skill.id, skill); console.log(加载技能: ${skill.name} (${skill.id})); } catch (error) { console.error(加载技能文件失败 ${filePath}:, error.message); } } getSkill(id) { return this.skills.get(id); } getAllSkills() { return Array.from(this.skills.values()); } } module.exports SkillLoader;注意这里我使用了ajv库来做JSON模式验证。这是一个非常重要的实践它能确保从外部加载的技能文件格式是正确的避免因为一个手写的JSON语法错误导致整个服务器崩溃。在生产环境中数据验证永远是第一步。3.3 实现MCP服务器主逻辑这是项目的核心在server.js中。我们将使用MCP SDK提供的Server类。const { Server } require(‘modelcontextprotocol/sdk/server/index.js’); const { StdioServerTransport } require(‘modelcontextprotocol/sdk/server/stdio.js’); const SkillLoader require(‘./skillLoader.js’); class SkillsMCPServer { constructor() { this.server new Server( { name: ‘codex-skills-mcp’, version: ‘1.0.0’ }, { capabilities: { tools: {} } } ); this.skillLoader new SkillLoader(); // 设置工具列表处理函数 this.server.setRequestHandler(‘tools/list’, async () { const skills this.skillLoader.getAllSkills(); const tools skills.map(skill ({ name: skill.id, description: skill.description, inputSchema: { type: ‘object’, properties: this._extractInputSchemaFromTemplate(skill.prompt_template), required: this._extractRequiredInputs(skill.prompt_template) } })); return { tools }; }); // 设置工具调用处理函数 this.server.setRequestHandler(‘tools/call’, async (request) { const { name, arguments: args } request.params; const skill this.skillLoader.getSkill(name); if (!skill) { throw new Error(未找到技能: ${name}); } // 渲染提示词模板 const finalPrompt this._renderTemplate(skill.prompt_template, args); // 返回结果。注意MCP服务器不直接调用AI模型它只返回结构化的提示信息。 // 客户端负责将 finalPrompt.content 发送给AI模型。 return { content: [ { type: ‘text’, text: finalPrompt } ], isError: false }; }); // 可选提供资源Resources接口让客户端能读取技能的元信息 this.server.setRequestHandler(‘resources/list’, async () { const skills this.skillLoader.getAllSkills(); const resources skills.map(skill ({ uri: skill://${skill.id}, mimeType: ‘application/json’, name: skill.name })); return { resources }; }); this.server.setRequestHandler(‘resources/read’, async (request) { const { uri } request.params; const skillId uri.replace(‘skill://’, ‘’); const skill this.skillLoader.getSkill(skillId); if (!skill) { throw new Error(资源未找到: ${uri}); } return { contents: [{ uri, mimeType: ‘application/json’, text: JSON.stringify(skill, null, 2) }] }; }); } // 从模板字符串中提取输入参数模式简化版实际需要更复杂的解析 _extractInputSchemaFromTemplate(template) { // 这是一个简单实现通过正则查找 {{variable}} const regex /\{\{(\w)\}\}/g; const properties {}; let match; while ((match regex.exec(template)) ! null) { const varName match[1]; if (!properties[varName]) { properties[varName] { type: ‘string’, description: 替换模板中的 {{${varName}}} }; } } return properties; } _extractRequiredInputs(template) { const regex /\{\{(\w)\}\}/g; const required []; let match; while ((match regex.exec(template)) ! null) { required.push(match[1]); } // 去重 return [...new Set(required)]; } _renderTemplate(template, args) { let result template; for (const [key, value] of Object.entries(args)) { const placeholder {{${key}}}; result result.replace(new RegExp(placeholder, ‘g’), String(value)); } // 检查是否还有未替换的占位符可选用于调试 const remainingPlaceholders result.match(/\{\{\w\}\}/g); if (remainingPlaceholders) { console.warn(‘警告模板中存在未提供的变量:’, remainingPlaceholders); } return result; } async run() { await this.skillLoader.loadAllSkills(); const transport new StdioServerTransport(); await this.server.connect(transport); console.error(‘Codex Skills MCP 服务器已启动 (通过stdio通信)’); } } // 启动服务器 if (require.main module) { const server new SkillsMCPServer(); server.run().catch((error) { console.error(‘服务器运行失败:’, error); process.exit(1); }); } module.exports SkillsMCPServer;关键点解析tools/list与tools/call这是MCP工具协议的核心。list告诉客户端“我有哪些工具”call是客户端说“请帮我用某个工具处理这些参数”。我们的服务器在call中并不执行AI推理而是完成提示词工程——将模板和参数结合成最终发送给AI的指令。这种职责分离非常清晰。输入模式动态生成_extractInputSchemaFromTemplate函数尝试从模板字符串中自动分析出需要哪些参数。这虽然是一个基础实现但它带来了巨大的便利性技能开发者只需要写模板服务器就能自动生成对应的调用接口描述。更复杂的实现可以支持类型注解如{{sql: string}}。资源Resources接口除了作为工具被调用技能本身也可以作为一种“只读文档”供客户端查询。通过resources/readAI助手可以在需要时比如用户问“这个技能是干嘛的”获取技能的完整描述。这丰富了交互维度。3.4 创建你的第一个技能文件在项目根目录下创建skills文件夹然后在里面新建一个JSON文件例如generate-ts-interface.json{ “id”: “generate-ts-interface”, “name”: “SQL转TypeScript接口”, “description”: “将SQL的CREATE TABLE语句自动转换为严谨的TypeScript接口定义包含字段类型映射、注释和可空标记。”, “prompt_template”: “你是一个专业的TypeScript开发者和数据库工程师。请将以下SQL表定义语句转换为精确、可直接使用的TypeScript接口定义。\n\n要求\n1. 字段类型映射VARCHAR/TEXT - string, INT/BIGINT - number, BOOLEAN - boolean, TIMESTAMP/DATETIME - Date, DECIMAL - number。\n2. 如果字段有NULL约束则在TS接口中设为可选属性添加?。\n3. 将SQL注释转换为TS的JSDoc注释。\n4. 接口名使用帕斯卡命名法PascalCase基于表名生成。\n5. 输出格式整洁只返回最终的TypeScript代码块。\n\nSQL语句\nsql\n{{sql}}\n”, “trigger_patterns”: [“create table”, “CREATE TABLE”, “sql”, “表结构”, “生成接口”], “category”: “code-generation”, “metadata”: { “author”: “开发者本人”, “version”: “1.0.1”, “compatibility”: [“cursor”, “claude-desktop”, “windsurf”] } }实操心得编写prompt_template是门艺术。我的经验是角色设定 明确要求 示例格式Few-shot。上面这个模板首先定义了AI的角色TypeScript专家和数据库工程师然后给出了5条非常具体、可操作的要求最后用 sql 代码块清晰地包裹了输入变量。这种结构化的提示词能极大提高AI输出结果的准确性和稳定性。避免使用模糊的指令如“请生成好的代码”。4. 客户端配置在Cursor等工具中连接你的MCP服务器服务器写好了技能也定义了接下来就是让AI助手能用到它。这里以目前对MCP支持较好的Cursor编辑器为例。4.1 配置Cursor连接本地MCP服务器Cursor可以通过配置文件来声明使用哪些MCP服务器。配置文件通常位于macOS/Linux:~/.cursor/mcp.jsonWindows:%USERPROFILE%\.cursor\mcp.json如果文件不存在就创建一个。配置内容如下{ “mcpServers”: { “codex-skills”: { “command”: “node”, “args”: [ “/ABSOLUTE/PATH/TO/YOUR/PROJECT/codex-skills-mcp-server/server.js” ], “env”: { “NODE_ENV”: “production” } } } }重要提示args中的路径必须是绝对路径。相对路径在Cursor的上下文中可能无法正确解析。保存配置文件后需要完全重启Cursor。重启后你可以通过Cursor的快捷键通常是Cmd/Ctrl Shift I打开命令面板输入“MCP”来查看已连接的服务器。如果配置成功你应该能看到codex-skills服务器以及它提供的工具列表。4.2 在编辑器中调用技能配置成功后使用方式因客户端而异。在Cursor中通常有以下几种方式直接通过“/”命令在编辑器中输入/会触发命令菜单你可能会看到你定义的技能名称如generate-ts-interface出现在列表中。选择它然后按照提示输入所需的参数如SQL语句。通过Chat面板在Cursor的AI聊天面板中你可以直接输入自然语言例如“帮我用那个SQL转TS接口的技能处理一下这个表定义”。Cursor的AIClaude在理解了你的意图后会在后台通过MCP协议调用对应的工具。智能触发未来展望这是我们设计trigger_patterns字段的初衷。一个理想的客户端实现应该能监控用户的输入当检测到用户正在写CREATE TABLE语句时自动在侧边栏或悬浮窗中提示“检测到您在编写SQL有一个‘SQL转TS接口’技能可用是否调用” 这需要客户端更深度的集成也是MCP协议潜力巨大的地方。踩坑记录在早期测试时我遇到了一个常见问题——Cursor重启后MCP服务器未加载。90%的原因出在配置文件路径错误或JSON格式错误上。一个排查技巧是打开Cursor的开发者工具Help - Toggle Developer Tools在控制台里查看日志。MCP相关的连接错误通常会打印在那里。另外确保你的Node.js脚本在命令行下能独立运行成功node server.js这是排查服务器端问题的第一步。5. 技能分享与生态构建个人使用固然好但“支持分享”才是这个项目的精髓。我设计了两种分享方式5.1 方式一技能包导出与导入我编写了一个简单的脚本可以将skills目录打包成一个单独的skills-bundle.json文件。// scripts/exportSkills.js const fs require(‘fs’).promises; const path require(‘path’); async function exportSkills() { const skillsDir ‘./skills’; const outputFile ‘./skills-bundle.json’; try { const files await fs.readdir(skillsDir); const skillFiles files.filter(f f.endsWith(‘.json’)); const bundle []; for (const file of skillFiles) { const content await fs.readFile(path.join(skillsDir, file), ‘utf8’); bundle.push(JSON.parse(content)); } await fs.writeFile(outputFile, JSON.stringify(bundle, null, 2)); console.log(技能包已导出至: ${outputFile}包含 ${bundle.length} 个技能); } catch (error) { console.error(‘导出失败:’, error); } } exportSkills();其他人拿到这个skills-bundle.json文件后可以运行对应的导入脚本将其解压到自己的skills目录中。// scripts/importSkills.js const fs require(‘fs’).promises; const path require(‘path’); async function importSkills(bundlePath) { const skillsDir ‘./skills’; await fs.mkdir(skillsDir, { recursive: true }); const bundleData await fs.readFile(bundlePath, ‘utf8’); const skills JSON.parse(bundleData); for (const skill of skills) { const fileName ${skill.id}.json; const filePath path.join(skillsDir, fileName); await fs.writeFile(filePath, JSON.stringify(skill, null, 2)); console.log(已导入技能: ${skill.name}); } console.log(‘全部技能导入完成’); } // 使用方式: node importSkills.js ./path/to/skills-bundle.json const bundlePath process.argv[2]; if (!bundlePath) { console.error(‘请指定技能包文件路径。示例: node importSkills.js ./skills-bundle.json’); process.exit(1); } importSkills(bundlePath);这种方式简单直接适合小范围团队共享或社区分享。5.2 方式二Git仓库作为技能源进阶更优雅的方式是将技能定义文件存放在一个Git仓库中。MCP服务器在启动时可以配置一个或多个Git仓库地址作为远程技能源。服务器增强设计在配置中增加remoteRepos字段指向包含技能JSON文件的Git仓库URL或本地路径。服务器启动时使用simple-git等库拉取或更新远程仓库。将远程仓库的技能文件与本地技能文件合并加载可设置优先级如本地技能覆盖远程同名技能。优点版本管理技能的迭代更新可以通过Git的提交历史来管理。协作方便团队成员可以通过Pull Request来贡献新的技能或改进现有技能。一键更新用户只需更新MCP服务器配置或重启服务即可获取最新的技能集合。配置示例 (config.json):{ “localSkillsDir”: “./skills”, “remoteSkillRepos”: [ “https://github.com/your-org/awesome-codex-skills.git” ] }个人体会从“可分享”到“易分享”中间隔着巨大的体验鸿沟。最初我只是手动复制JSON文件后来发现版本冲突和更新通知是痛点。转向Git仓库方案后不仅管理变得规范更重要的是形成了一个潜在的“技能市场”雏形。想象一下未来可能有专门的网站收录和评级这些开源技能包就像VS Code的插件市场一样。6. 常见问题与排查技巧实录在实际开发和使用的过程中我遇到了不少问题。这里把典型问题和解决方案列出来希望能帮你省点时间。6.1 服务器启动失败或连接错误问题现象可能原因排查步骤与解决方案Cursor重启后提示MCP连接错误1.mcp.json配置文件路径错误或格式错误。2. Node.js脚本本身有语法错误或依赖缺失。3. 系统环境变量问题找不到node命令。1.检查配置文件使用JSON验证工具检查~/.cursor/mcp.json格式。确保路径是绝对路径且转义正确Windows下注意反斜杠。2.独立运行测试在终端中切换到项目目录直接运行node /ABSOLUTE/PATH/server.js。观察是否有错误输出。确保所有依赖已安装 (npm install)。3.查看Cursor日志在Cursor中打开开发者工具控制台过滤“MCP”关键词查看详细的错误信息。服务器进程意外退出技能文件JSON格式错误导致服务器加载时抛出未捕获的异常。1. 在skillLoader.js的loadSkill方法中我已经添加了try-catch来捕获单个文件的错误防止一个坏文件拖垮整个服务。确保你的代码也有类似的容错机制。2. 检查skills/目录下的每个.json文件可以用jq . filename.json命令验证格式。工具列表为空技能加载目录不正确或技能文件未被识别。1. 确认server.js中SkillLoader实例化的目录参数与实际情况一致。2. 确认技能文件后缀名为.json。3. 在服务器启动日志中查看已加载 X 个技能的输出。6.2 技能调用无效果或结果不佳问题现象可能原因排查步骤与解决方案在Cursor中调用了技能但AI回复与预期不符1. 提示词模板 (prompt_template) 指令不够清晰。2. 传入的参数 (arguments) 格式不对未能正确替换模板中的变量。3. 客户端Cursor没有正确地将MCP返回的提示词送入AI上下文。1.优化提示词这是最常见的原因。在模板中给予AI更明确的角色、更具体的任务步骤和输出格式要求。可以参考OpenAI的Prompt Engineering最佳实践。2.调试模板渲染在服务器的_renderTemplate方法中添加日志打印出替换前后的模板确认变量替换是否正确完成。3.检查客户端确认你使用的AI模型如Claude 3.5 Sonnet有能力处理该任务。有时需要换一个更强大的模型。技能调用后AI回复“我不知道如何执行此操作”MCP服务器返回的content格式不符合客户端预期。确保tools/call请求处理器返回的content字段是一个数组且内部对象结构正确例如{type: ‘text’, text: ‘…’}。严格遵循MCP协议的定义。触发模式 (trigger_patterns) 不工作客户端如Cursor尚未实现或未启用基于关键词的智能触发功能。目前这更多是一个“未来兼容性”字段。你可以向客户端的开发团队反馈该需求。现阶段主要通过“/”命令或聊天指令来手动调用技能。6.3 性能与扩展性考量当技能数量增加到几十上百个时需要关注启动速度每次启动都同步读取所有技能文件可能会变慢。可以考虑实现缓存机制或者改为懒加载只在tools/list被调用时扫描目录。内存占用所有技能对象都保存在内存的Map中。对于大量技能需要关注其内存增长。通常文本类型的技能定义内存占用很小无需过度优化。技能冲突如果从多个源本地、远程Git加载技能可能出现ID重复。我的处理策略是“后来者优先”后加载的技能会覆盖先加载的同ID技能并在控制台给出警告。更复杂的系统可以引入命名空间如org.team.skill-id。一个实用的调试技巧我为服务器添加了一个简单的HTTP状态端点仅在开发模式开启可以通过curl http://localhost:8080/status查看已加载的技能数量、ID列表和服务器健康状态。这对于部署后的问题排查非常有用。7. 未来展望与扩展思路把这个基础版本跑通后我脑子里又冒出了很多可以优化的点子。如果你也感兴趣不妨一起尝试。1. 技能市场与发现平台这是最激动人心的方向。我们可以建立一个中心化的网站开发者可以提交自己的技能包Git仓库链接用户可以搜索、评分、一键安装。网站后端可以提供简单的API让MCP服务器能够直接从平台拉取技能索引。这就像Homebrew for Codex Skills。2. 技能组合与工作流单个技能能力有限但多个技能串联起来就能完成复杂任务。例如可以设计一个“重构助手”工作流先调用“代码分析”技能理解代码结构再调用“提取函数”技能进行重构最后调用“生成测试”技能补充单元测试。MCP协议本身支持工具的顺序调用这为工作流编排提供了可能。3. 技能效果评估与A/B测试如何判断一个技能提示词写得好不好可以引入一个评估框架针对同一个任务如“生成SQL查询”用不同的技能提示词A/B版本多次调用AI根据输出结果的准确性、完整性、简洁性进行自动或人工评分从而持续优化提示词。4. 客户端UI深度集成目前调用技能还比较“原始”。理想的客户端应该提供一个技能面板以卡片形式展示所有可用技能配有搜索、分类、收藏功能。点击技能卡片可以预览其描述和所需参数并提供一个漂亮的表单供用户输入。这需要客户端做更多定制开发但用户体验会提升一个量级。这个项目始于一个简单的需求但做着做着我发现它打开了一扇新的大门如何将人类在AI编程中积累的隐性知识即“技能”标准化、资产化、可流通化。MCP协议提供了一个绝佳的底层管道。目前它虽然只是一个粗糙的雏形但已经能实实在在地提升我的开发效率。至少我再也不用到处翻找那个“把Swagger JSON转成Zod模式”的提示词片段了。如果你也有类似的痛点不妨基于这个思路动手试试或者直接拿我的代码去用。期待看到更有趣的技能和更好的实现方式出现。