资讯详情 前端Skill能力建模与MCP协议实战指南
📅 2026/10/25 10:02:25
1. 项目概述一个前端工程师的“技能栈”与“能力协议”双线进化实录我干前端这行快八年了从 jQuery 时代切图仔一路走到现在写 React 微前端、搭 Playwright 自动化流水线、对接 AI Agent Skill 接口的“全栈式前端”。最近半年我明显感觉到一个变化光会写组件、调 API、搞性能优化已经不够用了。团队里开始频繁出现两个词——Skill和MCP它们不是新造的黑话而是真实嵌入到我们日常开发流程里的新能力单元。比如上周产品提了个需求“用户在 H5 直播页点击‘实时字幕’按钮要自动调用讯飞语音转写 SDK并把流式结果渲染成滚动字幕同时支持断网重连和低延迟缓冲。”——这个需求拆解下来前端要做的远不止 DOM 操作得封装一个可复用的SpeechToTextSkill定义它的输入 Schema音频流/设备 ID/语言、输出 Schema文本片段/时间戳/置信度、执行生命周期init → start → pause → resume → destroy还得通过 MCP 协议跟后端调度服务通信告诉它“我这边准备好了可以下发语音流了”。这就是标题里“在职前端 Skill 和 MCP 分享”的真实背景它不是讲概念不是画大饼而是我在真实业务场景中把 Skill 当作可插拔的原子能力模块、把 MCP 当作跨系统能力调用的统一契约一步步落地的过程。核心关键词前端是领域锚点Skill是能力封装范式MCP是通信协议标准而playwright-cli和ego-browser则是我在自动化测试和浏览器环境模拟中实际用到的工具链。你不需要是架构师只要写过 React/Vue、跑过 E2E 测试、配过 Webpack就能看懂这里每一步为什么这么选、踩过哪些坑、怎么绕过去。这篇文章就是我整理的“在职升级手记”不讲虚的只讲我每天打开 IDE 后真正在敲的代码、改的配置、调的接口。2. Skill 是什么不是插件不是 SDK而是一种前端能力建模方法2.1 从“函数封装”到“Skill 建模”一次认知升级刚听到 Skill 这个词时我第一反应是“哦又是个插件”。但很快发现不对劲。我们之前写的utils/date.js里有个formatDate函数它接受 date 和 format 字符串返回格式化后的字符串——这叫工具函数我们接入的aliyun-oss-sdk提供putObject、getObject等方法操作对象存储——这叫 SDK而 Skill 的定位完全不同。它要求你明确回答三个问题这个能力的边界在哪比如“直播字幕”Skill它的输入只能是音频流或麦克风设备 ID输出只能是带时间戳的文本片段。它不负责 UI 渲染那是组件的事也不负责网络重试那是底层 SDK 的事更不负责权限申请那是 App Shell 的事。它的职责被严格限定在“语音转文本”这一件事上。这个能力的生命周期如何管理它必须有init()初始化资源如加载 WASM 模型、start()开始监听音频、pause()暂停但保持上下文、destroy()释放内存、关闭 WebSocket。这比一个普通函数复杂得多但它让能力具备了状态可管理、可中断、可复用的特性。这个能力如何被外部发现和调用它不能靠import { SpeechToTextSkill } from ./skills/speech-to-text这种硬编码路径调用。它需要注册到一个全局 Skill Registry暴露一个标准化的元信息name: speech-to-text, version: 1.2.0, inputSchema: {...}, outputSchema: {...}让任何系统前端页面、Playwright 脚本、甚至桌面 Electron 应用都能通过SkillRegistry.get(speech-to-text)动态获取并调用。提示Skill 不是技术栈而是一种设计契约。它强制你把“功能”从“代码”中抽象出来变成可描述、可验证、可替换的实体。就像乐高积木每一块都有标准凸点和凹槽拼装时只关心接口不关心内部怎么造。2.2 Skill 编码规范实战以skill-encoding-193为例网络热词里反复出现的skill编码193、skill编码247其实是内部 Skill 注册中心分配的唯一 ID。我们团队约定所有 Skill 必须遵循一套轻量级编码规范确保跨项目复用。以skill-encoding-193即“GIS 空间分析 Skill”为例它的目录结构和关键文件如下src/skills/gis-analyze/ ├── index.ts // Skill 主入口导出 Skill 类 ├── schema.ts // 输入/输出 Schema 定义Zod 验证 ├── worker/ // 独立 Worker 线程执行 CPU 密集计算 │ ├── main.ts // Worker 入口 │ └── algorithms/ // 核心算法如缓冲区分析、叠加分析 ├── utils/ // 辅助工具坐标系转换、GeoJSON 处理 └── test/ // 单元测试 Playwright E2E 测试核心index.ts实现非常精简// src/skills/gis-analyze/index.ts import { Skill, SkillContext } from frontend/skill-core; import { analyzeBufferZone } from ./worker/main; import { InputSchema, OutputSchema } from ./schema; export class GISAnalyzeSkill extends Skill { static readonly id skill-encoding-193; static readonly name gis-buffer-analysis; static readonly version 1.0.0; // 定义输入输出 Schema供运行时校验 inputSchema InputSchema; outputSchema OutputSchema; async init(context: SkillContext) { // 初始化预加载 WebAssembly 模块建立 Worker 通信通道 this.worker new Worker(new URL(./worker/main.ts, import.meta.url)); this.worker.onmessage (e) { this.emit(output, e.data); }; } async start(input: z.infertypeof InputSchema) { // 校验输入 const validated InputSchema.parse(input); // 发送任务到 Worker this.worker.postMessage({ type: ANALYZE_BUFFER, payload: validated }); } async destroy() { this.worker?.terminate(); } }注意几个关键点SkillContext是 Skill 的“上下文容器”里面包含logger、config、eventBus等通用能力避免每个 Skill 重复造轮子emit(output, data)是 Skill 内部事件总线外部通过skill.on(output, handler)订阅实现解耦Worker 封装是必须的。GIS 计算极其耗 CPU直接在主线程跑会导致页面卡死。我们约定所有 CPU 密集型 Skill 必须用 Worker 隔离。2.3 Skill 与前端 SDK 的本质区别以“讯飞实时语音转写”为例热词里提到“讯飞实时语音转写大模型前端适配”这正是 Skill 思维落地的典型场景。讯飞官方 SDK 提供了createRecognizer()、start()、stop()等方法但它是面向“调用者”的 SDK不是面向“能力治理”的 Skill。我们做了三层封装第一层SDK 封装层xf-speech-sdk-wrapper.ts屏蔽原生 SDK 的浏览器兼容性差异如 Safari 对 Web Audio API 的限制、错误码映射把ERR_NO_PERMISSION统一转为PermissionDeniedError、基础重试逻辑。第二层Skill 适配层speech-to-text.skill.ts实现Skill抽象类将 SDK 方法映射为 Skill 生命周期init()→createRecognizer()start()→start()destroy()→stop()destroy()。重点是定义inputSchemaexport const InputSchema z.object({ deviceId: z.string().optional(), // 麦克风设备 ID language: z.enum([zh-CN, en-US]).default(zh-CN), enablePunctuation: z.boolean().default(true), maxDurationMs: z.number().min(1000).max(60000).default(30000), // 单次最长识别时长 });第三层业务组件层LiveSubtitle.vue在 Vue 组件中不再直接 import SDK而是import { SkillRegistry } from frontend/skill-registry; const speechSkill await SkillRegistry.get(skill-encoding-194); // 讯飞 Skill ID speechSkill.on(output, (data) { this.subtitles.push(data.text); }); speechSkill.start({ deviceId: this.selectedDeviceId });这样做的好处立竿见影当某天讯飞 SDK 升级导致 API 变更我们只需修改第二层speech-to-text.skill.ts所有使用它的组件直播页、会议页、教育课件页完全无感如果要切换成百度语音 SDK只需写一个新的baidu-speech.skill.ts注册为skill-encoding-247业务组件一行代码都不用改。3. MCP 是什么不是 RPC不是 REST而是前端能力的“服务发现协议”3.1 MCP 的诞生背景当 Skill 多到需要“找得到、调得对、管得住”Skill 解决了“能力怎么封装”的问题但没解决“能力怎么找、怎么调、怎么管”的问题。想象一下一个大型中台系统集成了 30 个 SkillGIS 分析、OCR 识别、语音转写、PDF 渲染、3D 模型加载……它们分散在不同 Git 仓库、不同 NPM 包、甚至不同团队维护。前端页面想用“PDF 渲染 Skill”它该 import 哪个路径版本该用 1.2.0 还是 2.0.0这个 Skill 是否支持当前浏览器有没有权限调用这些信息如果靠文档或口头约定不出三个月就乱成一锅粥。MCPModel Capability Protocol就是为了解决这个“能力治理”问题而设计的轻量级协议。它不是替代 HTTP 或 WebSocket 的传输层协议而是一套运行时能力发现与协商的标准。它的核心思想是每个 Skill 在注册时必须向 MCP 中心上报自己的元数据每个调用方在调用前必须先向 MCP 中心查询该 Skill 的可用性、兼容性、授权策略。我们团队的 MCP 中心是一个极简的前端服务纯 JS无后端依赖部署在 CDN 上URL 是https://mcp.example.com/v1。它只做三件事GET /skills返回所有已注册 Skill 的列表含 ID、名称、版本、浏览器兼容性、所需权限POST /skills/{id}/check传入当前环境信息UserAgent、Feature Detection 结果、用户角色返回该 Skill 在当前环境是否可用、需授权哪些权限POST /skills/{id}/invoke真正的调用入口MCP 中心负责路由、鉴权、日志、熔断。3.2 MCP 协议详解从playwright-cli到ego-browser的全链路验证热词里提到playwright-cli和ego-browser它们正是我们验证 MCP 协议的关键工具。Playwright 不仅用于 E2E 测试更是我们自动化 Skill 集成测试的主力ego-browser 是一个高度可定制的 Chromium 实例用来模拟各种受限浏览器环境如无摄像头、无麦克风、WebGL 关闭。3.2.1 MCP 的 Skill 注册流程一个 Skill 要被 MCP 中心发现必须在构建时生成mcp-manifest.json文件{ id: skill-encoding-193, name: gis-buffer-analysis, version: 1.0.0, description: 基于 Turf.js 的 GIS 缓冲区分析能力, entry: ./dist/skills/gis-analyze/index.js, browser: { minVersion: chrome/95, features: [webgl, wasm] }, permissions: [geolocation], inputSchema: { $ref: ./schema/input.json }, outputSchema: { $ref: ./schema/output.json } }构建脚本Vite 插件会自动扫描src/skills/**/mcp-manifest.json打包时将其注入到最终产物中并向 MCP 中心发起注册请求POST /register。注册成功后该 Skill 才会出现在/skills列表里。3.2.2 前端调用 MCP 的标准流程业务组件调用 Skill 的代码不再是直接new Skill()而是走 MCP 流程// src/composables/useSkill.ts import { MCPClient } from frontend/mcp-client; const mcp new MCPClient(https://mcp.example.com/v1); export async function useGISAnalyze() { // Step 1: 查询 Skill 可用性 const checkResult await mcp.check(skill-encoding-193, { userAgent: navigator.userAgent, features: { webgl: true, wasm: true }, permissions: { geolocation: granted } }); if (!checkResult.available) { throw new Error(Skill not available: ${checkResult.reason}); } // Step 2: 获取 Skill 实例MCP 中心返回一个代理对象 const skill await mcp.get(skill-encoding-193); // Step 3: 调用MCP 代理会处理加载、初始化、错误重试 return skill.invoke({ geometry: { type: Point, coordinates: [116.4, 39.9] }, radiusMeters: 500 }); }这个mcp.get()返回的不是原始 Skill 实例而是一个MCPProxy对象。它内部做了按需动态 import Skill 代码Code Splitting自动调用init()并等待完成将invoke()参数按inputSchema校验捕获异常并按 MCP 标准格式返回{ code: SKILL_EXECUTION_ERROR, message: ..., details: { ... } }记录调用日志用于后续分析 Skill 使用率、失败率。3.2.3playwright-cli如何驱动 MCP 自动化测试我们用 Playwright 编写了一套 MCP 自动化测试套件核心命令是playwright-cli mcp-test --skill-id skill-encoding-193。它的工作流程是启动一个 ego-browser 实例预设环境如禁用 WebGL、模拟低网速访问 MCP 中心/skills接口确认目标 Skill 存在调用/skills/{id}/check传入 ego-browser 的模拟环境参数验证兼容性判断是否正确加载一个最小化测试页面该页面只包含mcp.get()和invoke()调用截图、录屏、捕获 console 日志验证 Skill 执行结果是否符合预期如 GIS 分析返回的 GeoJSON 是否有效生成 HTML 报告标注每个环境下的测试通过/失败状态。这套测试让我们在发布新 Skill 版本前能快速验证它在 Chrome/Firefox/Safari/Edge 以及各种受限环境下的表现避免“本地 OK上线炸锅”的尴尬。4. 实操从零搭建一个可运行的 Skill MCP 前端工作流4.1 环境准备与工具链选型我们不追求“最新最潮”只选稳定、易调试、社区支持好的组合。以下是经过半年生产验证的工具链工具版本选型理由替代方案不推荐原因构建工具Vite 5.2极快的冷启动、优秀的 HMR、内置 TypeScript 支持插件生态成熟如vite-plugin-mcpWebpack配置复杂HMR 慢、Rollup缺乏开箱即用的 dev serverSkill 核心库frontend/skill-core1.3.0我们自研的轻量级 Skill 基类库5KB无外部依赖TypeScript 优先microsoft/skills太重绑定 Azure 生态、ai-skill-sdk文档差更新慢MCP 客户端frontend/mcp-client0.8.0纯前端实现支持离线缓存、重试、超时控制API 设计简洁自己手写 fetch易出错缺少标准错误处理测试框架Playwright 1.42跨浏览器、支持真实设备模拟、API 稳定playwright-cli可扩展性强Cypress不支持多 tab、对 iframe 支持弱、Jest无法测试真实浏览器行为浏览器模拟ego-browser 0.5.1基于 Chromium可精确控制 feature flags如--disable-webgl、权限--unsafely-disable-web-securityPuppeteer配置繁琐对非标准 flag 支持差注意ego-browser不是公开 npm 包而是我们基于 Chromium 定制的二进制。它通过playwright-cli的--browser-channel参数指定路径。如果你没有定制需求直接用 Playwright 内置的 Chromium 即可效果一样。4.2 创建第一个 Skillskill-encoding-194讯飞语音转写我们以热词“讯飞实时语音转写大模型前端适配”为蓝本创建一个可立即运行的 Skill。Step 1初始化 Skill 目录mkdir -p src/skills/speech-to-text cd src/skills/speech-to-text npm init -y # 安装讯飞 SDK假设已获得授权 npm install iflytek-speech-sdkStep 2编写 Schemaschema.tsimport { z } from zod; export const InputSchema z.object({ deviceId: z.string().optional(), language: z.enum([zh-CN, en-US]).default(zh-CN), sampleRate: z.number().int().min(8000).max(48000).default(16000), // 讯飞要求必须指定采样率否则识别不准 }); export const OutputSchema z.object({ text: z.string(), timestamp: z.number().int(), // 毫秒级时间戳 confidence: z.number().min(0).max(1).default(0.8), isFinal: z.boolean().default(false), // 是否为最终结果流式识别中可能为 false });Step 3实现 Skill 主体index.tsimport { Skill, SkillContext } from frontend/skill-core; import { InputSchema, OutputSchema } from ./schema; import { IFlyTekRecognizer } from iflytek-speech-sdk; export class SpeechToTextSkill extends Skill { static readonly id skill-encoding-194; static readonly name iflytek-speech-to-text; static readonly version 1.0.0; inputSchema InputSchema; outputSchema OutputSchema; private recognizer: IFlyTekRecognizer | null null; async init(context: SkillContext) { // 初始化讯飞 SDK需配置 appid、apiKey 等 this.recognizer new IFlyTekRecognizer({ appid: context.config.appid, apiKey: context.config.apiKey, apiSecret: context.config.apiSecret, // 这些配置从 MCP 中心下发或从环境变量读取 }); } async start(input: z.infertypeof InputSchema) { const validated InputSchema.parse(input); // 讯飞 SDK 的 start 方法返回 Promisevoid但实际是异步事件驱动 this.recognizer!.start({ deviceId: validated.deviceId, language: validated.language, sampleRate: validated.sampleRate, }); // 监听讯飞 SDK 的事件 this.recognizer!.onResult (result) { const output { text: result.text, timestamp: Date.now(), confidence: result.confidence, isFinal: result.isFinal, }; // 校验输出 Schema OutputSchema.parse(output); this.emit(output, output); }; this.recognizer!.onError (error) { this.emit(error, error); }; } async destroy() { this.recognizer?.stop(); this.recognizer null; } }Step 4生成 MCP Manifestmcp-manifest.json{ id: skill-encoding-194, name: iflytek-speech-to-text, version: 1.0.0, description: 讯飞语音转写 Skill支持流式识别, entry: ./dist/skills/speech-to-text/index.js, browser: { minVersion: chrome/85, features: [webaudio, mediarecorder] }, permissions: [microphone], inputSchema: ./schema/input.json, outputSchema: ./schema/output.json }Step 5注册到 MCP 中心在vite.config.ts中添加插件import { mcpPlugin } from frontend/vite-plugin-mcp; export default defineConfig({ plugins: [ mcpPlugin({ mcpCenterUrl: https://mcp.example.com/v1, // 构建时自动上传 manifest 并注册 }) ] });运行npm run build插件会自动将mcp-manifest.json发送到 MCP 中心完成注册。4.3 在业务页面中调用 Skill以一个直播 H5 页面为例src/views/LivePage.vuetemplate div classlive-container video refvideoRef autoplay muted / button clicktoggleSubtitles开启实时字幕/button div classsubtitles div v-foritem in subtitles :keyitem.timestamp classsubtitle-item {{ item.text }} /div /div /div /template script setup langts import { ref, onMounted, onUnmounted } from vue; import { useSkill } from /composables/useSkill; const videoRef refHTMLVideoElement | null(null); const subtitles refArray{ text: string; timestamp: number }([]); let speechSkill: any null; const toggleSubtitles async () { if (!speechSkill) { try { // 通过 MCP 获取 Skill speechSkill await useSkill(skill-encoding-194); // 订阅输出事件 speechSkill.on(output, (data: any) { subtitles.value.push({ text: data.text, timestamp: data.timestamp }); // 保持最多 5 条字幕滚动 if (subtitles.value.length 5) { subtitles.value.shift(); } }); // 开始识别传入麦克风设备 ID await speechSkill.invoke({ deviceId: default, // 实际项目中应从 getUserMedia 获取 language: zh-CN, }); } catch (error) { console.error(Failed to start speech skill:, error); alert(字幕功能不可用请检查麦克风权限); } } else { // 停止识别 await speechSkill.destroy(); speechSkill null; } }; onUnmounted(() { if (speechSkill) { speechSkill.destroy(); } }); /script这段代码的关键在于业务逻辑完全不知道讯飞 SDK 的存在它只和 Skill ID、输入输出 Schema 打交道。如果未来换成百度语音只需更换 MCP 中注册的skill-encoding-194实现页面代码零修改。4.4 Playwright 自动化测试实战创建测试文件tests/speech-to-text.spec.tsimport { test, expect } from playwright/test; test(speech-to-text skill should work in chrome, async ({ page }) { // Step 1: 访问 MCP 中心确认 Skill 存在 const mcpResponse await page.request.get(https://mcp.example.com/v1/skills); const skills await mcpResponse.json(); expect(skills.some((s: any) s.id skill-encoding-194)).toBe(true); // Step 2: 模拟环境检查 const checkResponse await page.request.post( https://mcp.example.com/v1/skills/skill-encoding-194/check, { data: { userAgent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36, features: { webaudio: true, mediarecorder: true }, permissions: { microphone: granted } } } ); const checkResult await checkResponse.json(); expect(checkResult.available).toBe(true); // Step 3: 加载测试页面一个只调用 speech skill 的简单 HTML await page.goto(http://localhost:5173/test-speech.html); // Step 4: 触发字幕功能 await page.getByRole(button, { name: 开启实时字幕 }).click(); // Step 5: 等待并验证字幕出现模拟 1 秒后收到第一条结果 await expect(page.locator(.subtitle-item)).toHaveCount(1); await expect(page.locator(.subtitle-item).first()).toContainText(你好); });运行npx playwright test tests/speech-to-text.spec.ts即可看到完整的测试过程。我们每天 CI 流水线都会跑这套测试确保所有 Skill 在主流浏览器下 100% 可用。5. 常见问题与避坑指南那些只有亲手踩过才懂的细节5.1 Skill 生命周期陷阱init()时机与资源泄漏问题现象在直播页面中用户频繁开关字幕功能一段时间后页面内存占用飙升Chrome DevTools 显示大量IFlyTekRecognizer实例未被释放。根本原因我们最初在init()中直接new IFlyTekRecognizer()但destroy()里只调用了recognizer.stop()没有调用recognizer.destroy()。讯飞 SDK 的destroy()方法会清理所有事件监听器和内部定时器而stop()只是暂停识别。解决方案强制约定所有 Skill 的destroy()方法必须做两件事1) 调用底层 SDK 的destroy()2) 将实例引用置为null。增加防御性检查在init()开头加if (this.recognizer) this.destroy();防止重复初始化。Playwright 测试覆盖在自动化测试中加入内存泄漏检测用page.evaluate(() performance.memory.usedJSHeapSize)对比开关 10 次前后的内存值。实操心得Skill 的init()和destroy()不是可选的而是必须严格配对的“资源守门员”。我们后来在frontend/skill-core库里加了一个ResourceGuardmixin自动帮你 track 所有new XXX()的实例并在destroy()时统一清理省去大量 boilerplate。5.2 MCP 兼容性检查失效Feature Detection 的坑问题现象MCP 中心返回available: true但实际调用 Skill 时抛出NotSupportedError: The request is not supported.。排查过程我们发现ego-browser模拟的navigator.mediaDevices对象其enumerateDevices()方法返回的deviceId是空字符串而讯飞 SDK 在start()时会尝试用这个空 ID 初始化音频源导致失败。根本原因MCP 的check接口只检查了mediarecorder: true这个布尔值但没检查navigator.mediaDevices的具体能力。Feature Detection 不能只看 API 存在还要看 API 的实际行为。解决方案细化 Feature Schema在mcp-manifest.json中browser.features改为对象形式features: { mediarecorder: { required: true, test: navigator.mediaDevices?.getUserMedia }, webaudio: { required: true, test: window.AudioContext } }MCP 中心增强检测逻辑/check接口收到请求后不仅检查navigator属性还会在沙箱环境中执行test字段的代码片段返回真实结果。前端兜底在useSkill()的checkResult后加一层await navigator.mediaDevices.getUserMedia({ audio: true })的实际调用失败则降级提示。5.3 Playwright 测试中的权限模拟ego-browser的正确用法问题现象playwright-cli mcp-test在 CI 环境中总是失败报错Permission denied for microphone。原因分析CI 服务器是无界面的 Linux 环境Playwright 默认启动的 Chromium 无法访问真实麦克风。我们误以为ego-browser的--unsafely-disable-web-security参数能绕过权限但实际上它只禁用 CORS不解决媒体设备权限。正确做法使用虚拟音频设备在 CI 服务器上安装pavucontrol和pulseaudio创建一个null-sink作为虚拟麦克风pactl load-module module-null-sink sink_namevirtual_micPlaywright 启动参数const browser await chromium.launch({ args: [ --use-fake-ui-for-media-stream, // 关键让浏览器自动授予媒体权限 --use-fake-device-for-media-stream, // 使用虚拟设备 --virtual-mic-devicevirtual_mic // 指定虚拟设备名 ] });测试代码中显式请求权限await page.evaluate(async () { // 在页面上下文中请求权限避免跨域问题 await navigator.permissions.query({ name: microphone }); });5.4 Skill 版本冲突当多个页面同时加载不同版本的同一 Skill问题现象A 页面加载了skill-encoding-1941.0.0B 页面加载了skill-encoding-1941.1.0两者共存时B 页面的invoke()调用偶尔会触发 A 页面的onOutput回调。根因我们最初用window.SkillRegistry作为全局单例所有 Skill 实例都注册到同一个对象上。当mcp.get(skill-encoding-194)被多次调用时它返回的是同一个 Skill 实例因为id相同但不同版本的 Skill 类可能有不同事件总线实现导致事件混杂。终极解法MCP 代理层隔离mcp.get()不再返回原始 Skill 实例而是返回一个MCPProxy每个MCPProxy拥有独立的事件总线和状态。版本感知加载mcp.get(skill-encoding-1941.1.0)和mcp.get(skill-encoding-1941.0.0)会加载不同的代码包即使 ID 相同也视为不同 Skill。强制版本语义化在mcp-manifest.json中id字段改为id: skill-encoding-1941.1.0彻底杜绝歧义。这个坑我们踩了整整两周。教训是Skill 的 ID 必须包含版本号或者 MCP 中心必须支持版本路由。没有版本的 Skill就像没有版本的 API注定混乱。5.5 前端防爬与 Skill 安全如何保护你的 Skill 不被滥用热词里提到“关于前端的一些安全技术操作怎么在前端进行防止爬虫防止查看页面源码”这和 Skill 安全直接相关。Skill 封装了业务核心逻辑如 GIS 分析、语音转写如果被恶意爬虫调用会造成 API 配额耗尽或敏感数据泄露。我们的防护组合拳MCP 层鉴权所有POST /skills/{id}/invoke请求必须携带Authorization: Bearer JWTJWT 由后端签发包含用户身份、Skill ID、有效期。Skill 内部校验在invoke()方法开头解析 JWT验证aud受众是否为当前 Skill IDexp是否过期。前端混淆用esbuild的--minify和--tree-shaking配合javascript-obfuscator对 Skill 代码进行轻量混淆不追求绝对安全只为