Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@digitable-lol/tools-core

Headless TypeScript port of the it-tools utility collection: pure functions, a machine-readable catalog, and an MCP stdio server that exposes all of it through three tools instead of ninety-four.

No Vue. No DOM. No browser APIs. Runs under Node, Bun, and as a single compiled binary.

  • 95 tools ported from 75 of the 86 upstream utilities
  • 14 categories, 114 worked examples (all executed by the test suite)
  • 220 tests passing, tsc --noEmit clean

Origin and licence. This is a derivative work of it-tools by Corentin Thomasset, which is published under the GNU General Public License version 3. Because this package copies and adapts that source, it inherits the same licence and is redistributed under GPL-3.0-only — see LICENSE and NOTICE. Not affiliated with or endorsed by the upstream author.


Why three MCP tools

Registering 95 MCP tools would push every input schema into the model's context on every turn. This package routes in two cheap steps instead:

MCP tool Purpose Payload
tools_categories Index of the 14 categories, one line each ~536 tokens
tools_list Full schemas + examples for one category 1-17 tools
tools_execute Run a tool by tool_id with args

Results are returned both as structuredContent and as a pretty-printed JSON text block, so clients that read either channel work unchanged.

The design target is a 0.6B router model, which is why descriptions are short and single-purpose, schemas are flat (never deeper than two levels of properties), and oneOf / anyOf never appear in an input schema. A test enforces all three properties.


Quick start

npm install
npm test                 # 220 tests
npm run typecheck
npm run build:binary     # -> dist/digit-tools-mcp
npm run smoke:binary     # replays all 114 catalog examples through the binary

Run the server straight from source:

npm run mcp              # node --experimental-strip-types src/mcp.ts

Use it as a library:

import { executeTool, getCategoryIndex, getToolsInCategory } from '@digitable-lol/tools-core';

getCategoryIndex();                    // step 1: which category?
getToolsInCategory('crypto');          // step 2: which tool, and what arguments?

await executeTool('hash_text', { text: 'hello', algorithm: 'SHA256' });
// { ok: true, result: { algorithm: 'SHA256', encoding: 'Hex', hash: '2cf24dba…' } }

await executeTool('hash_text', { algorithm: 'SHA256' });
// { ok: false, code: 'invalid_args', error: "must have required property 'text'" }

executeTool never throws. Every outcome is {ok: true, result} or {ok: false, error, code}, where code is one of unknown_tool, invalid_args, execution_error, timeout. Arguments are validated against the tool's input_schema with ajv (coerceTypes and useDefaults on, since small models routinely emit "2024" where a number is wanted).


Connecting an MCP client

Compiled binary — no runtime required on the host:

{
  "mcpServers": {
    "digit-tools": {
      "command": "/absolute/path/to/tools-core/dist/digit-tools-mcp"
    }
  }
}

From source, using Node's built-in TypeScript stripping:

{
  "mcpServers": {
    "digit-tools": {
      "command": "node",
      "args": [
        "--experimental-strip-types",
        "/absolute/path/to/tools-core/src/mcp.ts"
      ]
    }
  }
}

Binary size and startup

Metric Value
Binary size 111,683,904 bytes (107 MiB)
Startup (spawn → initialize response), median 542 ms
Startup, min / p90 390 ms / 641 ms
Same, running from source under Node ~3,900 ms median

Measured with npm run bench:startup (12 runs, first two discarded) on Linux 6.8, Node v24.18, Bun 1.3.12, at a 1-minute load average of ~10 on 8 cores.

Caveat on these numbers: this machine carries other workloads. A re-run at load average ~48 gave min 3,711 ms / median 7,794 ms for the same binary — roughly 14x worse purely from CPU contention. Re-measure on an idle host before treating 542 ms as the figure; the shape of the result (binary ≈ 7x faster than Node-from-source) held at both load levels.

