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
opsobject. 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.jsalready 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

Log in or sign up for Devpost to join the conversation.