Inspiration

Anyone who has done network cleanup knows the loop. You pull an ARP table off a switch, or a DHCP lease file, or a page of nmap output, and you are staring at 150 MAC addresses. Which ones are real devices? Which are VMs? Which are phones cycling through randomized addresses so they never look the same twice? Which OUI was never registered with the IEEE at all? The honest answer is usually a spreadsheet on one monitor and a vendor-lookup site on the other, and an hour of alt-tab.

The WebMCP Challenge asked for something specific: a page where a human and a browser agent work the same surface, not a chatbot bolted onto a UI. Network triage turned out to be a near perfect fit. The task is tedious, it is full of small judgment calls, and it produces an artifact someone actually applies afterward: a firewall allowlist. macadress.com already solved the hard part behind an API (OUI and IEEE registration data, device-class inference, virtualization fingerprints, randomization scoring), so the workbench itself could stay thin and honest.

What it does

Paste raw text. The app extracts every MAC in any common format (colon, hyphen, Cisco dotted, bare hex), enriches each one against macadress.com, and drops the results into a table you can slice by vendor, device category, virtualization platform, "randomized," or "unregistered." When the slice looks right, you export it as an nftables set, a Zeek annotation file, or CSV.

Every button on the page is also a WebMCP tool. There are eight: add_mac_addresses, load_sample_data, lookup_mac, enrich_inventory, summarize_inventory, filter_inventory, export_filter_list, clear_inventory. The agent and the person edit one inventory. There is no separate agent view and no "agent mode." If the agent filters to virtualized hosts and builds an nftables set, the human watches it happen in the same table and export box they would have used by hand.

How we built it

Two pieces, no build step.

web/ is static HTML, CSS, and JavaScript. The design rule was one shared state object. app.js defines an ops object with the actual operations. The on-page buttons call ops. The WebMCP tool handlers call ops. Because both paths mutate the same Map of rows and the same listener set re-renders the table, the human sees the agent's work land row by row, and the reverse.

web/wmcp.js is the only file that touches the WebMCP spec, on purpose. It resolves the host object (document.modelContext, falling back to navigator.modelContext) and supports both the modern registerTool({ name, description, inputSchema, execute }) shape and the older bulk provideContext({ tools }) shape. It wraps every handler so a handler can return a plain JavaScript object and the adapter stringifies it, since execute is required to resolve to a string.

worker/ is a Cloudflare Worker that proxies api.macadress.com. The API key lives as a Wrangler secret and never reaches the browser. The Worker also enforces a route allowlist (single lookup and a capped batch only), a per-IP rate limit, and a CORS origin pin.

The math that matters

A MAC address is 48 bits, so there are 2^48 of them, roughly 2.8 x 10^14. The IEEE hands out the top 24 bits as the OUI and the vendor owns the bottom 24, which is 2^24, or 16,777,216 addresses per registration.

Randomized addresses are the reason a hand-built allowlist rots. The naive detector is a single bit: the locally-administered bit, the second-least- significant bit of the first octet. Write b for that octet and use the wedge for bitwise AND:

$$b \land \mathtt{0x02} \neq 0 .$$

That flag is necessary but not sufficient, because plenty of legitimate hardware is locally administered. macadress.com layers a confidence score on top, and the workbench only marks a row as randomized when the API returns potentially_randomized with confidence possible or likely. Enrichment runs in batches of 40, so the cost is one upstream call per 40 addresses, with a per-row fallback if the batch endpoint misbehaves.

Challenges we ran into

The WebMCP surface is still moving. Different runtimes expose different objects and different registration methods. Isolating every assumption into wmcp.js meant the rest of the app never had to care, and a spec change is a one-file diff.

Tools have to return strings, but useful strings. A handler that returns { found: 12, added: 9 } is easy to write. Making sure the agent gets back something it can reason about took a pass over every return value and every tool description. The descriptions are the real API, so they say when to call a tool, not just what it does.

Percent-encoding broke lookups. The upstream API does not decode %3A, so a MAC sent as 00%3A11%3A22... fails. The Worker forwards the colons and dashes intact and only decodes a caller that pre-encoded, so both callers end up identical.

A public Worker URL cannot hold a real secret. The answer was layers instead of one wall: a dedicated low-quota key, an edge WAF rule that rejects any request whose Origin is not the demo page, a rate limit, and a rotatable friction token shipped in config.js. None of them is cryptographic alone. Together they make an abused URL cap out fast.

Demo limits during judging. Testing burned through the shared key more than once, so we added a bring-your-own-key path: paste a macadress.com key into the header, it is stored in that browser only and sent as X-Macadress-Key, and the Worker forwards it upstream while skipping the demo gate and the shared rate limit.

Accomplishments that we're proud of

  • One surface, zero duplicated logic. Eight buttons and eight tools route through a single ops object. There is no code path the agent can take that a human cannot, and none the human can take that the agent cannot.
  • It degrades cleanly. With no WebMCP host present the page is a fully working manual tool. Open it in a WebMCP runtime and the same eight actions become agent tools, no reload.
  • The spec coupling is one file. wmcp.js already absorbs two different host API shapes without the app noticing.
  • The output is real. nftables sets, Zeek annotation files, and CSV that you can apply, not a demo-only artifact.
  • A public demo that is safe to share, because the proxy leans on layered defense rather than a secret in the page, with BYOK as the escape hatch.

What we learned

The value of WebMCP is not "give an agent your API." It is that the agent and the human share one DOM-backed state, and the interesting design work is refusing to fork the view. Once ops existed, the tool layer was almost free, and progressive enhancement fell out for nothing. We also learned how much weight tool descriptions carry: with a good description the agent sequences filter_inventory then export_filter_list on its own, and with a vague one it guesses.

What's next for MAC Inventory Workbench

  • diff_inventory, so an agent can compare a fresh capture against last week's allowlist and report only what changed.
  • More export targets: Suricata datasets, EdgeOS and MikroTik address lists, a Prometheus textfile for alerting on unknown MACs.
  • Persist inventories so a triage session can be reopened, and so the diff has something to diff against.
  • Push the same tool set through the other macadress.com integrations so the workbench and those connectors share one vocabulary.

Built With

  • cloudflare-pages
  • cloudflare-worker
  • javascript
  • rest
  • webmcp
  • wrangler
Share this project:

Updates

Submission history