docs/adr/ADR-264-rvagent-mcp-and-cli-npm-deep-review.md
@ruvnet/rvagent MCP Server + @ruv/ruview-cli — Deep Review + Optimization Strategy| Field | Value |
|---|---|
| Status | Accepted — implemented (O1–O9, @ruvnet/[email protected]): exports fixed (types-first, no phantom .cjs), map-free tarball (127,704 B unpacked / 46 files / 0 maps — MEASURED, npm pack --dry-run, from 188 kB), Streamable HTTP wired behind RVAGENT_HTTP_PORT with per-session transports + 1 MiB body cap + port-aware origin gate, underscore tool names with dotted router aliases, single Zod validation gate with generated JSON Schemas, fd-leak fixed + persisted job records + bounded log tails, probing detectCogBinary, package.json-sourced version, ruview-cli bin renamed. 99/99 jest tests (MEASURED); both transports smoke-tested live |
| Date | 2026-07-02 |
| Deciders | ruv |
| Codename | RUVIEW-NPM-REVIEW-2 |
| Supersedes / amends | none (reviews the ADR-104/ADR-124 artifacts; feeds ADR-265 distribution strategy) |
Two TypeScript npm packages expose RuView sensing to agents and shells:
@ruvnet/[email protected] (tools/ruview-mcp/) — SENSE-BRIDGE, the MCP
server over the sensing-server HTTP API + cog binaries: 12 tools
(csi/pose/count/registry/train/job + ADR-124 BFLD/presence/vitals). Published
(188 kB unpacked — MEASURED, npm view @ruvnet/rvagent). Deps:
@modelcontextprotocol/sdk + zod.@ruv/[email protected] (tools/ruview-cli/) — private: true yargs CLI
mirroring the same capabilities; intentionally duplicates http.ts/cog.ts/
config.ts (~150 lines) to stay standalone.This ADR records a deep review of both: packaging correctness (verified against the published tarball, not just the source tree), protocol/interop, resource lifecycle, and the honesty of the package's own self-description — the same MEASURED-vs-CLAIMED bar the project applies to accuracy numbers.
require condition points at a file that does not existpackage.json exports["."].require = "./dist/index.cjs", but the build is
plain tsc (ESM only) and the published 0.1.0 tarball contains no
index.cjs (verified by listing the registry tarball). Any CJS consumer doing
require('@ruvnet/rvagent') resolves to a nonexistent file →
ERR_MODULE_NOT_FOUND. Additionally the types condition is listed after
import/require; TypeScript requires types first or it may be ignored under
moduleResolution: bundler/node16.
The 0.1.0 tarball ships 44 .map files = 62,698 B against 78,209 B of
actual .js (MEASURED, extracted registry tarball). src/ is not published, so
every sourceMappingURL points at ../src/*.ts that consumers do not have —
the maps can never resolve. Also files lists CHANGELOG.md, which does not
exist in tools/ruview-mcp/ (npm silently skips it), so the advertised file set
is partly fictional.
The description reads "dual-transport MCP server (stdio + Streamable HTTP)",
but main() in src/index.ts wires stdio only. http-transport.ts is a
complete, tested scaffold that nothing imports at runtime — there is no flag,
env var, or subcommand that starts it. By this project's own rule this is a
CLAIMED capability presented as shipped. Either wire it (--http /
RVAGENT_HTTP_PORT gate) or de-claim the description until it is.
Six tools use ruview_snake_case; six (ADR-124 additions) use
ruview.dotted.names. Same interop caveat as ADR-263 F9 (host tool-name
regexes commonly ^[a-zA-Z0-9_-]{1,64}$), plus the split convention makes the
tool surface look like two products. Standardize on underscores and accept the
dotted forms as aliases for one deprecation cycle.
CallToolRequestSchema handler runs TOOL_INPUT_SCHEMAS[name].safeParse(args),
then each tool handler runs its own schema.parse(args) again — two full Zod
passes per call. Separately, the inputSchema JSON advertised via tools/list
is hand-written and duplicates the Zod schema field-by-field (defaults,
min/max, descriptions) — schema drift between what is advertised and what is
enforced is a matter of time. Parse once at the gate, pass the typed result to
handlers, and generate the advertised JSON Schema from the Zod source
(zod-to-json-schema at build time, or Zod 4's native z.toJSONSchema when the
SDK's peer range allows).
train_count leaks 2 fds per job; job registry is process-localtrainCount opens logFdOut/logFdErr with openSync and never closes them
in the parent — the spawned cargo child inherits duplicates, but the parent's
descriptors stay open for the MCP server's lifetime: 2 leaked fds per training
job. jobRegistry is an in-memory Map, so ruview_job_status after a server
restart reports "not found" for a training run that is still burning GPU (the
source comments acknowledge this; the fix — persist ~/.ruview/jobs/<id>.json,
already the documented layout — is small). Also jobStatus re-imports
node:fs on every poll and reads the entire log to return 20 lines.
http-transport.ts buffers the request body with no size cap (memory DoS the
moment it is wired to a socket), reuses a single
StreamableHTTPServerTransport with sessionIdGenerator for all clients (the
SDK's stateful mode expects one transport per session — a second client's
initialize collides), and the Origin allowlist is exact-match
(http://localhost will not match a real browser origin http://localhost:5173).
Must be fixed before F3 wires it in; bearer-token + 127.0.0.1 defaults are
already right.
detectCogBinary always returns the bare nameIt builds a 4-candidate appliance-path array and then returns
candidates[candidates.length - 1] — i.e. always name — without checking
existence. The candidates are dead weight that reads as if path detection
happens. Either probe with existsSync or delete the array.
PACKAGE_VERSION = "0.1.0" (index.ts) duplicates package.json;
@types/express is unused (http-transport uses node:http); @types/jest@30
against jest@29; ruview-cli hardcodes .version("0.0.1"). And
@ruv/ruview-cli claims the ruview bin name, which collides with
@ruvnet/ruview's bin (ADR-182) if both are ever installed globally —
ADR-263/265 give the ruview name to the harness; the CLI must rename or fold.
exports: drop the require condition (ESM-only is fine for
a bin-first package) or add a real CJS build; put types first. Add a CI
smoke test that does npm pack + node -e "import('<tarball install>')".declarationMap: false, sourceMap: false
in a tsconfig.build.json used by prepack (or add !dist/**/*.map to
files). Remove the phantom CHANGELOG.md entry or create the file.
Acceptance: unpacked size ≤ ~125 kB (from 188 kB — MEASURED, npm pack --dry-run).RVAGENT_HTTP_PORT or --http), after F7 fixes: per-session transport map
keyed by mcp-session-id, 1 MiB body cap, origin matching that honors ports
(compare URL.origin prefixes or document exact origins). Until then, change
the description to "stdio MCP server (Streamable HTTP scaffold, unwired)".ruview_bfld_last_scan, …),
keep dotted aliases in the call router for one release, note it in the README.inputSchema generated from Zod at build time.closeSync post-spawn — the
child holds its own copies), persist job records to
<jobsDir>/<id>.json, and read log tails with a bounded read.detectCogBinary actually probe (existsSync over the
candidates) — it is the entire reason the function exists.@types/express;
align @types/jest with jest 29 (or move to node:test like the harness and
drop the jest toolchain entirely — it is the heaviest devDep in both
packages).@ruv/ruview-cli into rvagent as a second bin
(rvagent-cli) sharing http/cog/config, or keep it private-forever and say
so in its README. Its ruview bin name is surrendered to @ruvnet/ruview
either way.npm pack --dry-run asserted file list
(no .map, no phantom entries), pack-size budget in CI (ADR-265), jest/node --test suite green, and a tarball-install smoke test for both import and
the rvagent bin.