Repository navigation
Releases: CoderGamerUnknow/Project-Tango
Release list
v3.2.0
Four gaps between what this project claimed and what it actually verified.
One breaking change, and it is the honest kind: the server now refuses to
start on a runtime that cannot durably store your data. No tool name, parameter
or response field changed.
Changed
- Node 22.5 is now the floor — breaking for anyone on Node 18. The package
claimedengines.node >= 18.0.0whilenode:sqlitehas only existed since
22.5, and it claimed that in the one fieldnpmchecks at install time. So a
Node 18 user got a clean install, a working--help, and then a server that
failed at its first write with a stack trace about a module that does not
exist in their runtime — after they had already pointed a real agent at it.
The claim is now true (>=22.5.0), and on an older runtime the server exits 1
with a plain sentence explaining which version it needs and why, before it
creates a data directory or prints a mock-data warning that would have sent
them looking in the wrong place. - Two escape hatches, for the runtimes that cannot be upgraded. Set
PROJECT_TANGO_PERSIST=0to run stateless (nothing is written, nothing is
lost, it just does not survive a restart), orPROJECT_TANGO_STORE=jsonto
keep the JSON backend and accept that it cannot notice another process
replacing the catalog underneath it. Both start normally, and both are stated
in the README's new Runtime floor section rather than left in the source.
Added
- A release gate that runs before a release, not after one. Two releases in
a row went out over a red CI run, and the changelog/tag lineage checks that
would have caught the cause can only run once the tag exists — so they run
minutes after the release is public.scripts/release-check.mjsmoves every
one of those conditions to the moment you can still do something about them:
the version agrees acrosspackage.json,package-lock.jsonand
SERVER_VERSION; the changelog has a dated heading and a link for it; the tag
is annotated, sits onmain's lineage, is dated the day the heading claims,
and is an ancestor ofHEAD. It is wired asnpm run release:checkand runs
in CI on tag pushes, so the mistake is caught on a private commit as often as
on a public tag. - A Windows CI leg. Every gate in this project had only ever run on Linux,
on a machine whose line endings, path separators and shell differ from the
platform a surprising share of agent tooling is actually installed on. CI now
builds and tests onubuntu-latestandwindows-latestin the same matrix,
withfail-fast: falseso a Windows failure is visible rather than inferred. - The published tarball is installed and booted before it is believed. The
package smoke test used to run against the working tree, so it could pass on a
build whose tarball was missing the entry point. CI now packs, installs into a
clean directory, boots the real binary over stdio and completes a real
place_orderagainst it — the same check, run against the artefact users
download. - The start-up refusal is tested end to end.
src/bootstrap.test.tsspawns
the realindex.tsas a child process and asserts on its exit code and
stdout: a normal runtime answersinitialize, an old one exits 1 with an
explanation and an empty stdout, and — the assertion that matters — a refused
start leaves no data directory behind, so the failure cannot be mistaken for a
corrupt install.
Fixed
- The concurrency soak could not fail. 3.1.2's freshness test asserted no
read trailed a committed write by more than one commit. Measured, that bound
was never tested: a read cost about a quarter of a write, so the reader was
almost always reading between commits andworstLagwas structurally zero. A
reader that refreshed only one read in sixty would have passed. The reader now
pauses briefly between reads, so roughly three quarters of the gaps it
observes contain two or more commits, and a new assertion fails the suite if a
run never once put the reader behind — the test can no longer pass vacuously.
Lazy-refresh and zero-delay variants were confirmed to fail it. - The Windows leg's own smoke script is verified locally, not just in CI. It
was extracted from the workflow YAML and run against a real local pack before
being trusted, including the case where the binary is missing and the script
must exit non-zero rather than pass quietly.
Project Tango v3.1.2
Breaking for backend authors only.
StateStore.changeTokenis now
required rather than optional. Nothing an MCP client can see changed — no
tool name, parameter or response field — so if you only run the server,
upgrading is a normal reinstall.
Coming from 3.1.0? Those releases' data-loss defects are repaired
automatically by 3.1.1 and later; see the README's upgrade note.
Tightens the seam that 3.1.1 left loose, and proves it holds under load.
One breaking change, to theStateStoreinterface rather than to anything an
MCP client can see: no tool name, parameter or response field changed, and
upgrading is a normal reinstall.
Changed
StateStore.changeTokenis now required — breaking for backend authors.
3.1.1 fixed a server that kept serving a catalog another process had
replaced, and made the fix reachable by omitting an optional member: a
backend added later could leavechangeTokenoff the interface and silently
reinstate the exact defect, with nothing to notice it. Every backend this
project can return already provided one, so no shipped behaviour changes. A
backend that genuinely cannot detect another writer must now say so with
stableToken(reason)— the limitation becomes a decision someone wrote down
rather than a gap nobody noticed. The requirement is only that a rival's
commit is detectable: a token that also moves on the provider's own writes is
merely less efficient, which is what the JSON backend'ssize:mtimetoken does.
Correctness never depends on which kind a backend provides.
Added
- Freshness verified under load. Two real server processes, one data
directory, forty commits spread across four products while the reader serves
hundreds of interleavedlist_all_productscalls. The assertions are the ones
that only fail once the two have had time to drift apart: no read trails a
committed write by more than one commit, the reader is current within three
reads of the writer going quiet, it follows the stream rather than jumping once
at the end, and it never serves a catalog mid-write or reports stock moving
backwards. Both ways of breaking it were confirmed to fail the suite — pinning
the token, and refreshing only one read in sixty. - A performance guard on the token check. The reload the token exists to
avoid is the expensive path; if the check ever got slower than the reload it
would be pure overhead. A guard holdschangeTokenat least 20x cheaper than a
full load on the shipped dataset (measured ~72x).
Fixed
- The 3.1.0 release record was wrong in a way that mattered. Its body had
stray blank lines from the original extraction, and — more importantly — it
described two data-loss defects without telling anyone who installed it that
they were carrying them. Both it and 3.1.1's now lead with the upgrade path: if
your stored state holds fewer products than the seed catalog, 3.1.1 repairs it
on its own, with no manual step and nothing you placed lost.
Project Tango v3.1.1
Coming from 3.1.0? Read this before anything else. The two data-loss
defects fixed below are repaired automatically on first run: the missing seed
products are re-added and written back, with no manual step. See
3.1.0.
Bug fixes and release tooling. No tool names, parameters or response fields
changed. Two data-loss defects and one write failure, all found while trying to
make a server notice that another process had written, plus the workflow that
gives every release a verified artifact.
Fixed
- A server listed thirteen products and then refused to sell one of them.
Saved state can legitimately hold fewer products than the seed catalog — a
state.jsonwritten before a product was added, or one whose rows were pruned
— and the startup reconcile put the missing ones back in memory. A transaction
then re-read the store under its write lock and adopted that smaller row set
verbatim, so the catalog collapsed to whatever the file happened to contain,
andsimulate_order_placementansweredno product found with id "prod_001"
for a productlist_all_productshad just returned. The commit that followed
persisted the loss, so the erosion survived every restart. Reconciliation now
happens wherever stored state is adopted, and because a commit writes what is
in memory, the stored catalog is repaired rather than eroded. - A running server kept serving a catalog another process had replaced.
Reads came from the process's own copy, which only caught up when that process
wrote something itself — so an agent could place an order in one window and be
told stale inventory in the next. Reads now compare a cheap change token first
(PRAGMA data_version,staton the JSON backend) and reload only when
another process committed. The check is skipped during a transaction, where a
reload would discard state that has not committed yet.
Added
- Releases carry a verified installable tarball. A new workflow builds it,
installs it into a clean directory, drives the binary that install ships, and
only then attaches it to the release — onrelease: published, so it happens
without being asked. Two things this fixes:v3.1.0shipped source only, and
an artifact attached from a contributor's machine turned out to be corrupt
there (GitHub's asset endpoint stored the multipart envelope along with the
file on every framing tried, so a "successful" upload produced an unusable
download). Attaching from a runner also means the published file is the one
that was executed, not one built alongside the release and never run. StateStore.changeToken?()— an optional cheap token that moves when
another process committed. Optional on purpose: a backend that cannot detect
another writer keeps the previous behaviour instead of re-reading blindly.- Order ids carry their process id.
nextOrderIdproved unique only within
one process: its sequence is module state, so two servers sharing a data
directory minted the same id for orders placed in the same millisecond. With
orders.ida primary key, the stored list then held that id twice and the
second write was rejected outright — one server's order could not be placed at
all. The pid closes the gap with no randomness: two live processes cannot share
one, and a recycled pid only collides with an id from a different millisecond.
Fixed
- A rejected save reported itself as a successful one.
save()swallowed
every write failure and logged it, which is right for the environment (a
read-only disk, a closed handle) and wrong for the data:SQLITE_CONSTRAINT
means the rows are not storable, which is a defect in what was built rather
than where it is stored. That combination is how a caller comes to believe an
order was placed when the transaction behind it was rolled back — the exact
"silent partial write" this project promises cannot happen. Constraint failures
are now rethrown; environmental ones still degrade with a warning.
Project Tango v3.1.0
Upgrade: install 3.1.1 before trusting this release.
3.1.0 shipped two data-loss defects. If your saved state holds fewer products
than the seed catalog — which happens after any upgrade from the old
state.jsonformat — this release lists all thirteen products and then
refuses to sell one of them, committing the shortfall so it survives every
restart. It also reported a rejected save as a successful one, so an order can
come back with a receipt that was never written.3.1.1 repairs this on its own. Its first transaction re-adds the missing
seed products and writes the repaired catalog back, so no manual step is
needed — install it and restart the server. Nothing you placed is lost; orders
recorded against the missing products are still instate.dband resolve again
once the products are back.
State is now durable across processes, and the v2.0.0 lineage's two useful
ideas are in main. Tool names, parameters and response fields are unchanged
from 3.0.1, except that list_all_products now returns its matches ranked.
Added
- A SQLite durable store (
src/sqliteStore.ts). State moved from a
state.jsonsnapshot tostate.db, using Node's built-innode:sqlite— no
new dependency, nothing to install, nothing to host. An existingstate.json
is imported on first run, validated record by record with exactly the rules
the JSON store used, and kept asstate.json.migrated. - Cross-process write transactions.
StateStoregained an optionalupdate,
and the SQLite store implements it asBEGIN IMMEDIATE— the write lock is
taken before the state is read, so a second server validates against what
the first one committed rather than against a snapshot it has already moved
past.MockDataProvider.transact()uses it, so order placement's whole
read-validate-write is exclusive. This is the change that makes concurrent
writes safe: ten server attempts at 5 units against a stock of 42 fill
exactly eight orders and refuse the other two with a reason, where before the
last writer's snapshot simply overwrote the others. - Search ranking (
src/productIndex.ts), ported from thev2.0.0lineage's
src/trie.ts— but as a ranking layer, not the filter it was there. See
the note below. PROJECT_TANGO_STORE=jsonto force the previous whole-snapshot backend,
for a runtime withoutnode:sqlite(Node < 22.5). It keeps publishing one
process's complete snapshot and is still last-writer-wins across processes;
the README says so where it documents that flag.
Fixed
- Two servers starting together could split one catalog across two files.
PRAGMA journal_mode = WALreturnsSQLITE_BUSYimmediately, ignoring
busy_timeout, so opening the store raced: the loser gave up, fell back to
the JSON backend, and two processes then wrotestate.dbandstate.jsonas
separate catalogs that each believed they owned the stock. The store now
retries the open with backoff and waits out a lock another process holds
instead of routing around it. - Falling back to a different on-disk store is worse than losing durability.
Two files disagreeing about inventory is not recoverable; losing state across
a restart is. A store that cannot be opened now degrades to memory with a
warning on stderr, never to a second file. - A test run could create the real
~/.project-tangocatalog. The store is
resolved — and therefore created — whenmockData.tsis imported, and
src/storage.test.tsasked for the default directory to check its
description. The runner now pins a throwaway data directory for every test
process (src/testGlobalSetup.ts), and a suite-hygiene test fails if that
wiring is ever dropped.
Known limitation
Reads are served from a running process's own in-memory catalog, so a
long-lived server can trail another process's last commit until it writes
something itself. Writes never read a stale snapshot — they re-read under the
transaction lock — and a freshly started process is always current. Two servers
driven from one client are the case this shows up in; the lifecycle suite reads
the final state through a new process for exactly that reason.
Why the v2.0.0 trie was ported as ranking
v2.0.0 replaced substring search with prefix-only matching and called it an
improvement. Measured against this catalog, it is not: "tch" matches 7
products by substring and 5 by token prefix, and "a" matches 13 against 6.
Substring matching reaches inside tech and stitch; a prefix only matches from
the start of a token. Shipping it as written would have cut recall while
looking like a speed-up.
So the index here ranks instead of filtering. filterProducts still decides the
candidate set, and the trie only orders it — a product whose own token is the
query outranks one that merely contains it, on three tiers (the query is a whole
name/SKU/category/tag, is one word of one, or starts one). The tier is what
separates Cable from Cable Management Tray: both carry a cable token, and
a token-level comparison alone lets the longer name win on accumulated prefix
matches.
Project Tango v3.0.1 — lookback window fix, restored-state validation
Bug fixes
A bug-fix release: no API changes, no new tools, no behaviour change beyond the two defects below.
smart_restock_predictor measured one day more than it reported
The sales lookback window subtracted the full salesLookbackDays from its anchor date while the window filter includes both bounds — so a 30-day lookback actually covered 31 calendar days of demand and still divided by 30. Every velocity ran about 3% high, daysOfCover ran low, and recommendedReorderQuantity came out too large.
The window now spans exactly salesLookbackDays calendar days counting the anchor day, so the reported window.start/window.end match the arithmetic that produced the figures.
Verified end to end: a product selling 1 unit/day for 30 days, plus one order a week older than the window, previously reported unitsSoldInWindow: 31, dailyVelocity: 1.033, daysOfCover: 1.9, reorder 20. It now reports 30, 1, 2, 19.
Non-finite numbers in a restored state file broke every analytics tool
The state store validated restored records with typeof x === "number", which accepts Infinity — the value JSON.parse("1e999") produces — and fractional quantities. Those passed validation, reached the catalog, and then failed the tools' own declared output schemas:
MCP error -32602: Output validation error: Invalid structured content for tool
analyze_sales_metrics: Invalid input: expected number, received Infinity
One bad field in one record turned analyze_sales_metrics, smart_restock_predictor and find_orders into hard failures for the whole dataset. JSON.stringify cannot express either value, so only a hand-edited or foreign-written state file could produce it — but the file's stated contract is that an unreadable state degrades to the seed dataset rather than taking the tools down.
Record validation now requires finite numbers, and integers where the schemas declare them, so such records are dropped and warned about like any other malformed record.
Verification
| Gate | Result |
|---|---|
npm run typecheck |
0 |
npm run lint |
0 errors, 0 suppressions |
npm run test:coverage |
210/210 tests, 42 suites, above the 98/94/96 floors |
npm run build |
0 |
| Live stdio run | 8 tools, 30-day window matches policy, write path intact |
Upgrade
Drop-in: no tool names, parameters or response fields changed. If you compare restock output against v3.0.0, reorder quantities will be slightly lower — that is the over-ordering being corrected, not a regression.
Project Tango v3.0.0 — modular restructure, scoped tools, quality gates
What's new in v3.0.0
A full restructure of the server plus two long-requested tool capabilities — every change verified by the suite and by an end-to-end run against the built server.
Tools
- Real category scoping —
get_low_stock_alerts,analyze_sales_metricsandsmart_restock_predictoraccept an optionalcategoryand echo the filter they actually applied. Theweekly_inventory_reviewprompt now scopes every step it sequences (the old "only step 1 is scoped" disclaimer is gone with the gap it covered). find_ordersbounding — new optionallimitreturns the newest N matches whiletotalMatchesalways reports the full match count. The default stays deliberately uncapped: no pagination, nothing dropped.- Honest responses throughout: blank arguments echo back as
null, zero matches are successes, and tool descriptions interpolate their policy values and tool names so prose cannot drift from code.
Architecture
createServer(dataProvider)— the whole MCP surface (8 tools, 1 prompt, 1 resource) takes itsDataProvideras an argument;index.tsis a thin stdio bootstrap. The seam is exercised in-process against stub providers.- Split into focused modules:
server.ts,reviewPrompt.ts(pure prompt renderer),responses.ts,toolNames.ts, alongside the existing analytics split.
Correctness
- Cross-process safe state writes: per-write temp files so two writers can never publish a torn
state.json, with cleanup on failed rename; a two-writer race is pinned by test. - The README's dataset figures (130 matches / 142 orders / 46 KB) are asserted against live data by
docs.test.ts.
Quality gates
- 204 tests / 42 suites / 0 fail, coverage 99.96% lines / 96.16% branches / 98.17% functions enforced by floors (98/94/96).
- Type-aware ESLint with zero suppressions; GitHub Actions CI runs typecheck → lint → coverage floors → README test-count drift check → build (all green on this commit's pipeline).
- TypeScript 6.0.3 (typescript-eslint does not support the TS 7 API yet; typecheck remains clean).
Upgrade notes
No breaking changes. All new input parameters are optional and response fields (filters, totalMatches, limit) are additive — existing clients keep working unchanged.
Full changelog: v2.0.0...v3.0.0
v2.0.0 — The Hybrid Engine
🚀 v2.0.0 — The Hybrid Engine
Project Tango is now a production-grade MCP server powered by a three-pillar hybrid architecture: SQLite persistence, prefix-trie instant search, and a reactive event broker with saga rollback.
Previous release: v1.0.0
🏗️ Architecture
┌───────────────────────────────────┐
│ Client (Claude / LLM) │
└──────────────┬────────────────────┘
│ MCP Protocol (stdio)
▼
┌───────────────────────────────────┐
│ Project Tango MCP Server │
└──────────────┬────────────────────┘
│
┌──────────┴──────────┐
▼ (Atomic Writes) ▼ (Read Cache)
┌─────────────────┐ ┌─────────────────┐
│ SQLite DB │──►│ Prefix Trie │
│ (Single Source)│ │ (O(k) Search) │
└─────────────────┘ └─────────────────┘
│ │
└────── Event Broker ───┘
(Real-time Subs)
Three Pillars
1. 🗄️ SQLite Persistence (src/database.ts)
- Full relational schema:
products,orders,order_itemswith foreign keys - WAL mode for concurrent reads, journaled writes for crash safety
- Parameterized queries — zero SQL injection risk
DataProviderinterface implemented against SQLite: all 6 provider methods
2. ⚡ Prefix Trie Search (src/trie.ts)
- In-memory Trie indexing every token from product names, SKUs, categories, and tags
- Search in O(k) where k = query length (not O(n) like a linear scan)
- Prefix matching, wildcard support, relevance scoring
- Fully atomic index rebuild on server start — always in sync with SQLite
3. 🔄 Reactive Event Broker & Saga Rollback (src/engine.ts)
- Event bus emits typed events (
product:updated,order:placed,inventory:changed) - Subscribers push
notifications/resources/updatedto the MCP client in real time - Saga rollback: on any mid-transaction failure, the engine restores the before-state — no partial mutations, no corrupted data
🛠️ 7 MCP Tools
| Tool | Description |
|---|---|
list_all_products |
Browse/search catalog with category & price filtering |
search_products 🆕 |
New! Trie-powered instant prefix search — returns results in <1ms |
get_low_stock_alerts |
Scan inventory for low/critical stock levels |
analyze_sales_metrics |
Gross revenue, AOV, top sellers, order-status breakdown |
smart_restock_predictor |
Recommends reorder quantities from demand velocity |
draft_product_copy |
Generates marketing copy + SEO tags for a SKU |
simulate_order_placement |
Places a simulated order with stock validation |
🔒 All Fixes & Hardening (v1.0.0 → v2.0.0)
Critical
- ✅
addOrdernow clones before storing — prevents caller mutation of internal state - ✅
.convex/secrets removed from tracking —adminKeyandinstanceSecretno longer at risk - ✅
prod_006data inconsistency fixed — stock 1→3, order qty 2→1 to reflect available inventory
High
- ✅ Order IDs use
crypto.randomUUID()— unique even at sub-millisecond concurrency - ✅ Saga rollback on order failure — inventory restore, cache rebuild, structured telemetry
Medium
- ✅ Input validation bounds added —
.max()on all string parameters (category,sku,customer_name,product_id) - ✅ Error boundaries —
withSanitizedErrorswrapper on all 7 tool handlers, no stack leaks - ✅
npm testscript added — runnpm testto execute the 38-check integration suite - ✅
.gitignoreupdated —node_modules/,.env*,.convex/all protected - ✅ Unused imports & dead code removed from integration test
Low
- ✅
formatZodIssues— human-readable validation error output (not raw JSON) - ✅ Stale JSDoc corrected (“four methods” → “6 methods”)
- ✅
.prettierrcadded — project-wide formatting config - ✅
as TReturncast documented with safety notes
📁 File Structure
project-tango/
├── src/
│ ├── index.ts # MCP server entry — 7 tools + reactive broker
│ ├── types.ts # Shared TypeScript interfaces
│ ├── trie.ts # Prefix trie data structure
│ ├── database.ts # SQLite connection, schema, queries, seed
│ ├── engine.ts # Hybrid engine (SQLite + Trie + Events + Saga)
│ ├── mcpHelpers.ts # withSanitizedErrors, formatZodIssues
│ └── mockData.ts # In-memory DataProvider (legacy)
├── test/
│ └── integration.test.ts # 38-check MCP integration test
├── .prettierrc
├── .gitignore
├── package.json
├── tsconfig.json
└── README.md
📊 Performance
| Operation | v1.0.0 (in-memory) | v2.0.0 (hybrid) |
|---|---|---|
| Product search (13 products) | O(n) scan | O(k) trie lookup |
| Order placement | In-memory only | Durable + atomic via SQLite WAL |
| Read isolation | Clone on read | Clone on read + primary source in SQLite |
| Crash recovery | Lost on restart | Full recovery from disk |
| Concurrent safety | Module-level mutex | Mutex + saga rollback |
🚀 Getting Started
npm install
npm start # Launch MCP server over stdio
npm test # Run 38-check integration suite
npm run build # Compile to dist/
npm run inspect # Debug with MCP InspectorNo env vars required — runs entirely locally with SQLite.
Release prepared by the v2.0.0 hardening audit and full architectural rewrite.
Production-Ready MCP Overhaul
🚀 Project Tango v1.0.0 — Production-Ready MCP Server
Welcome to the initial, stable release of Project Tango—an enterprise-grade Model Context Protocol (MCP) server designed for secure, high-performance e-commerce catalog and order management.
This release bypasses typical prototype limitations, delivering a production-ready system built with a highly testable modular architecture and bulletproof concurrency protections.
🛠️ Key Features & Architecture
Modular Domain Isolation:
Business operations (catalog search, order processing, and restocking algorithms) are isolated into pure, dependency-free functions under /logic. MCP network adapters live cleanly in /tools, enabling lightning-fast, isolated testing.
Single Source of Truth Types:
To prevent type drift, TypeScript interfaces are dynamically inferred (z.infer) directly from runtime Zod validation schemas.
Atomic Concurrency Protection:
Built-in protection against inventory overselling. The order placement flow (placeOrder) runs as an indivisible transaction secured by an asynchronous Mutex write-lock.
Rich MCP Ecosystem Surface:
Exposes 7 custom tools, including a highly optimized search_products tool utilizing multi-token partial matching (normalizes spaces, hyphens, and underscores).
Out-of-the-box support for 4 dynamic resources (catalog, orders, alerts) with autocomplete.
2 interactive prompts (inventory-briefing, sales-report) for LLM assistance.
All tool outputs deliver both natural text and validated structuredContent.
Fail-Fast Environment Configuration:
System thresholds and parameters are validated via Zod at server boot. If an environment variable is misconfigured, the server fails loudly at startup rather than corrupting data downstream.
CI/CD & Code Quality:
Protected by a suite of 43 unit and integration tests (94.5% logic coverage), a strict ESLint configuration, and a GitHub Actions workflow to run type-checks, lints, and tests on every push.