-
-
Cleansheet logo
-
Cleansheet word mark
-
A 3,290-row finance export: 25 defect classes across 37,479 cells, found server-side in one pass.
-
the diffs. /review with before-and-after pairs and a destructive tag visible.
-
the tool surface. /activity with the extension panel visible if you can fit it, showing live and withdrawn tools with their reasons.
Inspiration
I chose spreadsheet cleaning because spreadsheets still sit behind a large amount of daily work. People use them for budgets, forecasts, inventory, finance reports, operations, and planning. Fixing a few cells sounds easy, but a large file can turn that job into hours of repetitive checking.
I wanted to test whether an agent could do the inspection without being trusted to silently rewrite the file. Cleansheet gives the agent access to the sheet, but keeps the final write decision with the person using it.
What I built
Cleansheet accepts a CSV upload and checks it for common data problems. It reports the defect type, the number of affected cells, an example, and a possible fix.
The bundled finance export contains 3,290 rows, 25 defect classes, and 37,479 damaged cells.
The app exposes 11 tools through WebMCP. An agent can read the sheet, inspect a column, preview a transformation, stage a proposal, and request a review.
The available tools change with the state of the sheet. During review, tools that could write are removed. The agent can explain a proposed change, but it cannot apply one. The Apply button is the only writer.
There is also a switch that turns WebMCP off. With tools enabled, the agent uses the tool surface. With tools disabled, it has to operate the interface. The page measures both cases using real tool-call and DOM-interaction counters.
How I built it
The frontend is static. Three Node functions handle parsing, transforming, and exporting the CSV. Uploaded files are processed for the session and are not retained.
src/core.mjs contains the state machine without DOM or network access. toolAvailability() is the single place that decides which tools may be called. resolveReview() is the only function that can write to a row. After each state change, the browser layer reconciles the registered tools with that core state.
scripts/proof.mjs uses the same core as the browser. That lets me test the important behavior without relying on a browser test.
Problems I ran into
Deduplication nearly shipped as a data-loss bug twice.
At first, the key columns were hardcoded. A file with different headers could cause every row to fall into one duplicate group. I changed the ranking to look at how well columns actually distinguish rows. When no reliable key set exists, Cleansheet reports that and does not offer deletion.
I also found a mismatch between preview and Apply. The preview said 61 rows would be dropped, but Apply would have removed 116 because blank-key rows were handled differently. The proof script caught it. I changed both paths to use the same decision logic.
The WebMCP client landscape also changed while I was building. Chromium renamed navigator.modelContext to document.modelContext and removed the old name in version 150. One in-app browser could render the page and register tools but could not invoke them. The page now registers against whichever supported entry point the browser exposes.
The work meter was wrong for a while too. It counted clicking the WebMCP switch as work avoided, which made the result look better than it was. The counters now attribute each event to the system that performed it.
What I learned
The most useful part of this project was deciding what the agent should leave alone.
Blank fields stay blank. Free-text fields are not automatically title-cased. Deduplication is only offered when the file contains reliable matching keys.
I also learned that the interface should enforce the safety boundary. A refusal message can be ignored. A tool that was never registered cannot be called.
The on/off switch made the comparison measurable. Instead of estimating how many interactions an agent might use, I could count the actual events on the page.
What's next
I want to add Excel workbook support. For now, the app returns a clear 415 response instead of treating the workbook ZIP as plain text.
Other planned additions are reversible applies, saved cleaning recipes, a manual key picker for deduplication, and support for larger files. The current limits are 8 MB and the first 20,000 rows.
Built With
- css
- csv
- html
- javascript
- node.js
- vercel
- webmcp
Log in or sign up for Devpost to join the conversation.