🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

skia-studio-mcp

Package Overview
Dependencies
Maintainers
1
Versions
2
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

skia-studio-mcp

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.

latest
Source
npmnpm
Version
0.2.1
Version published
Weekly downloads
23
-39.47%
Maintainers
1
Weekly downloads
 
Created
Source

skia-studio-mcp

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.mdset_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 publishpackage.json"private": true 是有意的安全网,防止在 决定发布前被误发);npm pack 产物已验证可安装可运行(见下)。

快速开始

1. 打开 Skia Studio 网页,保持这个 tab 开着。 桥连接靠网页里的 src/bridge/bridgeClient.ts 主动连过来(ws://127.0.0.1:8787)——本地开发 http://localhost:5185pnpm 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(见下方"已知取舍")再判定"没连",覆盖网页正在退避 重连的窗口。

本机代理注意事项(重要,踩过坑:见 ADR-16)

如果你本机开着 Surge / Clash / Charles 之类的代理软件,默认配置可能会把 ws://127.0.0.1:8787 这条本地回环连接也劫持走,导致网页连不上桥(或连上了但握手异常)。 ADR-16 的 F0 spike 实测踩过这个坑:本机代理会拦 localhost。排查方法:

  • 代理软件里把 127.0.0.1 / localhost 加进"直连 / 不代理"名单(Surge 叫 Bypass List / 跳过代理规则,其它代理软件术语类似)。
  • 或者临时关掉代理软件的"增强模式 / 系统代理"再试一次,确认是不是代理导致的,再回去精确配置 跳过规则。
  • 症状:网页控制台没有明显报错(WS 失败是网页桥的一等状态,静默重试,不刷屏),但徽标一直 是灰的、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 连接"提示文案里引导打开的 URLhttps://skia.xiaolin.work/studio/

SKIA_STUDIO_BRIDGE_PORT 传了非法值(非 1-65535 整数)时会在 stderr 打警告并回退默认端口, 不会让进程崩溃。

改端口要两边配对SKIA_STUDIO_BRIDGE_PORT 只改了 server 这一侧监听的端口;网页侧要用 ?bridgePort= URL query 参数指到同一个端口(见 src/bridge/bridgeClient.tsresolvePort),例如 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 的页面默认连 8788BRIDGE_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?(默认 falsetrue 时截图后把预览切回调用前的节点,见下)
set_preview_node单独切一次预览目标(不截图)——配合 render_frame 使用:先切过去让人看,再省略 nodeId 切回主输出nodeId?(省略=切回主输出)
create_node往图里新建一个节点,复用 UI「New」菜单同一套默认值type(11 种节点类型之一), name?, props?(该类型的初始属性覆盖,字段与 update_nodepatch 同一套)
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'), producerNodeIdnull = 断开)
delete_node删除一个节点nodeId, disconnectReferences?(默认 false:若节点仍被引用则拒绝并列出全部引用方;true 则先断开/清理全部引用再删)
set_main_node切换 graph.mainNodeId(渲染输出节点)nodeId
upsert_animation新建或整体替换一个事件触发 Animationanimation(整对象;id 省略=新建,给出且存在=整体替换,给出但不存在=报错)
delete_animation删除一个 AnimationanimationId
trigger_animation手动发射一个 Animation 自测效果(配合 render_frameanimationId(disabled 或所有 track 目标都被 binding 占用会明确报错)
upsert_binding新建或整体替换一个 Binding(input/control 驱动 uniform 或 nodeProp)binding(整对象,同 upsert_animation 的 upsert 语义;source.type'animation'/'expression' 会被拒绝,见下)
delete_binding删除一个 BindingbindingId
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_animationenabled: true);或者它全部 track 的 目标当前都被一个 enabled Binding 占用(docs/animation-model.md §2:binding 永远优先于 animation,需要先 delete_binding 或把 binding 设为禁用)。

import_asset 的两种入参分支

filePath 分支由 mcp-server 进程自己用 fs 读取绝对路径——文件字节从不进入 agent 的上下文, 按扩展名推断 mimeTypepng/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_projectassets[] 摘要化

project.assets[].dataUrl 是内联 base64,单个资源可达数 MB——原样返回会撑爆 Agent 的上下文。 read_project 在网页侧发送前就把每个 asset 的 dataUrl 换成摘要(src/bridge/bridgeCommands.tssummarizeAsset),保留其余字段,加三个新字段:

{ hasData: boolean, approxByteLength?: number, mimeType?: string }

approxByteLength 是从 base64 长度估算的字节数(约等于,不是精确值);hasData: false 表示该 asset 本来就没有 dataUrl(内置生成图,按 name 约定)。这个工具不会返回任何图片/视频/音频 二进制数据本身。

render_framerestorePreview

render_framenodeId 且该节点不是 renderPass 类型时,会像人手动点击那个节点一样切换 预览目标(setPreviewNode),再截图——这是有意的副作用,人会在网页上实时看到预览切换(见 01-产品原则.md #2)。默认 restorePreview: false 保留这个行为(切完不恢复,人接着看这个 节点)。传 restorePreview: true 会在截图完成后把预览切回调用前的节点(人依然会看到一次 "切过去又切回来",只是最终画面回到了原状)。

set_preview_node

单独把预览切到某个节点、或切回主输出(省略 nodeId),不带截图。跟 render_frame 内部切换 预览用的是同一个 store action,人会实时看到切换。典型用法:render_frame 检查完某个节点的输出后, 调 set_preview_node(不传 nodeId)把预览交还给人类正在看的主输出。

Resource:GLSL→SkSL 移植指南

除了 17 个工具,本包还注册了两个只读 MCP resource,其一是 skia-studio://guides/porting-glsl-to-sksl (另一个是 Agent 工作流指南,见下一节)。 内容就是仓库根 docs/porting-glsl-to-sksl.md——一份实测验证过的 GLSL(Shadertoy 风格)→AGSL/SkSL 移植规范(sampler2D vs uniform shadertexture()/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.jsonfiles 数组),运行时按 import.meta.url 相对路径读取;assets/ 目录缺失时 resource 读取会优雅降级(返回一段错误说明, 不会抛异常),不影响下面的 hint 机制。

update_shader_sourcecompileStatus'error' 时,会检查真实编译诊断文本是否命中典型 GLSL 症状(症状表见 src/glslHints.tsGLSL_SYMPTOMS,每条都摘自这份指南、附一句 SkSL 写法要点),命中就在返回 JSON 里附一个 hint 字段,指出这像 GLSL 习惯用法 + 对应 SkSL 写法要点

  • 提示去读这个 resource。

Resource:Agent 工作流指南

第二个只读 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 完全一致。

发布到 MCP Registry

(发布前提:包已 npm publish 到 npm 公共 registry——MCP Registry 只托管元数据,不托管制品。 mcp-server/package.json"private": true 是防误发安全网,npm publish 前需要人工摘除, 见下面「发布前检查清单」。)

  • 安装 mcp-publisher CLI(brew install mcp-publisher,或从 releases 下载对应平台的预编译 二进制)。
  • 确认 package.jsonmcpName 字段("io.github.lixiaolin94/skia-studio")与仓库根 mcp-server/server.jsonname 字段完全一致——这是 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 的顶层 versionpackages[0].version 都要跟 npm 上新发布的 package.json 版本号一致(三者必须同步),改完重新跑 mcp-publisher publish

依据:Publisher CLI 命令参考Quickstart: Publish an MCP ServerPackage 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 干净退出)。
  • README 的"尚未发布"措辞已全部更新。
  • MCP Registry 注册完成(2026-07-14):mcp-publisher login github && mcp-publisher publish, registry API 已可检索到 io.github.lixiaolin94/skia-studio@0.2.0。发布清单就此全部完成。

独立启动(调试用,不经 Claude Code)

node dist/index.js

stderr 会打印监听地址与连接事件;stdout 保留给 MCP JSON-RPC 帧,不会有任何人类可读输出。

端到端契约冒烟:pnpm verify:mcp

仓库根有一条 pnpm verify:mcpscripts/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 之后回根目录跑

已知取舍 / 限制

  • 不做 token 鉴权:仅本机回环(127.0.0.1)+ Origin 白名单(默认 skia.xiaolin.work / localhost / 127.0.0.1,可用 SKIA_STUDIO_EXTRA_ORIGINS 追加)。与 ADR-16 一致,MVP 阶段的 显式取舍。
  • 无 Origin 头的连接放行:真实浏览器跨源 WS 握手总带 Origin 头,白名单挡的是"恶意网页"这个 攻击面;非浏览器客户端(测试脚本等)可能不带 Origin 头,这里选择放行而不是拒绝——安全边界仍然是 "只绑 127.0.0.1"。见 src/wsBridge.ts 顶部注释。
  • render_frame 没有尺寸参数:早期版本声明过 width/height 但实现一直忽略它们(陷阱), 已经从 inputSchema 里彻底删掉。既有的 screenshot() / renderPassScreenshot() 截图能力不 接受尺寸参数,输出永远是网页当前 canvas 尺寸(project.preview.canvas,见 read_project)。
  • 同一时刻只服务一个浏览器 tab 连接:新连接会顶掉旧连接(多开 tab / 刷新页面时符合直觉)。 旧 tab 收到顶替通知(自定义 WS close code)后会停止自动重连并把徽标置灰,不会跟新 tab 轮流互踢——但也意味着旧 tab 从此不再是桥的连接方,除非你手动刷新那个 tab。
  • 工具调用到达但网页还没连上时,先等最多 8s(每 500ms 轮询一次) 再返回 "No browser tab connected" 指引,覆盖网页正在指数退避重连(1s 起步、上限 10s)的窗口;8s 仍没连上才真的判定为"没连"。
  • 协议类型物理上仍是两份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 区块的文本必须逐字节 相同、桥端口常量数值必须相同,任一不一致就让构建失败。
  • npm 与 MCP Registry 均已发布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 可检索)。
  • Safari / Firefox 未测:F0/F1/F2 的端到端实测都在 Chrome(Playwright channel: 'chrome') 上做的;ADR-16 提到的"PNA 政策可能变化"约束尚未在其它浏览器复核。

Keywords

mcp

FAQs

Package last updated on 14 Jul 2026

Did you know?

Socket

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.

Install

Related posts