Inspiration
My family's business is yarn. I grew up around patterns covered in red pen, and around customers coming back into the shop a week after buying wool to say the numbers did not work.
That is the part people outside the craft do not expect. A knitting pattern can read perfectly and still be impossible to knit. The numbers all have to agree with each other. A cast on has to be a multiple of the stitch repeat plus the edge stitches. A raglan yoke can only reach a target count in whole rounds of eight increases. Shaping has to land on whole rows. Every measurement has to grow as the sizes grow. Break one of those and the pattern still reads like English, prints fine, sells fine, and then fails on somebody's needles after they have paid for the yarn and knitted half a sleeve.
Designers do this work in spreadsheets today. A spreadsheet will happily give you a cast on of 87 stitches, because 87 is a number like any other. That is why tech editing is a paid profession and why publishers keep permanent errata pages.
So when I read the WebMCP spec, the thing that jumped out was not that an agent could fill in my form faster. It was that the page could say no, and say why. I built Jig for myself first, and I have been grading real patterns in it since.
What it does
Jig is a knitting design workbench that a person and an agent use at the same time, on the same live document.
Ask it for a cast on of 87 stitches on a twelve stitch cable panel. It refuses, names the rule in knitter language, gives back the offending value, and gives back 86 and 98 as the nearest counts that actually fit. The agent reads that one sentence and corrects itself, without ever being told what the repeat was. On the human side those same numbers become one click fixes in the size table.
Past that, the agent can do the whole job: read the pattern, read the chart as structured rows rather than a picture, set gauge, grade a full run of sizes while you keep typing into it, swap yarn against a second website and get the answer back in whole skeins, run a tech editing audit, and export. Every call, refusal and correction lands in a transcript in plain English, with whether a person or an agent did it.
Why WebMCP fits this use case
Three reasons, and all three are about the constraint living in the page instead of behind a server.
The rules can refuse. A tool call here is a proposal, not a command. A wrong number comes back with the rule it broke and the nearest values that satisfy it, so the caller never has to work out the arithmetic. It reads one sentence and tries again. That loop is only cheap when the rules sit next to the caller.
The rules cannot be bypassed. A person typing, an agent calling a tool, a declarative form submission
and the page auditing itself all enter the same propose function. The committed document type carries a
private symbol brand, so building one any other way is a compile error.
The document is shared rather than handed over. The designer keeps editing while a grading run is in flight, and the run picks up the edit for every size it has not reached yet. There is no session to own and nothing to merge afterwards.
What people and agents can do together here that was hard before
Correct a refused instruction without ever being told the rule. Ask for 87 and say nothing else. The refusal carries the rule, the offending value, and 86 and 98. A correct second call can only have come from reading that refusal, because the prompt never mentioned the repeat. That is measured against a real browser, not asserted.
Edit a number while an agent's grading run is still running. The run reads through a thunk rather than a snapshot, so every size it has not computed yet picks up your keystroke, and neither of you has to wait for the other.
Swap yarn across two websites and get a whole skein answer. Change yarn and gauge changes, so yardage per square inch changes, while the new skein has its own length. Two variables at once, which is the classic way to get this wrong. To the agent it is one verb. Underneath it resolves tools on the other origin, invokes them, quarantines every string that crosses the boundary, revalidates through the same gate, and answers in whole skeins per size.
Watch the whole negotiation in plain English. Every call, refusal, correction and human edit becomes a sentence in the transcript with its author, so the person is never guessing what the agent just did.
How I built it
Two real origins, both deployed on Cloudflare Pages. The workbench, and a separate yarn shop site that lends three read only tools to the workbench and to nobody else. The workbench also runs one Pages Function so the model key stays server side and a visitor never needs an API key.
Fourteen tools, and deliberately every registration path the spec offers rather than the same one
fourteen times: a React hook for the main verbs, raw registration with an AbortSignal for the two
tools that only exist while a chart panel is selected, raw registration with exposedTo for the tool
named to another origin, and one declarative HTML form that becomes a tool with no registration code at
all.
The rules live in a pure engine package that cannot import React, the DOM or WebMCP, enforced by a
custom lint rule. Every write from every author goes through one propose function, and the committed
document type carries a private symbol brand, so building one outside the engine is a compile error
rather than a code review note. Sixteen formal correctness properties run over generated inputs with
fast-check, including one that proves every repair offered is itself accepted and one that proves no
valid value sits between the refused number and the repair. 1,641 tests across 108 files.
It also works in a browser with no flag enabled, because a polyfill host stands in and reproduces the real behaviour, refusals included.
Challenges I ran into
The ones that mattered all came from driving a real browser instead of trusting a test double, and every one of them is now pinned by a test that fails if the fix comes out.
An AbortSignal only withdraws a tool when it is the second argument to registerTool. Put it on the
descriptor and Chrome accepts it and silently ignores it. executeTool will not take a tool name, it
wants the tool object from getTools. Arguments have to be a JSON string, and an object gets rejected.
And a refusal's first text part has to stand alone, because some hosts collapse the whole structured
result down to content[0].text, so the rule and the repairs have to be in that first part or a model
reading only that part gets nothing it can act on. All four of those live in one resolver now, and my
fake host reproduces the browser exactly, so a finding cannot quietly regress.
My favourite catch was the smallest one. A tool description named an example panel id that did not match the seeded document, which is invisible to every unit test in the suite and obvious the moment a real model follows the description. So now a test walks every panel name any schema mentions and checks it resolves against the real document.
One small thing that made a large difference: the field parameter is a closed enum rather than a free
string, because a model asked to change a cast on reaches for castOnCount first, which does not exist.
Naming the options in the contract removed a whole class of failed first attempts.
Accomplishments that I'm proud of
The refusal is the product, and it holds up under a machine. npm run verify:browser launches a real
browser against the live site, drives document.modelContext with no model and no API key, and proves
the loop end to end: 87 refused, the repair parsed out of the first text part alone with zero knowledge
of the document, sent back, accepted, and held. Eleven checks, eleven passed. The agent eval suite scores 4 of 4 with a real model choosing real tools.
I am also proud that text arriving from the other origin is wrapped in a type with no string member, so interpolating a stranger's comment into a prompt is a compile error. The demo carries a real prompt injection in a community note. The scanner flags it, the broker declines it, and the declined instruction gets written into the transcript where a person can see somebody tried.
What I learned
That a tool result is a piece of writing. The difference between "rejected" and "here is the rule, here is your value, here are the two that fit" is the difference between an agent giving up and an agent finishing the job, and it is one sentence of prose in a JSON field.
That a draft API has to be measured, not read. The four behaviours that shaped this build the most were ones the spec does not describe, and the only way to find them was to open a browser and watch.
And that constraints belong in the page. The refusal loop is only cheap when the rules sit next to the caller instead of behind a server the human is not editing.
What's next for Jig
Charts, read out loud, correctly.
A knitting chart is a grid of symbols. A screen reader cannot read it. Blind and low vision knitters have said this plainly: the Written Charts page on a blind and low vision knitting blog hosts volunteer translations of charted patterns and states outright that patterns with even simple charts go unknitted for this reason. So the work gets done by hand, for free, one pattern at a time, by people who have other things to do. Desktop software can do part of it, and a tech editor has written about how the output still needs judgment.
read_chart already returns a chart the way it has to be read: per row, in worked order, with stitch
counts and where the repeat boundaries fall. That is the hard half, and it is already built, because an
agent needed it anyway. Finishing it turns volunteer labour into something a browser does for free.
That matters everywhere and it matters more where I live. Hand knitting here runs on patterns passed around as photographs, in mixed units, with no size standards attached to them. My family's shop watches people give up on a garment because nothing in the pattern is checkable, and there is no tech editor to send it to. A page that checks the numbers and reads the chart, in the browser, with nothing to install and nothing to buy, is what I am building next.
After that: more garment constructions past raglan, more than one document open at once, and getting the whole thing in front of the designers my family sells to.
Built With
- chrome
- cloudflare-pages
- cloudflare-workers
- css
- eslint
- fast-check
- framer-motion
- gpt-4o-mini
- html
- iframe
- javascript
- localstorage
- model-context-protocol
- node.js
- openai
- property-based-testing
- react
- react-three-fiber
- svg
- three-js
- typescript
- vite
- vitest
- web-components
- webmcp
Log in or sign up for Devpost to join the conversation.