Inspiration
Every morning in an electricity distribution company a dispatcher opens two things: yesterday's work orders and a map of the grid. The orders say what customers reported and what maintenance is scheduled. The map says which transformer serves whom. Between the two sits the actual job: put every order where it belongs, understand who it affects, and turn the list into a route three crews can drive. In Lima that is hundreds of rows against 8,676 transformers, and it has always been done by hand, one lookup at a time.
I am an industrial engineer with ten years in Peru's electricity sector, inside the body that regulates it. From that seat you see how the operations of a public service actually run: a spreadsheet, a map, and a dispatcher deciding who goes where with tools built for other purposes. The regulator publishes the whole grid as open data, nine million supply points and 139,000 substations, and for a long time I have thought about what happens when that data meets the work. Every situation described here is one I have dealt with in person. Now, as a developer, Trafo is the tool I built for those mornings.
What it does
Trafo puts the real published grid of Lima and the day's work orders on one map, and hands an AI agent the same map through 27 WebMCP tools.
- The dispatcher drops in the overnight intake as it arrived, a CSV or Excel with whatever each row carries. The agent places every row on the grid in one query, resolves each to its transformer, and lists the ones that still need a person.
- The dispatcher asks what each order touches. The agent returns customers behind the transformer, the service footprint, the schools and clinics inside it, and the interruption hours already on record against the regulatory tolerance, with no score attached.
- The dispatcher hands over the rows that are nothing but a sentence. The agent reads them, turns "the corner where the pharmacy is" into candidate points and the transformer under each, decides which fits the text, and writes back type, priority, scope and location. The order lands on the map while the dispatcher watches.
- The dispatcher says how many crews there are tomorrow. The agent groups the stops by transformer, orders them by distance, splits them into rounds and draws each round on the map, one colour per crew, stops numbered in visiting order.
- The dispatcher questions a proposal. The agent cross-checks orders sharing a substation across dates, shades the feeders carrying high-priority work on its own layer, and clears it when asked.
- The dispatcher closes orders, tags rows, asks for a cleanup. The agent applies status and priority in batches, previews a deletion, and only deletes once the preview has been seen. Every write can be undone.
Why WebMCP is a strong fit for this use case
A map is not a dataset. It is live state in a browser tab: a viewport, a selection, a transformer somebody clicked, a layer somebody turned on, thirty stops numbered in visiting order. Everything valuable about it is geometric, and everything geometric lives in the page. The page can resolve a supply number to a point, take the convex hull of 1,870 supplies, count the facilities inside it and order stops by distance, in milliseconds. What the page cannot do is read "twenty houses behind the market" and understand it as a fault near a landmark. A language model reads that easily. And neither of them should decide whether 900 customers outrank a school. That belongs to a person.
WebMCP lets all three work on the same object. The page exposes its geometry as typed tools on document.modelContext, the agent calls them with what it read from the orders, and the dispatcher sees every result on the map in front of them, because the map the agent acts on and the map the person is looking at are the same one. A conventional API would have moved that exchange off the page: the agent would get JSON, the dispatcher would get a summary, and the two would be looking at different things. Keeping the tools in the tab is what makes the collaboration visible, checkable and fast.
How it creates a better user experience
The dispatcher works the way they already talk about the grid: three crews tomorrow, the hospital feeder first, leave the closed ones alone. The agent turns that into typed tool calls and the map updates in front of them. Orders land on their transformers, the ones that need reading get a location, the rounds draw themselves. Every proposal comes back with figures, so the dispatcher questions it, changes a constraint and asks again, in minutes.
The planning that used to take a morning now takes a conversation. Against the 82 sample orders: 82 lookups become one call; 76 visits become 33 because 13 transformers carry 56 of the orders; 33 stops reorder from 51.2 km to 16.8 km and split across crews; the question "which orders sit on a transformer that also feeds a clinic", which used to be a day of cross-referencing, is three tool calls. And the page is complete without the agent, because in an operations room the agent may not be available and the work still has to happen.
What people and agents can now do together
- Put a whole intake on the grid in one call, with the rows that need a person listed instead of lost. Until now it was one lookup per row in a billing system.
- Ask a spreadsheet which orders sit on a transformer that also feeds a school or a clinic, and get the facilities named. Until now that check waited for the day of the outage.
- Give the orders that arrived as a sentence a place on the grid. The agent reads, chooses, writes back; the dispatcher watches it land and can move it. Until now that reading stayed in someone's head.
- Turn 76 stops into 33 visits and a route two thirds shorter, drawn on the map per crew. Until now routing was experience and a paper map.
- Hand over a cleanup without handing over the keys. "Delete every closed order" gets a preview; the tool that deletes exists only after it, runs once, and everything can be undone.
Real impact
Measured against the 82 sample orders the app opens with, on the real grid of Lima:
| What the dispatcher needs | Before | With Trafo |
|---|---|---|
| Planning time for the day's intake | A morning of one-by-one lookups | Minutes of conversation |
| Locating every order on the grid | 82 lookups | 1 call |
| Visits to cover 76 located orders | 76 | 33, because 13 transformers carry 56 of the orders |
| Driving for a 33-stop round | 51.2 km | 16.8 km, 67% less |
| Orders that arrived as a sentence | Unplaced until someone read them | 6 of 6 read, placed and written back |
| Orders on a transformer that also feeds a school or clinic | A day of cross-referencing, rarely done | 3 tool calls, facilities named |
| Interruption hours against the NTCSE tolerance | Checked after the fact | Reported per substation while planning |
What that means on the ground:
- Prioritisation with facts. Every order carries its customers, its facilities and its accumulated hours before a crew is assigned, so the dispatcher decides what goes first with the consequence in view rather than from the row's text alone.
- Fewer kilometres and fewer trips per crew per day. Grouping by transformer and ordering the stops cuts the driving by two thirds on the sample day, and the same computation runs on any intake of any size.
- Outages on sensitive feeders caught at scheduling time. A planned outage on a substation that feeds a health post shows the facility when the order is placed, not on the morning of the work.
- Regulatory exposure visible while planning. Hours already accumulated against the tolerance are on the table before a new interruption is scheduled on the same customers.
- The rows nobody had time to read reach the map. Free-text claims get a place and a scope on the day they arrive.
How we built it
Vanilla JavaScript, Leaflet and SheetJS, plain files served over HTTP, no build step. 27 tools registered with document.modelContext.registerTool(), each with a JSON Schema inputSchema with bounded numbers and enums and an async execute returning plain JSON. Inputs are checked at runtime, not only in the schema: codes that reach the grid service are pattern-checked before a request leaves the browser, internal state fields are rebuilt on intake rather than accepted from the caller, and a filter-less bulk write needs all: true. confirm_deletion is registered dynamically by preview_deletion and unregisters itself after running. The page checks for the browser's own modelContext, names the cause in the header chip when it is absent, and falls back to a spec-compatible shim so every tool still runs from the built-in console. A 3×3 km bundle of central Lima ships with the app so the core flow runs with no network; the rest of Lima and beyond is queried live from the public service, and the status line names which tier answered.
Challenges
Working with a public map service whose behaviour had to be discovered by testing: grouped queries do not paginate, so the dataset build subdivides the area instead; a supply number is unique only within a utility, so batch lookups are scoped; the substation join uses a field with an unexpected name; and the low-voltage network is published as geometry without topology, so circuit membership is estimated by proximity and drawn at an opacity equal to its confidence. And keeping the tool surface honest: an early tool ranked orders by a formula nobody had agreed to, and it was replaced by one that reports facts and sorts only by what the caller names.
What's next
The same public server publishes Peru's natural gas distribution network under the same API. Nothing in the engine is electricity-specific, so the next step is a layer manifest for gas, then water, with the same question asked of a valve that is asked of a transformer: if this goes out of service, who loses supply and how bad is it?
Built With
- css3
- html5
- javascript
- webmcp
Log in or sign up for Devpost to join the conversation.