# An MCP server that lets an agent write WebGPU shaders — Tobi Moccagatta

> You cannot render WebGPU in a Node process, so the server owns nothing and relays into a live editor tab. The compile feedback loop is what makes an agent able to write shaders at all.

- Site: https://tobis.vision — Tobi Moccagatta, Creative Developer at basement.studio
- Canonical: https://tobis.vision/notes/mcp-server-that-writes-webgpu-shaders
- Contact: contact@tobis.vision

_Published 2026-09-01 · mcp, webgpu, tsl, three-js, agents, architecture_

You cannot render WebGPU in a Node process. So an MCP server that wants to let an
agent *write shaders* has a problem before it starts: the thing that compiles the
shader and the thing the agent talks to cannot live in the same place.

Shader Lab's MCP server solves it by owning nothing. It is a relay.

```
MCP client (Claude Code) ⇄ stdio ⇄ shader-lab-mcp (Bun process)
                                       ⇅ WebSocket (127.0.0.1:7420)
                                 editor tab (?agent=1)
```

The server speaks MCP over stdio and hosts a loopback-only WebSocket bridge. The
editor connects to that bridge when you open it with `?agent=1`. Every tool call is
relayed into the tab and executed through the editor's normal store actions — which
means **everything the agent does lands in the undo history**, and Cmd+Z works on it.

The renderer stays where the GPU is. The server stays where the agent is. Nothing
has to move.

## The loop is the product

The tools that create and reorder layers are the boring part. The one that matters
is `write_custom_shader`, and what makes it work is not that it writes a file —
it is what comes back:

```
Returns { compiled, error } — on failure, `error` is the exact
compiler/runtime message, so fix and retry.
```

That is the whole thing. An agent writing shader code without the compiler's exact
output is guessing, and guessing at TSL produces confident nonsense. Hand back the
verbatim error synchronously and the agent converges in two or three attempts,
because it is doing what a person does: read the error, fix that line, recompile.

Then `screenshot` closes it. Compiling is not the same as being right, and a shader
can compile perfectly into a black rectangle. The agent writes, compiles, reads the
error or reads the pixels, and goes again.

Write → compile → exact diagnostic → fix → look at the pixels. Four steps, and the
middle two are the ones people skip when they build this kind of tool.

## No imports, on purpose

The contract for a sketch is deliberately small:

```
export const sketch = Fn(() => {
  // returns a TSL node
})
```

No import statements are allowed. Everything — all of `three/tsl`, the house
utilities, `time`, and `inputTexture` when the shader is running in effect mode — is
injected as a global.

That looks like a limitation and is actually the point. Module resolution is one of
the most reliable ways to make a language model produce broken code: it will import
a real function from a plausible-but-wrong path, and the failure arrives as a
resolution error that has nothing to do with the shader it was trying to write.
Delete the import statement from the language and that entire failure class stops
existing. There is no dependency graph to get wrong because there are no
dependencies.

`effectMode` picks the other axis: false generates imagery from scratch, true
transforms whatever the layer stack below produces, sampled through `inputTexture`.

## What it is not

Being precise, because "live compilation" covers a lot of ground:

- **Not incremental.** The sketch recompiles as a unit. There is no dependency
  tracking and nothing is cached between attempts.
- **Not module hot-reload.** There are no modules. See above.
- **Not batched.** One tool call, one compile, one answer.

What it *is*: a synchronous compile against a live WebGPU renderer, with the real
diagnostic returned to the caller inside a 15-second budget, in a tab that is
already showing the result. That covers the case that matters — an agent iterating
on a shader — without pretending to be a compiler toolchain.

## The unglamorous parts that make it usable

**Timeouts are per-operation, not global.** 5s default, 15s for a compile, 30s for a
screenshot. A shader compile is not a state read and should not share its budget.

**The bridge is loopback-only, and the origin list is explicit** — localhost, plus
`eng.basement.studio` and `*.vercel.app` previews, extensible by env var. A deployed
editor tab connects *back* to your machine, so the deployment is never in the path.
The server runs locally and the bridge never leaves it.

**One tab at a time.** The bridge holds a single connection. Two editors would mean
two renderers disagreeing about which is authoritative, and there is no good answer
to that, so it is not allowed.

**The API reference is generated.** `get_shader_api_reference` is built from a
generation script rather than hand-written, so the surface described to the agent
cannot drift from the surface that exists. Agent-facing documentation that goes
stale is worse than none — it teaches the model to call things that were removed.

**Errors are addressed to whoever can fix them.** Not "connection refused" but *open
the editor with `?agent=1` appended and try again*, and for a port clash, the port
number and the env var that changes it. The reader of an MCP error is usually an
agent that will act on it immediately, so the message should say what to do rather
than what went wrong.

## The takeaway

If you are giving an agent control of something visual, the temptation is to spend
the effort on the API surface — more tools, more parameters, more coverage. That is
the wrong end. The surface barely matters. What determines whether the agent can
actually work is whether failure comes back *specifically* and *fast*, and whether
it can see the result afterwards.