Startup was 1,214 ms before the heaviest payloads — mathjs, the OUI table, markdown-it, libphonenumber, sql-formatter, node-forge, qrcode, figlet, the emoji dataset — were moved behind dynamic import() calls inside the tools that need them. The first call to those tools pays a one-time load cost; everything else starts fast.


The catalog

src/catalog.ts is the key artifact. Each entry:

{
  id: 'hash_text',
  name_ru: 'Хеш текста',
  name_en: 'Hash text',
  category: 'crypto',
  description_ru: 'Считает криптографический хеш строки выбранным алгоритмом.',
  deterministic: true,
  input_schema:  { /* flat JSON Schema */ },
  output_schema: { /* flat JSON Schema */ },
  examples: [{ input: {...}, output: {...} }],
  run: hashText,
}

API:

  • getCategoryIndex() — the 14 categories with one-line descriptions and tool counts (1,875 chars ≈ 536 tokens)
  • getToolsInCategory(cat) — full schemas for one category, implementations stripped
  • getToolSchema(id), getFullCatalog(), getToolIds()
  • npm run catalog:dump writes the whole thing as JSON (~184 KB)

Entries live in src/catalog/*.ts grouped by family; src/catalog.ts aggregates them and exposes the routing API.

Categories

id Russian name Tools
crypto Криптография 9
generators Генераторы 9
auth Аутентификация 7
encoding Кодирование 12
converter Конвертеры форматов 17
text Работа с текстом 8
web Веб 10
network Сети 5
development Разработка 5
data Разбор данных 4
datetime Дата и время 1
math Математика 3
measurement Измерения 2
images Изображения 3

Ten come straight from the upstream registry; generators, auth, encoding and datetime were split out of it-tools' oversized Converter and Web buckets so no category is large enough to blow the router's context.

Examples are the eval set

Every tool has at least one example, and test/catalog.test.ts executes all 114 through executeTool. For the 80 deterministic tools the output is compared with a deep equality check. For the 15 non-deterministic ones the example carries match: 'shape' and only the key set and value kinds are verified — the values genuinely change between runs.

npm run smoke:binary replays the same 114 examples through the compiled binary over real stdio. This is what caught the figlet font-loading bug described below; a passing unit suite would not have.


What was ported

Group A — pure deterministic functions (62 upstream utilities). Hashes, HMAC, codecs, format converters, parsers, formatters, calculators. Logic carried over from the upstream .service.ts / .models.ts where one existed, or lifted out of the <script setup> block where it did not. Cryptographic code (bcrypt, BIP39, JWT, crypto-js ciphers, OTP) was moved verbatim and still leans on the same npm packages at the same major versions as it-tools.

Group C — random or clock-dependent (13 upstream utilities, 15 tool ids). Ported and flagged deterministic: false:

bcrypt_hash, encrypt_text, rsa_keypair_generate, token_generate, uuid_generate, ulid_generate, bip39_generate, lorem_ipsum_generate, random_port_generate, mac_address_generate, otp_generate_totp, otp_verify_totp, otp_secret_generate, ipv6_ula_generate, eta_calculate

Where it was cheap, randomness was made reproducible. src/utils.ts provides a seeded mulberry32 PRNG, and an optional integer seed makes token_generate, uuid_generate, ulid_generate, lorem_ipsum_generate, random_port_generate, mac_address_generate and otp_secret_generate repeatable. Clock-driven tools take an explicit time instead: otp_generate_totp/otp_verify_totp accept now, ipv6_ula_generate accepts timestamp, eta_calculate accepts startedAtMs.

Not seedable, deliberately: bcrypt_hash and encrypt_text draw their salt from the underlying library, and rsa_keypair_generate from node-forge. Forcing a seed there would mean reimplementing cryptographic primitives, which is exactly the kind of change that introduces silent security bugs.

Three tools are deterministic despite living next to random ones, and are marked as such: bcrypt_compare, decrypt_text, bip39_from_entropy / bip39_to_entropy.

What was NOT ported (Group B — 11 utilities)

Utility Reason
base64-file-converter Needs a browser File/FileReader upload
camera-recorder MediaRecorder + camera permissions
chronometer A live wall-clock stopwatch; no meaning in a request/response tool
device-information Reads navigator, screen, window
keycode-info Reads live DOM keyboard events
pdf-signature-checker Needs an uploaded PDF binary
html-wysiwyg-editor TipTap editor UI; no headless logic
text-diff Pure Monaco diff-editor widget; the .vue file contains no logic at all
git-memo Static cheatsheet, not a computation
regex-memo Static cheatsheet, not a computation
meta-tag-generator Skipped: depends on @it-tools/oggen, and the value is a form UI rather than a function

The last three are honest omissions rather than technical impossibilities — a cheatsheet lookup tool could be built, it just would not be a port of anything.


Deliberate deviations from upstream

Ten behaviour changes, made on purpose, each because the browser original was wrong for a headless caller. Everything not on this list is a straight port — see NOTICE for the structural changes (Vue removed, catalog / execution / MCP layers added) that sit on top of these.

  1. xml_format emits LF, not CRLF. xml-formatter defaults to \r\n.
  2. temperature_convert rounds to 10 decimals. The scale factors are irrational in binary, so 100 °C → °F produced 211.99999999999994. Rounding removes the noise without touching any precision a temperature reading carries.
  3. benchmark_stats takes one series, not many. Upstream compared several suites at once, which forces an array-of-objects-holding-arrays input — three levels of schema nesting. One series per call keeps the schema flat.
  4. chmod_calculate takes a flat string. Upstream drove nine checkboxes; here "755" or "rwxr-xr-x" works, in either direction.
  5. json_diff returns a flat change list. Upstream produced a nested tree for its tree widget; a list of {path, status, oldValue, value} is far easier for a small model.
  6. Errors are thrown, not swallowed. Upstream wrapped most tools in withDefaultOnError(..., '') and rendered an empty string. Here the throw propagates to executeTool, which turns it into {ok: false, code: 'execution_error'}. Affects xml_format, token_generate (empty alphabet) and the format converters.
  7. eta_calculate formats durations without date-fns locales, so output is stable.
  8. jwt_parse renders exp/iat/nbf as ISO 8601, not a machine-local locale string.
  9. QR codes render as SVG, not a canvas — same qrcode package, headless renderer.
  10. emoji-picker became emoji_search. Upstream is a scrollable grid with a fuse.js search box; headless, only the search means anything. Fuzzy matching is replaced by exact / prefix / substring tiers over name, slug, keywords, group, code points and the emoji itself, so a query always returns the same list in the same order. With no query the tool returns the first limit emoji of the dataset (optionally of one group).

Everything else is a straight port. Where upstream had a .service.ts, the algorithm is unchanged.


Tests

test/catalog.test.ts   124 tests   catalog integrity + all 114 examples via executeTool
test/ported.test.ts     60 tests   upstream unit tests, imports adapted
test/tools.test.ts      18 tests   mac_address_lookup and emoji_search, incl. embedded data
test/execute.test.ts    11 tests   validation, defaults, coercion, timeouts, error mapping
test/mcp.test.ts         7 tests   the three MCP tools over an in-memory transport
──────────────────────────────────
                       220 passing

test/ported.test.ts carries over every upstream *.service.test.ts / *.models.test.ts that survived the port: chmod, hash-text, integer-base-converter, ipv4-address-converter, ipv4-range-expander, json-diff, json-to-csv, json-viewer, list-converter, mac-address-generator, numeronym, OTP, password-strength, regex-tester, roman-numerals, safelink, string-obfuscator, text-statistics, text-to-binary, text-to-unicode, token-generator, xml-formatter, color-converter, date-time-converter. The two assertions that changed are commented in place, both consequences of deviation #6 above.

Upstream tests that were dropped: chronometer.service.test.ts (tool not ported).


Known limitations

  • The execution timeout is not preemptive. JavaScript cannot interrupt synchronous CPU-bound work. withTimeout fires at the next await boundary, which in practice covers the lazily-loaded tools; a runaway regex inside regex_test would still block the process. Fixing this properly needs a worker thread. Documented on withTimeout in src/execute.ts and pinned by a test.
  • ascii_art_generate ships 15 fonts, not figlet's 328. figlet reads .flf files from disk at runtime, which bun build --compile does not bundle — the binary crashed with ENOENT: /$bunfs/fonts/Standard.flf. A curated set is embedded as data by scripts/generate-figlet-fonts.mjs (~300 KB) and loaded lazily. Add names to that script and to src/tools/figlet-font-names.ts to include more.
  • The OUI vendor table is embedded, not read from oui-data. Same trap as the fonts: the package is a bare index.json that require resolved from node_modules, so the compiled binary failed with Cannot find package 'oui-data' from '/$bunfs/root/…' whenever it ran outside this checkout. All 39,227 entries are inlined by scripts/generate-oui-data.mjs into src/tools/oui-data.ts (~4 MB) and loaded through a lazy import(). Run npm run generate:oui after bumping the dev dependency to refresh it.
  • emoji_search ships a snapshot of the CLDR emoji data. 1,870 emoji with names, slugs, groups and keywords are inlined in src/tools/emoji-data.ts (~260 KB), taken from unicode-emoji-json 0.4.0 and emojilib 3.0.10 — the datasets upstream loads in the browser. Refreshing them means regenerating that file.
  • json_to_toml / yaml_to_toml write 8_080, not 8080. @iarna/toml uses underscore digit separators for larger integers. This is the library's canonical output, left as-is.
  • iban_validate reports a generic checksum error. ibantools v4 exposes its detailed error-code list through an overload that extractIBAN does not surface, so the errors array is coarser than upstream's.
  • The binary is 107 MiB. Bun's compiled output embeds the whole runtime. The embedded OUI table (~4 MB), the figlet fonts (~300 KB) and the emoji dataset (~260 KB) add to it, but the runtime dominates.
  • iarna-toml-esm (used upstream) is unloadable under plain Node ESM, so this package uses @iarna/toml v3 — the same code, same major version, packaged for CommonJS.

Layout

src/
  catalog.ts            aggregation + getCategoryIndex / getToolsInCategory
  catalog/*.ts          catalog entries grouped by family
  execute.ts            executeTool: ajv validation, timeout, structured results
  mcp.ts                binary entry point
  mcp-server.ts         the three MCP tools
  tools/*.ts            the ported implementations
  utils.ts              base64, seeded PRNG, shared helpers
  interop.ts            dual-build (CJS/ESM) default-export unwrapping
  node-require-shim.ts  global `require` for js-sha256's eval probe
scripts/                font/OUI generation, catalog dump, binary smoke test, benchmark
test/                   the four suites above

src/interop.ts and src/node-require-shim.ts exist because several dependencies ship both a CommonJS and an ESM build. Node resolves one, Bun's bundler resolves the other, and a plain default import works in exactly one of them. The namespace-import-plus-unwrap pattern is what lets identical source run under node --experimental-strip-types, vitest, and bun build --compile.

License

GPL-3.0-only, inherited — not chosen — from it-tools by Corentin Thomasset. This package copies and adapts it-tools source code, which makes it a derivative work, so the GNU General Public License version 3 covers it as a whole. Upstream states "GNU GPLv3" and ships the plain GPL-3.0 text with no "or any later version" grant, so version 3 is the version that applies here too.

The full licence text is in LICENSE. Provenance, the list of changes, and the third-party data notices are in NOTICE.

Practical consequence: if you hand someone the compiled binary (dist/digit-tools-mcp), GPL-3.0 sections 4-6 require you to give them the corresponding source under the same licence. That obligation is why this repository is published rather than kept private — the binary is distributable precisely because its source is here.

About

94 утилиты it-tools без Vue: библиотека, каталог JSON-схем, MCP-сервер, сборка в один бинарь. GPL-3.0.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages