# io.github.lixiaolin94/skia-studio (npm · skia-studio-mcp)

MCP bridge to a live Skia Studio tab: AGSL/SkSL shader authoring, graph editing, screenshots.

- Trust score: 61/100 (medium)
- Change this week: +17
- Registry status: active
- Liveness: live
- Owner verified: no
- Last scored: 2026-08-03

## Components

- npm · `skia-studio-mcp`: 61/100 (this document), [markdown](https://verifymcp.io/servers/lixiaolin94-skia-studio/skia-studio-mcp.md), [page](https://verifymcp.io/servers/lixiaolin94-skia-studio/skia-studio-mcp)

## Channel facts

- Registry: `npm`
- Package: `skia-studio-mcp`
- Version: `0.2.1`
- Transport: `stdio`

## Trust breakdown

How this component scores in each security and reliability category. Every signal is checked automatically from public evidence about the published package, including repeated runs of it in an isolated sandbox, and we only credit what we can confirm. Scores are 0–100 per category. Scoring method: https://verifymcp.io/docs/scoring (what has changed: https://verifymcp.io/docs/scoring/changelog)

Scored 2026-08-03.

- **Supply Chain Security**: 87/100
  - No malware found by supply-chain analysis.
  - Only part of the dependency tree could be resolved (96 of 100), so this covers what we could see, not the whole tree.
  - No install/post-install scripts declared.
  - Only part of the dependency tree could be resolved (96 of 100), so this covers what we could see, not the whole tree.
- **Provenance & Transparency**: 6/100
  - Repository check failed: the declared repository URL returned HTTP 404.
  - Provenance check failed: no build-provenance attestation is published.
  - License check failed: the license (PolyForm-Noncommercial-1.0.0) isn't a recognized OSI-approved license.
  - Actively maintained (last published 19 days ago).
  - Security-disclosure policy not yet verified: we couldn't inspect the source repository.
- **Schema Quality & AI Usability**: 78/100
  - 100% of prompts and resources have a non-trivial description (not blank, and not just the item's name).
  - AI-judged instruction clarity (excellent).
  - Context-footprint check failed: tool/resource definitions use about 4548 tokens (~239/item across 19 items; 17 tools + 2 resources), over budget; trim descriptions and params.
  - Usage-examples check failed: none of the tools include examples.
- **Stability & Change Management**: 23/100
  - Stability observed for 7 of 30 days with no destabilising changes; credit accrues until the full window elapses.
- **Tool Coverage**: 98/100
  - 100% of tools have a non-trivial description (not blank, and not just the tool's name).
  - 93% of tool parameters carry a description.
- **Capabilities**: 100/100
  - Implements a supported MCP spec version (2025-11-25); the latest is 2026-07-28.

## Install

### Claude

```bash
claude mcp add lixiaolin94-skia-studio -- npx -y skia-studio-mcp
```

### Codex

```bash
codex mcp add lixiaolin94-skia-studio -- npx -y skia-studio-mcp
```

### opencode

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "lixiaolin94-skia-studio": {
      "type": "local",
      "command": [
        "npx",
        "-y",
        "skia-studio-mcp"
      ],
      "enabled": true
    }
  }
}
```

### OpenClaw

```bash
openclaw mcp add lixiaolin94-skia-studio --command npx --arg -y --arg skia-studio-mcp
```

### Hermes

```yaml
mcp_servers:
  lixiaolin94-skia-studio:
    command: "npx"
    args: ["-y", "skia-studio-mcp"]
```

### Other

```json
{
  "mcpServers": {
    "lixiaolin94-skia-studio": {
      "command": "npx",
      "args": [
        "-y",
        "skia-studio-mcp"
      ]
    }
  }
}
```

## Changelog

Every change recorded for this component, newest first. Days that predate change tracking, or that we cannot explain, say so: "we were watching and nothing happened" and "we were not watching" are different claims.

### 2026-08-02 (score 61, +42)

- [security regression] Provenance: unverified → fail
- [security improvement] Install scripts: unverified → pass
- [security improvement] Known CVEs: unverified → partial
- [security improvement] Malware scan: unverified → pass
- [functional regression] License: unverified → fail
- [functional improvement] Stability: unverified → 0.20
- [functional improvement] Maintenance: unverified → pass
- [functional improvement] MCP protocol: unverified → pass
- [functional improvement] Schema quality: unverified → excellent
- [functional improvement] Dependency health: unverified → partial
- [functional] Licence: PolyForm-Noncommercial-1.0.0

### 2026-07-31 (score 19, −25)

- [functional] We updated how we score, so this day's move reflects our rubric, not a change to the server

### 2026-07-27 (score 44)

First indexed and scored.

## MCP tools (17)

### `read_project` (~233 tokens)

Read Skia Studio project

Read the Skia Studio project currently open in the connected browser tab: the full project document (graph nodes, shader sources, uniforms, bindings, controls, preview/canvas config — the same SkiaStudioProjectDocument the app itself uses), plus per-node compileStatus (idle | compiling | ok | error) and diagnostics (real Skia compiler errors/warnings with line/column, the same ones shown in the Inspector). project.assets[] is summarized: each asset's dataUrl (which can be several MB of inline base64) is stripped and replaced with { hasData, approxByteLength, mimeType } — this tool never returns binary asset data. Call this first to see the project shape before editing anything, and again after a change to confirm the resulting state. The result includes connectedTab.url — the page address of the tab you are bridged to; report it to the human so they know whether you are editing their local dev instance or the production site (the bridge serves one tab, last to connect wins). Takes no arguments. If no browser tab is connected, returns guidance on what URL to open.

### `update_shader_source` (~325 tokens)

Update shader source and get real compile diagnostics

Replace the AGSL/SkSL source code of a runtimeShader graph node, then wait (up to 5s) for the real CanvasKit compiler to finish and return the resulting compileStatus (ok | error) and diagnostics — the exact compiler errors/warnings (with line/column) shown in the Skia Studio Inspector, not a mock. If compileStatus is 'error', read the diagnostics, fix the reported issues, and call this tool again with corrected code; the diagnostics are authoritative, do not guess blindly. If the error looks like a typical GLSL (Shadertoy-style) habit (sampler2D, texture()/texelFetch(), gl_FragCoord, mainImage, fwidth/dFdx/dFdy, precision qualifiers, #define, ...), the result includes a 'hint' field pointing out the SkSL equivalent and the skia-studio://guides/porting-glsl-to-sksl resource for the full porting rules — read that resource if you are porting from GLSL. nodeId must reference an existing runtimeShader node (see read_project — nodes of other types like imageShader/blur/renderPass have no editable source and will return an error). code is the full replacement source, not a diff/patch. This mutates the live project the human is viewing in real time.

Input parameters:

- `code` (string, required): Full replacement AGSL/SkSL source for the node.
- `nodeId` (string, required): Graph node id, e.g. from read_project's project.graph.nodes[].id.

### `set_uniform` (~236 tokens)

Set a shader uniform value

Set the current value of one uniform on a runtimeShader node. nodeId + uniformName identify the target (uniformName as declared in the shader source; see read_project's project.graph.nodes[].uniforms for the node's uniform list — each entry's currentValue/defaultValue array length tells you the required component count for value). value is a flat number array matching that component count — e.g. [0.5] for a float, [1, 0, 0, 1] for a float4 or layout(color) uniform, [1] / [0] for a bool encoded as 1/0. This overwrites any existing binding on that uniform with a constant value and returns the value actually applied after the store commits (so you can confirm it took effect).

Input parameters:

- `nodeId` (string, required): Graph node id, e.g. from read_project's project.graph.nodes[].id.
- `uniformName` (string, required): Uniform name as declared in the shader source.
- `value` (array, required): Flat numeric components matching the uniform's component count (1 for scalar, up to 4 for vector/color).

### `render_frame` (~329 tokens)

Render a PNG screenshot of the live preview

Capture what the connected browser tab is currently rendering, as a PNG, so you can visually check your changes. Omit nodeId to capture the main preview output (the same frame the human sees). Pass nodeId to inspect one graph node's output instead: for a 'renderPass' node this reads its offscreen buffer directly without disturbing what the human is currently viewing; for any other node type this switches the tab's preview target to that node first (the human will see the preview switch in real time, like clicking that node in the UI) and then captures the main output. Pass restorePreview: true to switch the preview back to whatever it was before this call once the screenshot has been captured (the human still briefly sees it switch and switch back); omit it or pass false (the default) to leave the preview on that node afterwards. The image is always the tab's current canvas size (see project.preview.canvas from read_project) — there is no parameter to request a different size. Requires the previewer to be in single mode — graph mode (the previewer's node-graph view) does not host a live renderer, and this tool returns a clear error telling you to ask the human to switch the previewer to single mode.

Input parameters:

- `nodeId` (string): Graph node id to preview instead of the main output. Optional.
- `restorePreview` (boolean): When nodeId is given and refers to a non-renderPass node, restore the preview target to what it was before this call once the screenshot is captured, instead of leaving it switched to nodeId. Default…

### `set_preview_node` (~189 tokens)

Switch (or restore) the live preview target

Switch the connected browser tab's preview to show a specific graph node's output instead of the main render output — the human sees this switch happen in real time, exactly like clicking that node in the UI (this is the same mechanism render_frame uses internally when given a non-renderPass nodeId, but without taking a screenshot). Pass nodeId to switch to that node (it must already exist — see read_project for valid node ids, or this returns an error); omit nodeId to switch back to the main output. Useful paired with render_frame: e.g. switch the preview to a node so the human can see it, or call again with no nodeId afterwards to hand the view back to the main output. Returns the previewNodeId actually applied (null means main output).

Input parameters:

- `nodeId` (string): Graph node id to preview. Omit to switch back to the main output.

### `create_node` (~417 tokens)

Create a new graph node

Add a new node to the shader graph, using the same default property values Skia Studio's own "New" menus use for each type (e.g. blur defaults to sigmaX/sigmaY 4 with clamp tiling, dropShadow to a soft dark shadow, renderPass to scale 1). The new node starts unconnected (no inputNodeId/shaderNodeId/sourceNodeId, no child slot bindings) — use connect_nodes afterwards to wire it into the graph, and it is not graph.mainNodeId unless you call set_main_node. runtimeShader/shaderPass ignore props (they get a blank starter AGSL template); use update_shader_source to set real source and set_uniform for uniform values once the node exists. For every other type, props lets you override the initial fields for that type in one call instead of create_node then update_node — same field names/ranges as update_node's patch (e.g. { sigmaX: 8 } for blur, { color: [1, 0, 0, 1] } for colorShader, { assetId: 'asset_xyz' } for imageShader). name defaults to a numbered placeholder like the UI does ("Blur 2", "Shader Pass 3", ...) if omitted. Returns the full created node JSON, plus verifierErrors if the project is no longer fully valid afterwards (e.g. an imageShader created with an assetId is fine, but the graph overall might already have unrelated issues — this is informational, not a rejection of this call).

Input parameters:

- `name` (string): Optional display name. Omit for a UI-style default name like "Blur 2".
- `props` (object): Optional initial property overrides for the chosen type (ignored/rejected for runtimeShader/shaderPass). Same fields and validation as update_node's patch for that node type — see that tool's descrip…
- `type` (string, required): Graph node type. See read_project's project.graph.nodes[].type for these same 11 strings in context.

### `update_node` (~363 tokens)

Patch a graph node's own properties

Change one or more of a node's own scalar/enum properties in place — NOT connections, source code, or uniform values (those are separate tools; see below). Which fields are legal depends on nodeId's type (see read_project's project.graph.nodes[].type): every type accepts name; blur accepts sigmaX/sigmaY/tileMode; dropShadow accepts dx/dy/sigmaX/sigmaY/color/mode; colorShader accepts color; renderPass accepts scale; feedback accepts clearOnResize; imageShader accepts assetId/tileModeX/tileModeY/filterMode; videoShader additionally accepts loop/muted/autoplay/playbackRate (0.25-2)/volume (0-1)/precise; cameraShader accepts tileModeX/tileModeY/filterMode/facingMode/resolution ('720p'|'1080p'|null)/inputId; audioShader accepts tileModeX/tileModeY/filterMode/resolution (256|512)/inputId. runtimeShader/shaderPass only accept name here. Rejected fields (with guidance to the right tool): source/uniforms (use update_shader_source / set_uniform), childSlots/inputNodeId/shaderNodeId/sourceNodeId (use connect_nodes). color arrays are 4 numbers in [0, 1] (linear, unmultiplied alpha), not 0-255. Unknown fields for the node's type are rejected with the exact supported-field list, not silently ignored. Returns the updated node JSON plus verifierErrors if applicable.

Input parameters:

- `nodeId` (string, required): Graph node id, e.g. from read_project's project.graph.nodes[].id.
- `patch` (object, required): Fields to change; see description for the legal set per node type.

### `connect_nodes` (~330 tokens)

Wire one node into another (or disconnect)

Connect producerNodeId's output into one of consumerNodeId's input slots, or disconnect that slot by passing producerNodeId: null. slot depends on the consumer's type (see read_project's project.graph.nodes[].type / childSlots): for a runtimeShader consumer, slot is the uniformName of one of its childSlots (e.g. a shader declared as 'uniform shader background;' has slot 'background' — see that node's childSlots array; there is no slot for a runtimeShader that declares none); for shaderPass, blur, or dropShadow, slot is always 'input'; for renderPass, slot is always 'shader'; for feedback, slot is always 'source'. imageShader/videoShader/cameraShader/audioShader/colorShader are producer-only leaf nodes and can never be the consumer of this tool (they have no input slot at all — connecting into them always errors). Any node type may be the producer for any compatible slot (no type restriction on producerNodeId). This call is rejected up front (nothing is changed) if it would create a cycle in the shader graph — the error names the offending producer/consumer pair so you can inspect the current graph via read_project before retrying. Returns the consumer node's updated JSON plus verifierErrors if applicable.

Input parameters:

- `consumerNodeId` (string, required): The node whose input slot is being wired (or disconnected).
- `producerNodeId` (required): The node whose output feeds the slot, or null to disconnect the slot.
- `slot` (string, required): Which input slot on consumerNodeId; see description for the rules per node type.

### `delete_node` (~249 tokens)

Delete a graph node

Permanently remove a node from the graph. Refuses if nodeId is graph.mainNodeId (call set_main_node with a different node first) or if it is the only remaining node in the graph. If other nodes/bindings/animation tracks still reference this node (as a child shader slot, inputNodeId, shaderNodeId, sourceNodeId, a Binding target, or an Animation track target), the call is refused by default and the error lists every referrer so you can decide what to do; pass disconnectReferences: true to have this tool disconnect/remove all of those references first and then delete the node in the same call (this is the safe default to reach for once you have reviewed the referrer list, or immediately if you already know you want a clean removal). Returns { deleted, disconnected: { graphReferences, bindings, animationTracks } } describing what was touched, plus verifierErrors if applicable.

Input parameters:

- `disconnectReferences` (boolean): Pass true to disconnect/remove all references to this node and delete it anyway. Default false (refuse if referenced).
- `nodeId` (string, required): Graph node id, e.g. from read_project's project.graph.nodes[].id.

### `set_main_node` (~127 tokens)

Set the graph's render output node

Change graph.mainNodeId — the node whose output is the project's final rendered result (what render_frame with no nodeId, and the human's main preview, both show). nodeId must already exist in the graph (see read_project) but can be of any node type. This is also the only way to reassign the render output away from a node before calling delete_node on it (delete_node refuses to delete graph.mainNodeId directly). Returns the resulting mainNodeId plus verifierErrors if applicable.

Input parameters:

- `nodeId` (string, required): The node to make the new render output.

### `upsert_animation` (~344 tokens)

Create or replace an event-triggered Animation

Create a new Animation, or replace one entirely, in project.animations — the event-triggered parameter transitions described in docs/animation-model.md (spring/cubicBezier tracks armed by a gesture, onLoad, or manual trigger). Omit animation.id to create a new one; pass an existing project.animations[].id to replace that animation completely (this is a full-object replace, not a field patch — every field you omit reverts to this call's defaults, it does not merge with what was there before); an id that does not match any existing animation is rejected (to catch typos instead of silently creating a duplicate). Each track's target is either { type: 'uniform', nodeId, uniformName } (must exist — see read_project's project.graph.nodes[].uniforms) or { type: 'nodeProp', nodeId, propPath } (must be one of the closed set of numeric node properties in src/core/bindableNodeProps.ts — blur sigmaX/sigmaY, dropShadow dx/dy/sigmaX/sigmaY, renderPass scale). track.to (and track.from, if given) must be a number array matching that target's component count. curve is either { type: 'spring', stiffness: >0, dampingRatio: >0 } or { type: 'cubicBezier', x1, y1, x2, y2 (x1/x2 in [0,1], y unrestricted), durationMs: >0 }. Returns the stored animation object plus verifierErrors if applicable. Use trigger_animation afterwards (with render_frame) to see the animation actually play.

Input parameters:

- `animation` (object, required)

### `delete_animation` (~50 tokens)

Delete an Animation

Permanently remove an Animation from project.animations. Rejected if animationId does not exist.

Input parameters:

- `animationId` (string, required): Animation id, e.g. from read_project's project.animations[].id.

### `trigger_animation` (~222 tokens)

Manually fire an Animation (for self-testing)

Arm an Animation's tracks right now, as if its trigger event just happened — regardless of what trigger type it is actually configured with (click, onLoad, manual, ...). This lets you self-test an animation's visual effect: call this, then call render_frame one or more times (the animation plays in real time in the connected browser tab, same as CanvasKitRenderer's per-frame render loop) to see it progress toward its target values. Refuses with a clear error if the animation is disabled (call upsert_animation with enabled: true first), has no tracks, or if every one of its track targets is currently driven by an enabled Binding (bindings always take priority over animations — see docs/animation-model.md §2; use delete_binding or upsert_binding with enabled: false to free the target first). Also refuses if the previewer is in graph mode (same constraint as render_frame — no live renderer to arm tracks against).

Input parameters:

- `animationId` (string, required): Animation id, e.g. from read_project's project.animations[].id.

### `upsert_binding` (~326 tokens)

Create or replace a Binding

Create a new Binding, or replace one entirely, in project.bindings — the mechanism that drives a uniform or node property from an input (time/resolution/pointer/audio/microphone/camera) or a reusable Control. Omit binding.id to create a new one; pass an existing project.bindings[].id to replace that binding completely (full-object replace, not a patch); an id that does not match any existing binding is rejected. source.type 'input' needs an inputId that exists in project.inputs; 'control' needs a controlId that exists in project.controls; 'animation' and 'expression' are REJECTED — these are schema-reserved source types that Runtime v1 does not evaluate yet (see docs/runtime-binding-model.md §2.1), so accepting one would silently create a binding that never does anything. target is either { type: 'uniform', nodeId, uniformName } or { type: 'nodeProp', nodeId, propPath } (same closed nodeProp set as upsert_animation's tracks — see src/core/bindableNodeProps.ts). transform.scale/offset, if given, must be finite numbers. If the target already has another enabled binding, the result includes a warning: per the project's evaluation order, only the first enabled binding on a target (in project.bindings[] array order) takes effect, so a newly added second one may have no visible effect until the earlier one is disabled/removed. Returns the stored binding object plus that optional warning and verifierErrors if applicable.

Input parameters:

- `binding` (object, required)

### `delete_binding` (~50 tokens)

Delete a Binding

Permanently remove a Binding from project.bindings. Rejected if bindingId does not exist.

Input parameters:

- `bindingId` (string, required): Binding id, e.g. from read_project's project.bindings[].id.

### `import_asset` (~398 tokens)

Import an image/video/audio asset

Add a media asset to project.assets, for later use as an imageShader/videoShader assetId (via create_node or update_node) or as an audio input source. Pass exactly one of filePath or dataBase64. filePath is read from disk by this mcp-server process itself — the file's bytes never pass through your context — and is the right choice for real media files (rejected if over 25MB; use the web UI's Assets panel for larger files); its mimeType is inferred from the extension, any mimeType argument is ignored in this branch. dataBase64 is for a small asset you generated yourself (e.g. a procedurally generated texture) and requires mimeType alongside it; decoded size is limited to ~2MB (this is a much smaller cap than filePath because these bytes DO pass through your context). Supported formats mirror docs/media-support-matrix.md: images png/jpg/jpeg/webp/avif/gif, video mp4/m4v/webm/mov, audio mp3/wav/ogg/m4a/aac/flac — an unsupported extension or mimeType is rejected with a clear message. Returns a summary { id, name, mimeType, approxByteLength } — never the asset bytes themselves (same summarization principle as read_project's project.assets[]).

Input parameters:

- `dataBase64` (string): Base64-encoded bytes for a small agent-generated asset. Requires mimeType. Up to ~2MB decoded.
- `filePath` (string): Absolute path to a local file to import. Read by this mcp-server process, not sent through your context. Up to 25MB.
- `mimeType` (string): Required with dataBase64. Ignored (inferred from the extension instead) with filePath.
- `name` (string): Display name. Defaults to the file name (filePath) or a generic placeholder (dataBase64).

### `remove_asset` (~139 tokens)

Remove a media asset

Permanently remove an asset from project.assets. If any imageShader/videoShader node, or audio/videoProgress input, still references this assetId, the call is refused by default and the error lists every referrer; pass disconnectReferences: true to have this tool null out those references first and then remove the asset in the same call. Returns { deleted, disconnected: { nodes, inputs } }.

Input parameters:

- `assetId` (string, required): Asset id, e.g. from read_project's project.assets[].id.
- `disconnectReferences` (boolean): Pass true to clear all references to this asset and remove it anyway. Default false (refuse if referenced).

## Diagnostics

Captured diagnostic sections: Provenance, Dependencies. The full working is on the page: https://verifymcp.io/servers/lixiaolin94-skia-studio/skia-studio-mcp#diagnostics

## Score history

- 2026-08-03: 61
- 2026-08-02: 61
- 2026-08-01: 19
- 2026-07-31: 19
- 2026-07-30: 44
- 2026-07-28: 44
- 2026-07-27: 44

## Links

- npm package: https://www.npmjs.com/package/skia-studio-mcp
- Socket report: https://socket.dev/npm/package/skia-studio-mcp
- Changelog RSS feed: https://verifymcp.io/servers/lixiaolin94-skia-studio/skia-studio-mcp/changelog.xml
- Changelog JSON feed: https://verifymcp.io/servers/lixiaolin94-skia-studio/skia-studio-mcp/changelog.json
- HTML version of this page: https://verifymcp.io/servers/lixiaolin94-skia-studio/skia-studio-mcp