Exact compiler output and a screenshot. Everything else is layers and plumbing.

---

## Other pages

- [Creative Developer](https://tobis.vision) — hey, I'm tobi. creative developer at basement.studio, working on shaders and 3D on the web. peek into my vision. Markdown: https://tobis.vision/index.md
- [Experiments](https://tobis.vision/experiments) — A growing list of experiments across shaders, sound, scenes, and interaction. Markdown: https://tobis.vision/experiments.md
- [Works](https://tobis.vision/works) — Shaders and real-time 3D for Coinbase, Modal, Baseten, E2B and Rox at basement.studio, plus Shader Lab and Annex. Markdown: https://tobis.vision/works.md
- [About](https://tobis.vision/about) — Creative developer at basement.studio working on shaders, WebGL and WebGPU. Self-taught, in Buenos Aires, open to freelance. Markdown: https://tobis.vision/about.md
- [Contact](https://tobis.vision/contact) — How to reach Tobi Moccagatta: one email address, what to put in it, what he takes on, and current availability from Buenos Aires (UTC-3). Markdown: https://tobis.vision/contact.md
- [Privacy](https://tobis.vision/privacy) — What this site collects and what it does not: cookieless analytics, no forms, no trackers, and an in-memory rate-limit counter that expires with its window. Markdown: https://tobis.vision/privacy.md
- [Developer resources](https://tobis.vision/developers) — Machine-readable tobis.vision: llms.txt, the OpenAPI spec, the read-only content API, and the markdown representation of every page. Markdown: https://tobis.vision/developers.md
- [Notes](https://tobis.vision/notes) — Write-ups from building shaders and real-time 3D on the web: WebGPU, TSL and three.js, mostly the parts that went wrong. Markdown: https://tobis.vision/notes.md
- [Grass (2026)](https://tobis.vision/experiments/grass) — A walkable Val d'Orcia field: 790,000 instanced grass cards on a procedurally baked atlas, backlit through a translucency map, bent by a travelling wind field and parted by a displacement trail you leave behind you. Markdown: https://tobis.vision/experiments/grass.md
- [Critters](https://tobis.vision/experiments/critters) — Soft vinyl creatures raymarched from signed distance fields, morphing between forms as one continuous surface and reacting to your cursor. Markdown: https://tobis.vision/experiments/critters.md
- [LPV](https://tobis.vision/experiments/lpv) — Real-time global illumination via light propagation volumes, hand-written in TSL on WebGPU compute. Markdown: https://tobis.vision/experiments/lpv.md
- [BIP](https://tobis.vision/experiments/bip) — A holoprojector experiment with BIP, the robot influencer. Markdown: https://tobis.vision/experiments/bip.md
- [Fluid](https://tobis.vision/experiments/fluid) — A WebGPU fluid simulation ported from Pavel Dobryakov's classic to three.js TSL. Markdown: https://tobis.vision/experiments/fluid.md
- [Alpha-to-coverage, and the black edges it doesn't fix](https://tobis.vision/notes/alpha-to-coverage-foliage-atlases) — 790,000 grass cards you can walk inside cannot be depth-sorted. Alpha-to-coverage solves the ordering, a contrast curve solves mip alpha collapse, and neither touches the dark outlines. Markdown: https://tobis.vision/notes/alpha-to-coverage-foliage-atlases.md
- [Real-time GI that just brightened the walls](https://tobis.vision/notes/real-time-gi-that-just-brightened-the-walls) — A light propagation volume can solve live and still read as a brightness slider: the missing sun-patch bounce, the dynamic range propagation destroys, and the read-side falloff that gives it back. Markdown: https://tobis.vision/notes/real-time-gi-that-just-brightened-the-walls.md
- [MSAA on a RenderTarget breaks GTAO on WebGPU](https://tobis.vision/notes/msaa-breaks-gtao-on-webgpu) — Why three.js GTAO fails with `Invalid ShaderModule "fragment_GTAO"` the moment a RenderTarget asks for samples, and what to do instead. Markdown: https://tobis.vision/notes/msaa-breaks-gtao-on-webgpu.md
- [Making three.js bloom 9x faster with the Call of Duty blur](https://tobis.vision/notes/three-js-bloom-jimenez-dual-filter) — three.js renders a fixed 5-mip chain of separable Gaussians every frame and ignores your radius. The Advanced Warfare downsample/upsample took bloom from 8.2ms to 0.9ms. Markdown: https://tobis.vision/notes/three-js-bloom-jimenez-dual-filter.md

## Machine-readable

- [llms.txt](https://tobis.vision/llms.txt)
- [sitemap.xml](https://tobis.vision/sitemap.xml)
- [robots.txt](https://tobis.vision/robots.txt)
- [openapi.json](https://tobis.vision/openapi.json)
- [developer resources](https://tobis.vision/developers)
