Skip to main content
Glama
dzhunka

Napkin

by dzhunka

Napkin

A blank napkin for your agent. It sketches nothing — you do.

Napkin is an agent plugin whose core is an MCP App: a remote MCP server that ships an interactive widget the host renders next to the tool result. The agent calls one tool, a square of paper appears in the conversation, you draw on it, press Send, and the drawing arrives as an image in your next message.

It exists because the alternative is worse. Describing a shape in words is slow and lossy, and the workaround — pick up a real napkin, draw on it, photograph it, upload the photo — works but costs a detour through your phone every time. Some directions are only five seconds of pen: a logo that should feel like a mountain but rounder, which panel sits where, how three boxes connect.

Layer

Where

What it is

MCP App

app/mcp/route.ts + widget/

A Next.js route serving a remote MCP server, and the napkin it hands the host

Plugin package

plugin.json, mcp.json, skills/

Agent Plugins 1.0.0 package that installs into Cursor, Codex, and Claude Code

Built from dzhunka/mcp-app-boilerplate.

What v1 is

A pen and a Send button. No eraser, no undo, no colours, no shapes, no rulers. A napkin is the rough version of an idea, and the constraint is the point — to change a sketch, ask for a fresh napkin.

Related MCP server: cnvs-whiteboard

How the sketch gets back

This is the part worth understanding, because the obvious design does not work.

The instinct is to have the widget call a tool with the PNG and let the tool return it. But the result of a tool the widget invoked goes back to the widget, and no host is obliged to also put it in front of the model. A sketch the model never sees is useless.

So the widget hands the image to the host instead, with ui/message and an image content block, and the host adds it to the thread as though you had attached the file yourself. Codex ships a localised string for exactly this — "Shared an image from {appName}" — so it is a designed path, not a loophole.

Two consequences fall out of that, and both are good:

  • The server is stateless. The drawing never touches the deployment. There is one tool, no upload route, and nothing stored, which is why this runs on serverless functions without a database.

  • The image lands in your own transcript. You see what you sent, and the turn starts on your press rather than on the model's guess about when you are done.

The route is negotiated from the host's declared capabilities rather than assumed. A host that takes images only as model context gets the image parked there with a short text message as the trigger; a host that takes them nowhere gets a disabled Send button and an honest explanation. See DECISIONS.md for the host evidence behind this.

The tool

open_napkin({ brief? }) — model-visible, and the only tool. It opens the napkin and returns, telling the agent to stop and wait. brief is a short reminder printed above the paper, like "rough logo direction".

skills/napkin/SKILL.md teaches agents when to reach for it, and — just as importantly — that after calling it they should say something brief and then wait rather than narrate a drawing that does not exist yet.

How the MCP App works

MCP Apps is SEP-1865, stable since 2026-01-26 and identified as io.modelcontextprotocol/ui. Three things wire it together:

  1. A resource with a ui:// URI and MIME type text/html;profile=mcp-app returns the widget's HTML.

  2. A tool points at that resource through _meta.ui.resourceUri.

  3. The host renders the HTML in a sandboxed iframe and speaks MCP JSON-RPC to it over postMessage.

The widget is not served as a web page. Its HTML travels as text inside the JSON-RPC response, and the host injects it into an iframe under the host's own origin. The deployment is never navigated to — it only answers /mcp.

That is why this repo has two builds and one deployment. Next.js is the server: it terminates Streamable HTTP, runs the tool, and serves the resource. The napkin is a Vite app compiled into a single self-contained HTML file, which the MCP route reads off disk and returns.

Inlining everything is deliberate. Host sandboxes enforce a CSP that restricts which origins a widget may load subresources from, and a widget that fetches nothing works on every host regardless of that policy.

app/
  mcp/route.ts          MCP server — open_napkin + the ui:// resource
widget/
  index.html            Vite entry
  main.tsx              React root
  app.tsx               the napkin: canvas, pen, Send
  use-mcp-app.ts        the host bridge, including the negotiated image route
  styles.css            Tailwind
  dist/index.html       built bundle (generated, gitignored)
vite.config.mts         single-file widget build
proxy.ts                CORS headers, so browser-based MCP clients can connect
scripts/dev-codex.mjs   isolated Codex instance with this plugin installed
scripts/test-client.mjs protocol smoke test
scripts/set-endpoint.mjs writes your deployment URL into both manifests
scripts/capture-seed-thread.mjs captures one of your threads as a seed fixture

The canvas

