units-le-mcp
v1.1.0
Published
Extract durations, sizes, percentages and frequencies in one base unit, refusing ambiguous ones.
Maintainers
Readme
units-le-mcp
An MCP server that finds every quantity in a document — a number welded to a unit — and returns it twice: as the document wrote it, and in one base unit so two of them can be compared. Durations come back in milliseconds, sizes in bytes, percentages as a ratio, frequencies in hertz, each with its line, column and key path. It is the engine behind the Units-LE editor extension, exposed as a tool an agent can call.
A quantity it cannot read unambiguously comes back with a named reason,
never a guess. A bare 500m is minutes in one config format, milliseconds in
another and millicores in Kubernetes, so it is returned as ambiguous_unit
rather than as any of them. 1.5KB, 1,5s and 1h + 30m are refused the same
way. 1MB is returned as 10^6 bytes, with a flag saying the writer may have
meant 2^20. A bare number with no unit is not a finding.
No dependencies, no network calls, no filesystem access. Content goes in, structured results come out.
Use it
Point any MCP host at npx units-le-mcp.
Claude Code
claude mcp add units-le -- npx -y units-le-mcpAnything with a JSON config — Cursor, Windsurf, Claude Desktop:
{
"mcpServers": {
"units-le": {
"command": "npx",
"args": ["-y", "units-le-mcp"]
}
}
}VS Code needs nothing here. Install the extension instead — it carries this server and registers it for you: VS Code Marketplace · Open VSX
No Node? The same extract_units tool ships in a static Rust binary:
cargo install units-le, then units-le mcp
(crates.io). The two servers answer
identically — one corpus runs against both, and a differential test feeds both
thousands of generated documents in every format, broken ones included, and
compares every answer, parser errors and all. The binary additionally offers
units_le_scan, which walks a tree; this server reads no files.
Prefer a global install to npx on every launch:
npm install -g units-le-mcpNo environment variables, no API key, no configuration of its own. To check it before wiring it into anything:
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | npx -y units-le-mcpIf that prints the tool name, the server works.
The tool
extract_units
| argument | type | |
|---|---|---|
| content | string | required. The document text. |
| format | json jsonc yaml csv tsv toml ini env | Optional. Anything else, or nothing, scans the text directly. |
| filename | string | Used to infer the format when format is absent, e.g. config.toml. |
| dimension | duration bytes percent frequency | Report only one. A refusal that names no dimension is always kept. |
| maxResults | number | Default 500, from 1 to 5000. |
Refusals are ambiguous_unit, fractional_bytes, locale_separator,
compound_arithmetic and out_of_range, each with a sentence a person can
act on; si_iec_hazard accompanies a value rather than replacing it. A
structured document that does not parse is a diagnostic in its parser's own
words, and ok is false while one is present:
{
"ok": true,
"data": {
"quantities": [
{
"value": "1h30m",
"dimension": "duration",
"baseUnit": "milliseconds",
"base": "5400000",
"key": "cache.ttl",
"line": 2,
"column": 8
},
{
"value": "500m",
"dimension": null,
"baseUnit": null,
"base": null,
"reason": "ambiguous_unit",
"detail": "`m` is minutes in one config format, milliseconds in another and millicores in Kubernetes. Write `min`, `ms`, or spell out the core count.",
"key": "cpu",
"line": 3,
"column": 6
}
],
"fileType": "yaml",
"refused": 1
},
"diagnostics": [],
"meta": {
"tool": "extract_units",
"count": 2,
"truncated": false
}
}Also in the MCP registry
io.github.nolindnaidoo/units-le —
registry.modelcontextprotocol.io
Fifteen more like it
One tool each, same shape: content in, structured data out, no network and no
filesystem. Every one is on npm as <name>-mcp and in the MCP registry as
io.github.nolindnaidoo/<name>.
| Package | Tool | Does |
|---|---|---|
| urls-le-mcp | extract_urls | URLs, with protocol and position |
| colors-le-mcp | extract_colors | colors from stylesheets and code |
| dates-le-mcp | extract_dates | dates and timestamps |
| numbers-le-mcp | extract_numbers | numeric values |
| paths-le-mcp | extract_paths | file and directory paths |
| string-le-mcp | extract_strings | string values |
| regex-le-mcp | extract_patterns | regexes, with a ReDoS verdict |
| secrets-le-mcp | detect_secrets | credentials, masked — never the value |
| envsync-le-mcp | compare_env_files | dotenv key drift, names only |
| scrape-le-mcp | analyze_robots_txt | whether a path may be crawled |
| unicode-le-mcp | detect_unicode_risks | Unicode that hides meaning, as codepoints |
| i18n-le-mcp | check_catalogues | translation catalogues, keys only |
| ids-le-mcp | extract_ids | UUIDs, ULIDs and Snowflakes, with the time inside |
| ips-le-mcp | extract_ips | IP addresses, CIDR blocks and MACs, normalized |
| versions-le-mcp | compare_versions | dependency constraints that disagree across manifests |
Every tool in the family, one page: letools.dev
Built by
Nolin Naidoo — Chief Engineer, AI/ML & Platform Architecture. nolindnaidoo.com · GitHub · LinkedIn
Also from the same workshop
Twelve Rust tools built the same way: small, single-purpose, and driven by a machine rather than a person. pixelcoords and pixelactions make up one loop — pixelcoords answers where, pixelactions acts there. The ten LE crates are the terminal half of the extensions they sit in: the same detection, held to the extension's own corpus, and an exit code instead of a results editor.
| | | | |---|---|---| | pixelcoords | Freeze your screen, mark regions, get pixel-exact coordinates and crops | site · crates.io · docs.rs | | pixelactions | Consume human-verified coordinates, perform the interaction, confirm it landed | site · crates.io · docs.rs | | paths-le | Find every path in a codebase and report whether it still points at anything | crates.io | | secrets-le | Find hardcoded credentials, and never print one | crates.io | | urls-le | Extract every URL from a codebase, with its protocol and exact position | crates.io | | regex-le | Find every regex in a codebase and report which can be driven into catastrophic backtracking | crates.io | | string-le | Get every string in a codebase out where a person can read them | crates.io | | numbers-le | Find every hardcoded number in a codebase so a person can check them | crates.io | | envsync-le | Compare the dotenv files in a tree and say which keys are missing from which | crates.io | | colors-le | Find every colour in a codebase, and say which are not in your palette | crates.io | | dates-le | Extract every date and timestamp, and the exact instant each one resolves to | crates.io | | scrape-le | Check whether a page is scrapeable before the scraper is written | crates.io |
Licence
MIT © Nolin Naidoo
