
Company News
AWS Security Hub Adds Socket for Supply Chain Security
Socket is now in the AWS Security Hub Extended plan. Adopt it through AWS, apply committed spend, and block malicious open source packages.
skia-studio-mcp
Advanced tools
MCP stdio server that bridges an AI agent to a live Skia Studio browser tab for AGSL/SkSL shader authoring, uniform/graph editing, animation & binding control, and screenshot-based visual verification.
MCP stdio server,把一个 Agent(Claude Code 等 MCP client)桥接到一个正在运行的 Skia Studio
浏览器 tab。最初四个核心工具(read_project/update_shader_source/set_uniform/render_frame)
的语义、Origin 校验义务定死于 ../docs/team/briefs/F1-mcp-bridge.md;协议草案与 F0 实测数据见
../spike/mcp-bridge-poc/README.md。set_preview_node、GLSL→SkSL 移植 resource、服务端可配置
环境变量、图编辑五工具(create_node/update_node/connect_nodes/delete_node/
set_main_node)、以及动画/绑定/资产七工具(upsert_animation/delete_animation/
trigger_animation/upsert_binding/delete_binding/import_asset/remove_asset)是之后按
「MCP 发布前批次」追加的能力,语义以本 README 与 src/index.ts 里每个工具的 description 为准;
这十二个工具的权威校验都在网页侧 src/bridge/bridgeCommands.ts(见该文件顶部注释),本包的 zod
inputSchema 只把关形状。
Agent(Claude Code) ↔ stdio(MCP) ↔ 本包(skia-studio-mcp) ↔ ws://127.0.0.1:8787 ↔ 网页桥模块(../src/bridge/)
这是独立于主 app 的顶层包:自己的 package.json / pnpm-lock.yaml / node_modules,不进根
pnpm-workspace(仓库根本来就没有 workspace 文件),pnpm install 只影响这个目录,不碰根
lockfile。当前未 npm publish(package.json 里 "private": true 是有意的安全网,防止在
决定发布前被误发);npm pack 产物已验证可安装可运行(见下)。
1. 打开 Skia Studio 网页,保持这个 tab 开着。 桥连接靠网页里的 src/bridge/bridgeClient.ts
主动连过来(ws://127.0.0.1:8787)——本地开发 http://localhost:5185(pnpm dev 起的端口),
或生产 https://skia.xiaolin.work/studio/。这个 tab 必须留在前台标签页里(不用切到 Editor/
Preview 视图也行,但 render_frame 需要三区视图才有活的渲染器,见下)。同一时刻只服务一个
浏览器 tab:多开一个新 tab 会顶替旧 tab 的连接(旧 tab 的"Agent connected"徽标会变灰并停止
自动重连,不会跟新 tab 抢)。
2. 安装并构建本包:
cd mcp-server
pnpm install
pnpm build # 先跑 scripts/check-protocol-parity.mjs 校验协议一致,再 tsc -p tsconfig.json → dist/
3. 接入 Claude Code(.mcp.json)。 在仓库根(或任意项目目录)添加 .mcp.json:
npm 包(已发布 skia-studio-mcp@0.2.0,零克隆零构建,推荐):
{
"mcpServers": {
"skia-studio": {
"command": "npx",
"args": ["-y", "skia-studio-mcp"]
}
}
}
等价的一行命令:claude mcp add skia-studio -- npx -y skia-studio-mcp。
开发本仓库时用本地构建产物(免 npx 拉包,改完 pnpm build 即生效):
{
"mcpServers": {
"skia-studio": {
"command": "node",
"args": ["/绝对路径/skia-studio/mcp-server/dist/index.js"]
}
}
}
不想预构建,也可以直接用 tsx 跑源码(省一步 pnpm build,多一次 tsx 启动开销):
{
"mcpServers": {
"skia-studio": {
"command": "npx",
"args": ["-y", "tsx", "/绝对路径/skia-studio/mcp-server/src/index.ts"]
}
}
}
配好后重启 Claude Code(或重新加载 MCP 配置),打开网页,左下角状态栏应该在几秒内亮起 "Agent connected" 徽标——这条连接是网页主动连过去的,跟哪个先起没关系:Agent 侧先起、网页 还没连上时,工具调用会先原地等最多 8s(见下方"已知取舍")再判定"没连",覆盖网页正在退避 重连的窗口。
如果你本机开着 Surge / Clash / Charles 之类的代理软件,默认配置可能会把
ws://127.0.0.1:8787 这条本地回环连接也劫持走,导致网页连不上桥(或连上了但握手异常)。
ADR-16 的 F0 spike 实测踩过这个坑:本机代理会拦 localhost。排查方法:
127.0.0.1 / localhost 加进"直连 / 不代理"名单(Surge 叫 Bypass List /
跳过代理规则,其它代理软件术语类似)。read_project 等工具一直返回"No browser tab connected"——先怀疑代理,再怀疑
server 没起。启动 node dist/index.js(或 pnpm start)前可以设三个可选环境变量,都有默认值,不设就是
之前的行为:
| 变量 | 作用 | 默认值 |
|---|---|---|
SKIA_STUDIO_BRIDGE_PORT | 覆盖桥监听端口 | 8787 |
SKIA_STUDIO_EXTRA_ORIGINS | 逗号分隔的 hostname 列表,追加进 Origin 白名单(不是替换:skia.xiaolin.work/localhost/127.0.0.1 这三个默认值永远有效) | 空 |
SKIA_STUDIO_APP_URL | 覆盖"没有浏览器 tab 连接"提示文案里引导打开的 URL | https://skia.xiaolin.work/studio/ |
SKIA_STUDIO_BRIDGE_PORT 传了非法值(非 1-65535 整数)时会在 stderr 打警告并回退默认端口,
不会让进程崩溃。
改端口要两边配对:SKIA_STUDIO_BRIDGE_PORT 只改了 server 这一侧监听的端口;网页侧要用
?bridgePort= URL query 参数指到同一个端口(见 src/bridge/bridgeClient.ts 的
resolvePort),例如 server 用 SKIA_STUDIO_BRIDGE_PORT=9911 node dist/index.js 起在 9911,
网页就要打开 http://localhost:5185/?bridgePort=9911(或生产域名同理拼上这个 query)——两边
互不感知对方,改一边忘了改另一边,症状就是网页连不上桥(跟"本机代理劫持"长得很像,先看端口
是不是配对了再去查代理)。
SKIA_STUDIO_BRIDGE_PORT=9911 SKIA_STUDIO_EXTRA_ORIGINS=my-preview.example.com node dist/index.js
开发隔离(默认行为,仅开发者关心,线上用户无感知):本地 pnpm dev 的页面默认连
8788(BRIDGE_DEV_DEFAULT_PORT,见 src/bridge/bridgeProtocol.ts),生产构建默认 8787——
dev tab 与生产 tab 天然各连各的桥,不互相顶替,agent 不会静默连错工程(read_project 回包的
connectedTab.url 让 agent 说出自己连的是谁,页面紫色指示器让人一眼看到桥在哪个 tab)。
开发本仓库时在 .mcp.json 注册两个 server 即可同时驱动两边:
{
"mcpServers": {
"skia-studio": { "command": "npx", "args": ["-y", "skia-studio-mcp"] },
"skia-studio-dev": { "command": "npx", "args": ["-y", "skia-studio-mcp"],
"env": { "SKIA_STUDIO_BRIDGE_PORT": "8788" } }
}
}
?bridgePort= URL 参数仍可覆盖两种构建的默认端口(比如让 dev 页面临时连生产桥)。
| 工具 | 何时用 | 关键参数 |
|---|---|---|
read_project | 改之前先看工程当前形状;改完确认结果 | 无。project.assets[] 已摘要化,见下 |
update_shader_source | 改一个 runtimeShader 节点的 AGSL/SkSL 源码,拿回真实编译诊断 | nodeId, code(完整替换,非 diff)。compileStatus: 'error' 时若诊断像典型 GLSL 写法,回包会带一个 hint 字段,见下「Resource」 |
set_uniform | 调一个 uniform 的当前值 | nodeId, uniformName, value: number[] |
render_frame | 截图看效果——以 MCP image content 返回,Claude 等多模态 Agent 直接"看到"图;配一段 JSON 文本(byteLength/width/height/nodeId) | nodeId?(省略=主输出), restorePreview?(默认 false;true 时截图后把预览切回调用前的节点,见下) |
set_preview_node | 单独切一次预览目标(不截图)——配合 render_frame 使用:先切过去让人看,再省略 nodeId 切回主输出 | nodeId?(省略=切回主输出) |
create_node | 往图里新建一个节点,复用 UI「New」菜单同一套默认值 | type(11 种节点类型之一), name?, props?(该类型的初始属性覆盖,字段与 update_node 的 patch 同一套) |
update_node | 改一个已存在节点自身的标量/枚举属性(不是连线、不是源码、不是 uniform 值) | nodeId, patch(按节点类型有各自的合法字段表,见 src/index.ts 里该工具的 description) |
connect_nodes | 把一个节点的输出接进另一个节点的输入槽,或断开 | consumerNodeId, slot(按 consumer 类型:runtimeShader 用 childSlot 的 uniformName;shaderPass/blur/dropShadow 固定 'input';renderPass 固定 'shader';feedback 固定 'source'), producerNodeId(null = 断开) |
delete_node | 删除一个节点 | nodeId, disconnectReferences?(默认 false:若节点仍被引用则拒绝并列出全部引用方;true 则先断开/清理全部引用再删) |
set_main_node | 切换 graph.mainNodeId(渲染输出节点) | nodeId |
upsert_animation | 新建或整体替换一个事件触发 Animation | animation(整对象;id 省略=新建,给出且存在=整体替换,给出但不存在=报错) |
delete_animation | 删除一个 Animation | animationId |
trigger_animation | 手动发射一个 Animation 自测效果(配合 render_frame) | animationId(disabled 或所有 track 目标都被 binding 占用会明确报错) |
upsert_binding | 新建或整体替换一个 Binding(input/control 驱动 uniform 或 nodeProp) | binding(整对象,同 upsert_animation 的 upsert 语义;source.type 为 'animation'/'expression' 会被拒绝,见下) |
delete_binding | 删除一个 Binding | bindingId |
import_asset | 导入图片/视频/音频资产(只入库,不建节点) | filePath?(mcp-server 侧读,≤25MB)或 dataBase64?+mimeType(agent 生成的小资源,≤~2MB),二选一 |
remove_asset | 删除一个资产 | assetId, disconnectReferences?(默认 false:被 imageShader/videoShader/audio 输入引用则拒绝并列出引用方) |
面向 Agent 的完整语义文案见 src/index.ts 里每个工具的 description(那是产品文案,不是这里
的摘要)。图编辑五工具(create_node/update_node/connect_nodes/delete_node/
set_main_node)与动画/绑定两个 upsert/delete 工具(upsert_animation/delete_animation/
upsert_binding/delete_binding)的每次 mutation 回包都可能带一个 verifierErrors 数组:只在该
操作之后工程整体出现 fatal/error 级诊断时才出现(如工程原本就有 fatal 级问题——比较少见的边缘
情况),不阻塞该次调用本身的成功返回,纯供 Agent 感知"工程整体状态是否仍然健康"。
upsert_binding 拒绝 source.type: 'animation' | 'expression'BindingSource 的 schema 保留了 animation/expression 两个分支(见
docs/runtime-binding-model.md §2.1),但 Runtime v1 完全不求值它们——如果 upsert_binding 静默
接受这类 binding,agent 会以为它生效了,实际上这个 binding 永远不会驱动任何东西。所以这两个
source.type 会被明确拒绝,错误文案指向该文档;目前只支持 'input'(驱动的 input 需已存在于
project.inputs)与 'control'(controlId 需已存在于 project.controls)。
trigger_animation 与 disabled / binding 占用trigger_animation 复用 AnimationPanel.tsx 里 Play 按钮同一条路径
(CanvasKitRenderer.triggerManualAnimation),语义是"让这个 animation 的触发事件现在发生",
不管它自己配置的 trigger.type 是什么。两种情况会被挡住并给出明确文案而不是静默 no-op:
animation 本身 enabled: false(先 upsert_animation 传 enabled: true);或者它全部 track 的
目标当前都被一个 enabled Binding 占用(docs/animation-model.md §2:binding 永远优先于
animation,需要先 delete_binding 或把 binding 设为禁用)。
import_asset 的两种入参分支filePath 分支由 mcp-server 进程自己用 fs 读取绝对路径——文件字节从不进入 agent 的上下文,
按扩展名推断 mimeType(png/jpg/jpeg/webp/avif/gif 图片、mp4/m4v/webm/mov 视频、
mp3/wav/ogg/m4a/aac/flac 音频,见 docs/media-support-matrix.md),超过 25MB 拒绝并提示改走
网页 UI 的 Assets 面板。dataBase64 分支是给 agent 自己生成的小资源用的(比如程序化生成的贴图),
必须同时给 mimeType,解码后限制在 ~2MB(比 filePath 严格得多,因为这些字节确实经过了 agent 的
上下文)。两种分支最终都只把资产写入 project.assets,不自动建节点——把资产接到图里要另外调
create_node/update_node 设置 assetId。
read_project 的 assets[] 摘要化project.assets[].dataUrl 是内联 base64,单个资源可达数 MB——原样返回会撑爆 Agent 的上下文。
read_project 在网页侧发送前就把每个 asset 的 dataUrl 换成摘要(src/bridge/bridgeCommands.ts
的 summarizeAsset),保留其余字段,加三个新字段:
{ hasData: boolean, approxByteLength?: number, mimeType?: string }
approxByteLength 是从 base64 长度估算的字节数(约等于,不是精确值);hasData: false 表示该
asset 本来就没有 dataUrl(内置生成图,按 name 约定)。这个工具不会返回任何图片/视频/音频
二进制数据本身。
render_frame 的 restorePreviewrender_frame 传 nodeId 且该节点不是 renderPass 类型时,会像人手动点击那个节点一样切换
预览目标(setPreviewNode),再截图——这是有意的副作用,人会在网页上实时看到预览切换(见
01-产品原则.md #2)。默认 restorePreview: false 保留这个行为(切完不恢复,人接着看这个
节点)。传 restorePreview: true 会在截图完成后把预览切回调用前的节点(人依然会看到一次
"切过去又切回来",只是最终画面回到了原状)。
set_preview_node单独把预览切到某个节点、或切回主输出(省略 nodeId),不带截图。跟 render_frame 内部切换
预览用的是同一个 store action,人会实时看到切换。典型用法:render_frame 检查完某个节点的输出后,
调 set_preview_node(不传 nodeId)把预览交还给人类正在看的主输出。
除了 17 个工具,本包还注册了两个只读 MCP resource,其一是 skia-studio://guides/porting-glsl-to-sksl
(另一个是 Agent 工作流指南,见下一节)。
内容就是仓库根 docs/porting-glsl-to-sksl.md——一份实测验证过的 GLSL(Shadertoy 风格)→AGSL/SkSL
移植规范(sampler2D vs uniform shader、texture()/texelFetch() vs shader.eval()、
fwidth/dFdx/dFdy 屏幕空间导数缺失、gl_FragCoord vs main(coord) 参数、mainImage vs
main、precision 限定符、#define/预处理器差异等)。构建期由 scripts/copy-assets.mjs 把该
文档拷进 mcp-server/assets/(跟 dist/ 平级,见 package.json 的 files 数组),运行时按
import.meta.url 相对路径读取;assets/ 目录缺失时 resource 读取会优雅降级(返回一段错误说明,
不会抛异常),不影响下面的 hint 机制。
update_shader_source 在 compileStatus 是 'error' 时,会检查真实编译诊断文本是否命中典型
GLSL 症状(症状表见 src/glslHints.ts 的 GLSL_SYMPTOMS,每条都摘自这份指南、附一句 SkSL
写法要点),命中就在返回 JSON 里附一个 hint 字段,指出这像 GLSL 习惯用法 + 对应 SkSL 写法要点
第二个只读 MCP resource:skia-studio://guides/agent-workflow。内容是 guides/agent-workflow.md
(英文,源文件在本包 guides/ 目录下入库维护,assets/ 里的同名文件跟 porting guide 一样只是
构建产物)——面向 Agent 的实操指南:典型闭环(read_project → 改动 → render_frame)、按意图分组
的十七工具速查表、工程模型(图的边语义、uniform 取值在 binding/animation/currentValue 之间的优先级
真相)、正在被人实时观看这个 tab 时的礼仪、动画自测配方、资产导入建议、已知限制。构建期同样由
scripts/copy-assets.mjs 拷进 mcp-server/assets/,运行时读取与优雅降级模式跟 porting guide
完全一致。
(发布前提:包已 npm publish 到 npm 公共 registry——MCP Registry 只托管元数据,不托管制品。
mcp-server/package.json 里 "private": true 是防误发安全网,npm publish 前需要人工摘除,
见下面「发布前检查清单」。)
mcp-publisher CLI(brew install mcp-publisher,或从
releases 下载对应平台的预编译
二进制)。package.json 的 mcpName 字段("io.github.lixiaolin94/skia-studio")与仓库根
mcp-server/server.json 的 name 字段完全一致——这是 registry 验证 npm 包归属的机制(读
npm 包 package.json 里的 mcpName,必须等于 server.json.name)。用 GitHub 认证发布时,
name 必须以 io.github.<你的 GitHub 用户名>/ 开头。mcp-publisher login github(弹出 GitHub Device Flow:打开
https://github.com/login/device,输入终端里打出的一次性代码授权)。cd mcp-server && mcp-publisher publish(默认读同目录下的 server.json)。成功后可用
curl "https://registry.modelcontextprotocol.io/v0.1/servers?search=io.github.lixiaolin94/skia-studio"
确认。server.json 的顶层 version 与 packages[0].version 都要跟 npm 上新发布的
package.json 版本号一致(三者必须同步),改完重新跑 mcp-publisher publish。依据:Publisher CLI 命令参考、 Quickstart: Publish an MCP Server、 Package Types(npm 归属校验)。
许可证已确认(2026-07-14,笑林拍板):PolyForm Noncommercial 1.0.0——源码开放、 允许任何非商业用途(个人/教育/研究/非营利),不允许商业使用。
package.json的"license": "PolyForm-Noncommercial-1.0.0"(合法 SPDX 标识符)与仓库根 / 本目录的LICENSE.md(官方原文,取自 polyformproject/polyform-licenses)均已就位,npm pack已验证 LICENSE.md 随包分发。
清单执行状态(2026-07-14):
"private": true(笑林执行)。npm publish --access public —— skia-studio-mcp@0.2.0 已发布,npx -y skia-studio-mcp
线上冒烟通过(17 工具 + 2 resources 全注册,SIGTERM 干净退出)。mcp-publisher login github && mcp-publisher publish,
registry API 已可检索到 io.github.lixiaolin94/skia-studio@0.2.0。发布清单就此全部完成。node dist/index.js
stderr 会打印监听地址与连接事件;stdout 保留给 MCP JSON-RPC 帧,不会有任何人类可读输出。
pnpm verify:mcp仓库根有一条 pnpm verify:mcp(scripts/verify-mcp.mjs),一条命令跑通「真实 vite dev server +
真实本包 stdio 子进程 + 真实 @modelcontextprotocol/sdk Client + headless 系统 Chrome
(playwright-core,channel:'chrome',不是 Playwright 自带 Chromium)」的完整链路,逐个调用全部
17 个工具 + 读 2 个 resource,断言契约行为(工具/annotations 注册、GLSL hint、图编辑/动画/绑定/
资产的合法与非法路径、render_frame 截图等)。
什么时候跑:改了 Project Schema(src/core/projectSchema.ts)、src/bridge/**
(bridgeCommands.ts/bridgeProtocol.ts)、或任何被 bridgeCommands.ts 调用的 store action
(src/store/studioStore.ts)之后——这些改动不会碰本包的 TypeScript 类型检查,只有真实跑一遍
17 个工具才能知道 Agent 实际感知到的行为有没有被破坏。
前置:本包必须已构建(pnpm build,产出 dist/index.js);本机需要一份真实 Google Chrome。
脚本自己起独立的 vite/桥端口(不用默认的 5173/8787),跟你正开着的真实 dev server / mcp-server
互不干扰,可以直接在开发中跑,不用先关掉手头的服务。
cd .. && pnpm verify:mcp # 从仓库根跑;或直接 cd mcp-server && pnpm build 之后回根目录跑
127.0.0.1)+ Origin 白名单(默认 skia.xiaolin.work /
localhost / 127.0.0.1,可用 SKIA_STUDIO_EXTRA_ORIGINS 追加)。与 ADR-16 一致,MVP 阶段的
显式取舍。src/wsBridge.ts 顶部注释。render_frame 没有尺寸参数:早期版本声明过 width/height 但实现一直忽略它们(陷阱),
已经从 inputSchema 里彻底删掉。既有的 screenshot() / renderPassScreenshot() 截图能力不
接受尺寸参数,输出永远是网页当前 canvas 尺寸(project.preview.canvas,见 read_project)。src/protocol.ts(本包)与 ../src/bridge/bridgeProtocol.ts
(主 app)各自维护一份桥接 wire 格式类型,没有共享 npm 包(评估过抽共享包 / 跨包相对路径
import,两条路都要动 tsconfig(rootDir) 或引入 monorepo 工具,成本超过收益)。改用零依赖的
构建期一致性校验:pnpm build / pnpm typecheck 前会先跑
scripts/check-protocol-parity.mjs,两侧标了 shared-wire-protocol 区块的文本必须逐字节
相同、桥端口常量数值必须相同,任一不一致就让构建失败。skia-studio-mcp@0.2.0(2026-07-14,许可证
PolyForm Noncommercial 1.0.0,不允许商用),npx -y skia-studio-mcp 线上冒烟通过;
官方 Registry 记录 io.github.lixiaolin94/skia-studio(registry API 可检索)。channel: 'chrome')
上做的;ADR-16 提到的"PNA 政策可能变化"约束尚未在其它浏览器复核。FAQs
MCP stdio server that bridges an AI agent to a live Skia Studio browser tab for AGSL/SkSL shader authoring, uniform/graph editing, animation & binding control, and screenshot-based visual verification.
The npm package skia-studio-mcp receives a total of 17 weekly downloads. As such, skia-studio-mcp popularity was classified as not popular.
We found that skia-studio-mcp demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Company News
Socket is now in the AWS Security Hub Extended plan. Adopt it through AWS, apply committed spend, and block malicious open source packages.

Research
/Security News
Popular npm packages keyv and cacheable compromised.

Security News
A misconfiguration gave three Anthropic models internet access, and one, believing it was in a simulation, shipped a credential-stealing package to PyPI.