DEV Community

Cover image for Context Over MCP: Part V The Form Is the Tool.
wolfejam.dev
wolfejam.dev Subscriber

Posted on

Context Over MCP: Part V The Form Is the Tool.

Part IV showed a WebMCP page with three tools. The companion walked through
building one yourself, the JavaScript way: registerTool(), a schema, a
handler. It mentioned the other way in one sentence and moved on.

This is the other way. No registerTool(), and no schema written by hand.
It's an HTML form, with a few attributes that tell the browser it's also a tool.

The playground from Part IV is now listed in the W3C Web Machine Learning
group's awesome-webmcp,
under Demos. That page is the production version of what you're about to build.

Save a blank index.html, serve it with python3 -m http.server, and open
http://localhost:8000 in Chrome with chrome://flags/#enable-webmcp-testing
turned on. Opened straight from disk, the page has no origin and the tool won't register.

Step 1 — start with a form that already works

Before it's a tool, it's a form a person can use. This one estimates a
shipping price. It's read-only: it calculates a number and places no order.

 id="ship">
  Weight (kg)
     name="weight_kg" type="number" min="0.1" max="30" step="0.1" required>
  
  Destination
     name="zone" required>
       value="domestic">Domestic
       value="eu">EU
       value="world">Rest of world
    
  
  Speed
     name="speed">
       value="standard">Standard
       value="express">Express
    
  
   type="submit">Estimate
  

Result: id="result">

Enter fullscreen mode Exit fullscreen mode

Submit it by hand. You get a price, and the URL doesn't change. This is the
same order as the companion's Step 2: the logic is a plain function before
anything calls it.

Step 2 — add three attributes, and it's a tool

 id="ship"
      toolname="estimate_shipping"
      tooldescription="Estimate the shipping price for one parcel. Returns price_eur. Does not place an order."
      toolautosubmit>
  Weight (kg)
     name="weight_kg" type="number" min="0.1" max="30" step="0.1" required
           toolparamdescription="Parcel weight in kilograms, 0.1 to 30">
  
  Destination
     name="zone" required toolparamdescription="Destination zone: domestic, eu or world">
       value="domestic">Domestic
       value="eu">EU
       value="world">Rest of world
    
  
  Speed
     name="speed" toolparamdescription="standard or express">
       value="standard">Standard
       value="express">Express
    
  
   type="submit">Estimate
  

Result: id="result">

Enter fullscreen mode Exit fullscreen mode
  • toolname names the tool.
  • tooldescription tells the agent what it does. Say what it doesn't do too ("does not place an order"), so the agent doesn't have to guess.
  • toolparamdescription on each control becomes that parameter's description.

You don't write the schema. The browser builds the tool's input schema from
the form's named controls, and it reads more of the form than you might
expect. Here's what Chrome 153 generated from the form above:

{
  "type": "object",
  "properties": {
    "weight_kg": { "type": "number", "minimum": 0.1, "maximum": 30, "multipleOf": 0.1,
                   "description": "Parcel weight in kilograms, 0.1 to 30" },
    "zone":      { "type": "string", "enum": ["domestic", "eu", "world"],
                   "description": "Destination zone: domestic, eu or world" },
    "speed":     { "type": "string", "enum": ["standard", "express"],
                   "description": "standard or express" }
  },
  "required": ["weight_kg", "zone"]
}
Enter fullscreen mode Exit fullscreen mode

min, max and step became minimum, maximum and multipleOf. Each
Speed name="speed" toolparamdescription="standard or express"> value="standard">Standard value="express">Express type="submit">Estimate

Result: id="result">

Enter fullscreen mode Exit fullscreen mode

Step 6 — verify it before you add anything else

In Chrome with the WebMCP flag on, document.modelContext also has
getTools() and executeTool(), so you can play the agent from the console.
Run these in order:

  1. The tool is there, with the schema from Step 2.
   const [tool] = await document.modelContext.getTools();
   JSON.parse(tool.inputSchema);
Enter fullscreen mode Exit fullscreen mode

tool.name is estimate_shipping, and the schema has minimum, maximum, the two enums and required: ["weight_kg", "zone"].

  1. A valid call returns a value, and the page stays put.
   await document.modelContext.executeTool(tool,
     JSON.stringify({ weight_kg: 2, zone: 'eu', speed: 'standard' }));
Enter fullscreen mode Exit fullscreen mode

It returns '{"price_eur":12,"zone":"eu","speed":"standard"}', the page shows €12, and the URL doesn't change. Two things to note: executeTool takes the tool object from getTools(), not its name, and the result comes back as a JSON string.

  1. Invalid calls are rejected by the form. Try { weight_kg: 99, zone: 'eu' } and then { weight_kg: 1, zone: 'mars' }. Both reject, with Form validation failed: weight_kg: Value must be less than or equal to 30. and Invalid value "mars" for parameter zone. Your handler never runs.

  2. Without autosubmit, the call waits for a person.

   document.getElementById('ship').removeAttribute('toolautosubmit');
   const pending = document.modelContext.executeTool(tool,
     JSON.stringify({ weight_kg: 5, zone: 'world', speed: 'express' }));
Enter fullscreen mode Exit fullscreen mode

The fields fill, the outline appears, the result line says an agent filled the form, and pending doesn't settle. Press Estimate: it resolves with '{"price_eur":45.9,"zone":"world","speed":"express"}' and the outline goes away.

  1. Nothing leaves the tab. Open DevTools → Network and repeat step 2. No requests.

(Checked in Chrome 153 with #enable-webmcp-testing on, 25 September 2026.)

The same shape, in production

fill_6ws on faf.one/webmcp is this pattern: a plain
form with toolname, tooldescription, toolautosubmit and a
toolparamdescription on each of its six fields. It calls
preventDefault(), answers with respondWith({ yaml }), and doesn't navigate.
It auto-submits for the same reason Step 4 gives: it builds text and changes nothing.

Pick by what the page already has. If it already has a form that does the
job, the form is the tool. If it doesn't, registerTool() is there.


Series: Part I — Invisible AGENTS.md? · Part II — Publishing to the Registry · Part III — Horses for Courses · Part IV — No Working Directory At All · Build a WebMCP Tool From Scratch.

Top comments (0)