Phase 1: MCP design, contracts, and ADR - #1314
Merged
neuromechanist merged 10 commits intoSep 8, 2026
Merged
neuromechanist merged 10 commits into
neuromechanist merged 10 commits into
Conversation
Prerequisite for the MCP SDK's @modelcontextprotocol/hono peer (^4.11.4); previously pinned ^4.6.0. typecheck and the backend suite are green at this version.
The single-argument z.record(valueSchema) form is invalid under zod 4 (z.record(keySchema, valueSchema) is required); the two-argument form is valid under zod 3 too, so this is a safe fix regardless of which major version the repo ends up on. Found while gating the phase 1 MCP contracts (issue #1293) against a zod 4 bump.
Captured with curl -sL -A "nemar-cli/mcp-phase1" (issue #1293): - zarr-index-nm000329-slice.json: first 3 stores of the live index, keeping events_parquet and top-level doi/license/citation. - zarr-index-on003392-meg-sss-slice.json: the one live MEG store carrying an ADR 0028 sss object. - zarr-catalog-slice.json: 3 entries from zarr.nemar.org/catalog.json. - zarr-array-level0.zarr.json / zarr-array-view1.zarr.json: the level-0 and view/1 array metadata documents for nm000329's eeg_250hz group, matching the epic's verified geometry.
Issue #1293, phase 1 of epic #1065. shared/contract/zarr-index.ts is a .passthrough() zod reader mirroring shared/zarr-index.schema.json (the producer's closed contract stays the JSON Schema; this is a client-side lower bound on it, matching dataset.ts's convention). shared/contract/mcp.ts is the MCP wire vocabulary ADR 0049 fixes: the provenance envelope every tool response carries, buildReadRecipe() (recipe-first read_window, per the plan), and one input/output schema pair per tool (search_datasets, describe_dataset, list_recordings, get_events, render_overview, read_window), including the taste cap (60s x 64 channels) read_window's schema enforces. Both contract-*.unit.test.ts files exercise the public schema/function surface against real fixtures (on008083, and the two live slices), including three deliberate mutation tests (dropped source_commit, format_version 1, non-hex commit) proving the zod reader rejects what the Ajv-compiled JSON Schema also rejects.
Issue #1293 step 5. Answers the two things the phase 1 plan left open: does @modelcontextprotocol/server@2.0.0 + hono@2.0.0 serve both protocol eras under workerd (wrangler dev), and which blosc/ zstd decode path (decision 7) actually runs there. Both eras of the transport work: server/discover, tools/list, tools/call in the 2026-07-28 envelope, the legacy initialize handshake, a legacy tools/call with no envelope, the -32020 rejection (disagreeing Mcp-Name, missing Mcp-Method), and GET/ DELETE 405 all behave as documented -- see smoke.sh. numcodecs' WASM Blosc codec does not run under workerd (dynamic WebAssembly.instantiate() on a runtime-fetched buffer is disallowed by the embedder); the pure-JS decoder in decode.ts (blosc2 header parse + fzstd + unshuffle) works and matches a Python-derived ground truth exactly. Path (b) is the chosen decode primitive. fixtures/chunk.bin is one real inner chunk of nm000329's level-0 array, captured via two range requests against the shard's trailing index; README.md documents the byte-level derivation and the full measurement set (bundle size, wall time). Replaced by phase 2 (#1294) and deleted in that PR.
Issue #1293. Purpose and constraints, the 2026-07-28 protocol revision and dual-era behavior (with the exact SEP-2243 header/ envelope mechanics verified against a real wrangler dev run, not just the spec summary), host and routing (phase 2 scope), the six- tool surface with cost class and cache behavior per tool, the envelope and read-recipe field tables, compute-minimization rules and the cache key scheme, the analytics event shape, the pinned dependency table with the spike's verdicts, the client compatibility table, and open items for phases 2-5. ADR 0050 is omitted from this PR per decision 11: origin/dev does not yet carry ADR 0049 (PR #1292 is still open). The decision text lives in this document instead; phase 5 files the ADR once #1049 has landed.
The design doc drifted from plan decision 4 (cache the rendered overview PNG keyed by width, not the decoded level) and from its own schema (an over-cap taste is rejected by the input schema, not downgraded to a recipe). The zarrita snippet now uses the documented FetchStore form; zarrita's open.v3 takes a store, not a URL string.
📦 Package PreviewYour package is ready to test! bun install -g nemar-cli@PR1314Version: 🤖 Updated on each push. |
2 tasks
decodePathB now refuses a short buffer, a non-int16 typesize, a zero blocksize (blosc2 marks a single block with blocksize == nbytes, never 0), a cbytes that disagrees with the input, offsets or lengths past the end of the chunk, and an odd decoded byte length that Int16Array would silently floor. Offsets and lengths are read unsigned. smoke.sh checks the HTTP status on every call, keeps bodies in a mktemp dir it removes on exit, and kills workerd along with wrangler. Smoke run: 10/10 PASS.
- a catalog row's null doi/license is authoritative over the index's stale hoisted copy; the index is consulted only without a row - fillTemplate fills layout templates in one pass and throws on a template that lacks a placeholder the caller needs - buildReadRecipe takes Zarr's own array metadata (data_type), with a zarrArrayMetadataSchema reader for the served arrays' zarr.json - a taste requires channels: the schema cannot know n_channels and a live MEG store has 320 - sss is present exactly when derived is true, enforced on the store reader and the envelope; sss models the converter's known keys - ranges enforce end >= start; flagToBoolean is the one 0|1|null to boolean conversion; min_participants dropped (no server-side filter) - tests: 26 -> 51, covering every tool input default and cap, both read_window output branches, the catalog document, the real zarr.json fixtures, and each fallback branch of the envelope
search_datasets loses min_participants and names the two catalog routes it wraps; read_window's taste requires channels; the envelope table states the catalog-over-index rule; the recipe table splits dtype from codecs; section 12 records how phase 2 registers tools across the zod 3 / zod 4 split (zod4 npm alias plus a JSON Schema drift test).
Contributor
Author
|
Review pass (code, tests, comments, types, silent failures) folded into three commits on top of the implementer's work:
|
neuromechanist
merged commit Sep 8, 2026
465134f
into
feature/issue-1065-epic-mcp-server
20 of 23 checks passed
This was referenced Sep 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 1 of epic #1065 (issue #1293): design doc, zod contracts for the zarr index v3 reader and the MCP server's wire vocabulary, and a transport/decode spike. No route exists yet -- phases 2-4 (#1294-#1296) build the host fork and the tools.
Closes #1293
Part of epic #1065
Deliverables
.context/mcp-server-design.md-- purpose/constraints, protocol revision and dual-era behavior (verified against a realwrangler devrun), host/routing, the six-tool surface, envelope/recipe field tables, compute-minimization rules and cache key scheme, analytics event shape, dependency table with spike verdicts, client compatibility table, phase 2-5 open items.shared/contract/zarr-index.ts--.passthrough()zod reader mirroringshared/zarr-index.schema.json, plustest/contract-zarr-index.unit.test.ts(every real fixture passes both Ajv and the zod reader; three mutations fail both; the lower-bound-vs-closed-contract divergence is itself tested).shared/contract/mcp.ts-- the provenance envelope,buildReadRecipe(), and one input/output schema pair per tool, plustest/contract-mcp.unit.test.ts.curl -sL -A "nemar-cli/mcp-phase1":zarr-index-nm000329-slice.json,zarr-index-on003392-meg-sss-slice.json(the one live MEG store carrying an ADR 0028sssobject),zarr-catalog-slice.json,zarr-array-level0.zarr.json,zarr-array-view1.zarr.json.backend/spike/mcp-transport/-- a self-contained Worker (ownpackage.json/wrangler.toml) proving the transport and settling decision 7's decode path under a realwrangler devrun. Replaced by phase 2 (Phase 2: MCP host fork and discovery tools #1294) and deleted in that PR.Spike measurements
server/discover,tools/list,tools/call(2026-07-28 envelope), legacyinitialize, legacytools/callwith no envelope,-32020(disagreeingMcp-Name, missingMcp-Method),GET/DELETE405 -- all correctnumcodecsnumcodecstools/listwall time (local)tools/call decode_chunkwall time (local)numcodecsWASM BloscRuntimeError: Aborted(CompileError: WebAssembly.instantiate(): Wasm code generation disallowed by embedder). Dynamic WASM compilation from a runtime-fetched buffer is exactly what workerd's embedder blocks; the npm package also ships no.wasmfile to statically import instead.fzstd+ unshuffle)fzstdalso servesevents.parquetviahyparquet-compressors.Full derivation (including how
fixtures/chunk.binwas captured via two range requests against the shard's trailing index) is inbackend/spike/mcp-transport/README.md.zod / hono gate (decision 10)
Attempted per plan step 5: bump
zodto^4.2.0(root + backend) andhonoto^4.11.4(backend), thenbun install && bun test && bun run typecheck.honobump: kept. Unconditional per decision 10;bun run typecheckand the backend suite are green.zodbump: reverted.bun run typecheckwent green after two mechanical fixes (z.record(valueSchema)->z.record(z.string(), valueSchema)insrc/lib/upload-progress.tsand two call sites inbackend/src/routes/admin/doi.ts-- kept regardless of the revert, since the two-arg form is valid under zod 3 too). Butbun testsurfaced a real incompatibility:backend/test/openapi-document.test.ts's drift guard failed withstrictVersionTagSchema.openapi is not a function. Traced to a genuine zod-4/@asteasolutions/zod-to-openapiorder dependency --extendZodWithOpenApi(z)'s prototype patch is no longer retroactive under zod 4, so a contract schema imported before the patch runs never gets.openapi(). Bumpingzod-to-openapiitself to9.1.0(which declareszod: "^4.0.0"as a peer) did not change this. A per-file import reorder fixes the standalone script, butbun testrunstest/andbackend/test/in ONE process, and dozens of other files importshared/contract/*before this test's own module graph runs -- closing that gap for real needs abunfig.tomltest-preload hook the repo does not have today. That's new test infrastructure, not a mechanical fix, so per decision 10's fallback the bump was reverted: this phase's contracts stay on zod 3, and the MCP SDK's own nested zod 4 copy is what the SDK uses (the "two-copy risk" decision 10 named). Full derivation in.context/mcp-server-design.mdsection 10.1.bun testat HEAD: 5551 pass / 111 skip / 49 fail, identical failure set to a baseline run before any of this PR's dependency changes (diffed line-for-line -- the one difference between the two runs was the openapi drift-guard failure, which the revert fixed). Every remaining failure is a pre-existing environment gap in this worktree (missingtest/.env.test/TEST_ADMIN_API_KEY/TEST_USER_API_KEY, missing AWS credentials, one D1 SQLite table gap in an ephemeral test context) -- none touch zod, hono, or anything this PR changes.bun run typecheckis clean.ADR 0050
Omitted from this PR per decision 11.
origin/devdoes not yet carry ADR 0049 (PR #1292 is still open) -- re-checked immediately before opening this PR. The decision text (protocol/stack, access, compute-minimization, decode path, and the zod/hono gate outcome) lives in.context/mcp-server-design.mdinstead; phase 5 files the ADR once #1292 lands.Deviations from the plan
feature/issue-1065-epic-mcp-serverdid not exist onorigin. It existed only as a local branch in a sibling worktree (/Users/yahya/Documents/git/nemar/epic-mcp-server), unpushed. Pushed it as-is (no new commits, just the existing ref) so this PR could target it as specified, rather than redesigning around a different base.origin/devat PR time.Test plan
bun test test/contract-zarr-index.unit.test.ts test/contract-mcp.unit.test.ts-- greenbun run typecheck-- greenbash backend/spike/mcp-transport/smoke.sh-- all required checks PASSbun test test/adr-index.unit.test.ts-- green (no ADR added this PR, still consistent)bun testat repo root -- green modulo pre-existing environment-gap failures (see above), diffed against a pre-change baseline to confirm no regressions