The pixel buffer is a fixed 1024×1024 square while CSS decides the rendered size. Two reasons: resizing a canvas clears it, so pinning the buffer keeps a layout change from wiping a drawing, and every napkin submits at the same resolution. The nib is deliberately bolder than a real pen at that size, because models downscale images before reading them and a hairline does not survive it. The paper is painted rather than left transparent — a transparent PNG composited onto a dark background hides the ink completely.

Develop

Requires macOS with the Codex desktop app, and Node 22.12+ (Vite 8).

pnpm install
pnpm dev

pnpm dev is the whole loop. It builds the widget, starts a Vite watcher and the Next.js dev server on port 3100, installs this repo as the only local plugin in a throwaway Codex instance, points that plugin at http://127.0.0.1:3100/mcp, and launches a separate Codex desktop window. Closing the window removes the isolated state. Your everyday Codex install is untouched.

No tunnel is required, because the widget requests no subresources. Because each run gets a fresh Codex instance, its widget cache starts empty, so you see the current bundle without bumping UI_VERSION.

A fresh instance also starts with an empty thread list. pnpm capture-thread captures a real conversation into scripts/fixtures/ so pnpm dev replays it into the isolated instance, waiting in the thread list and ready to continue — useful here, since testing Napkin means first getting to a point where a sketch would help:

pnpm capture-thread --last                    # or: pnpm capture-thread <thread-id>
pnpm capture-thread <thread-id> --name logo-brief --description "what it sets up"

Capturing rewrites your home directory, Codex home, and working directory into placeholders that the harness substitutes at seed time. It does not rewrite the conversation, so read a fixture before committing it.

For server-only work, or to check the protocol:

pnpm dev:server               # Vite watcher + Next.js on port 3000
pnpm test:client              # verifies the protocol against localhost:3000

pnpm test:client connects as a UI-capable client and asserts the parts that are easy to get subtly wrong: the extension capability, the ui:// metadata on the tool, the resource MIME type, that the resource is real HTML, that the bundle is self-contained, and that it still carries a canvas and a PNG export.

Bump UI_VERSION in app/mcp/route.ts whenever you ship a widget change, or hosts will serve a cached copy.

Deploy

pnpm build                                           # widget bundle, then Next.js
vercel deploy --prod
pnpm set-endpoint https://your-project.vercel.app    # updates both manifests
pnpm test:client   https://your-project.vercel.app

mcp.json and .mcp.json ship with a replace-me.example.com placeholder, so a deployment that forgets pnpm set-endpoint fails loudly instead of silently talking to someone else's server.

next.config.ts lists the widget bundle in outputFileTracingIncludes so the serverless function can read it at runtime. If you move the bundle, update that path too, or production will fail to serve the resource.

Install

The plugin root is the repository root, and both manifests describe the same package; distribute it as a git repo. Run pnpm set-endpoint first — installing while the placeholder URL is in place gives you a plugin whose MCP server never connects.

Cursor natively supports Agent Plugins. Add the repo through Customize → Plugins, which reads the root plugin.json.

Codex (0.147+) reads the portable root plugin.json and mcp.json:

codex plugin marketplace add dzhunka/Napkin
codex plugin add napkin@Napkin
codex plugin list --json

Claude Code does not yet parse the Agent Plugins $schema, so it reads the parallel .claude-plugin/plugin.json and .mcp.json committed here. Its skills/ discovery is the same directory, so the skill is shared:

claude plugin marketplace add dzhunka/Napkin

To connect only the MCP server without the plugin wrapper, any host that speaks Streamable HTTP can point at the endpoint directly — though without a host that renders MCP Apps and accepts images from them, there is nothing to draw on:

claude mcp add --transport http napkin https://your-project.vercel.app/mcp

Stack

  • mcp-handler 2 — framework-agnostic MCP HTTP adapter, serves the 2026-07-28 protocol natively with a fallback for 2025-era Streamable HTTP clients

  • @modelcontextprotocol/server 2 and @modelcontextprotocol/ext-apps 2 — MCP SDK v2 plus the MCP Apps helpers (registerAppTool, registerAppResource)

  • Vite 8 with vite-plugin-singlefile and Tailwind 4 — the widget bundle

  • Next.js 16 on Vercel Fluid Compute — the MCP server

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables creating and refining pencil-style drawings as layered HTML Canvas snippets through natural language descriptions, using classical construction methodology.
    -
  • F
    license
    Not graded
    quality
    B
    maintenance
    Enables Claude Code to request and read hand-drawn sketches or annotations on a persistent canvas displayed on local or networked devices.
    1
    -