Skip to content

refactor(cli,mcp): port the CLI and MCP server from C to Zig - #163

Merged
ata-sesli merged 1 commit into
mainfrom
refactor/cli-mcp-zig
Nov 5, 2026
Merged

ata-sesli merged 1 commit into
mainfrom
refactor/cli-mcp-zig

Conversation

@ata-sesli

Copy link
Copy Markdown
Member

What does this PR do?

Ports the CLI (src/cli) and the MCP server (src/mcp) from C to Zig. These were the last two large C modules on the Zig port list. They call into each other, so they move together.

The C headers do not change. cli.h, progress_sink.h, mcp.h and index_supervisor.h remain the ABI. Each Zig module exports the same symbols, and cbm_zig.zig checks every exported cbm_* function against its header declaration at compile time. A signature that drifts from its header fails the build.

Removed C Replaced by
src/cli/cli.c, hook_augment.c, progress_sink.c (6,206 lines) src/cli/{cli,commands,archive,store,tool_args,hook_augment,progress_sink}.zig (4,506 lines)
src/mcp/mcp.c, index_supervisor.c (9,220 lines) src/mcp/{mcp,server,search_tools,code_tools,graph_tools,project_tools,index_tools,index_supervisor}.zig (7,311 lines)

Net change: −3,536 lines.

Behavior differences

  • search_code sorts hits (by file, then by score) with a stable sort. qsort left the order of tied hits unspecified.
  • get_architecture frees scope_path when the store fails to open. The C code leaked it.
  • A JSON-RPC request with no method gets "Method not found". The C code dereferenced NULL.
  • Request durations use cbm_now_ns() (monotonic) instead of clock_gettime.
  • Not ported: the unreachable enrich_connected helper, and the alternatives parameter of build_snippet_response, which every caller passed as NULL.

Build and audit wiring

  • Makefile.cbm and build.zig drop the C sources and list the Zig files. scripts/check-build-sources.sh passes.
  • cbm_zig_c.h adds the headers the ported code calls.
  • security-audit.sh and security-allowlist.txt name the Zig files instead of cli.c and mcp.c.
    • The MCP file-read audit now counts all of src/mcp/*.zig, and its limit is 18 reads, up from 13. Reviewers should check this: mcp.c alone already had 19 matching lines, and index_supervisor.c was never counted. All 18 are path-contained reads or transport reads.
    • The old cli.c allowlist justification ("sha256 checksum") was stale. The call it covers is the latest-release curl lookup, and the new entry says so.
  • Test and smoke scripts that grep the old C files now grep the Zig files.

Verification

Checks run on macOS arm64:

  • The mcp suite passed 176 tests, the same count as the earlier p119 baseline run. The cli suite passed 132. Both ran through scripts/zova-run-tests.sh.
  • libcbm_zig.a builds for native, x86_64-linux-gnu, aarch64-linux-gnu, x86_64-windows-gnu and aarch64-macos.
  • zig fmt --check, scripts/check-build-sources.sh and scripts/security-audit.sh pass.

Not run:

  • the full test suite (make -f Makefile.cbm test);
  • lint-ci;
  • smoke-test.sh;
  • any runtime test on Windows. The Windows paths are compile-checked only.

Checklist

  • Every commit is signed off (git commit -s) — required, CI rejects
    unsigned commits (DCO, see CONTRIBUTING.md)
  • Tests pass locally (make -f Makefile.cbm test): only the mcp and cli suites were run
  • Lint passes (make -f Makefile.cbm lint-ci): not run
  • New behavior is covered by a test (reproduce-first for bug fixes): no new behavior; the existing suites cover the ported code

Replace src/cli/{cli,hook_augment,progress_sink}.c and
src/mcp/{mcp,index_supervisor}.c with Zig modules in libcbm_zig. The C
headers (cli.h, progress_sink.h, mcp.h, index_supervisor.h) stay the
ABI: each Zig module exports the same symbols, and cbm_zig.zig checks
every pub cbm_* export against its translated header declaration, so a
signature that drifts from its header fails the build.

The two modules call into each other, so they move together:

- src/cli: cli.zig, commands.zig, archive.zig, store.zig, tool_args.zig,
  hook_augment.zig, progress_sink.zig.
- src/mcp: mcp.zig (JSON-RPC, tool registry, arguments, server object,
  store resolution), server.zig (dispatch, compact output,
  detect_changes, session auto-index and update check, request handler,
  stdio loop), search_tools.zig, code_tools.zig, graph_tools.zig,
  project_tools.zig, index_tools.zig, index_supervisor.zig.

Behavior differences from the C code:

- search_code sorts its hits (by file, then by score) with a stable
  sort; qsort left the order of ties unspecified.
- get_architecture frees scope_path when the store fails to open; the C
  code leaked it.
- An unreachable enrich_connected helper and the always-NULL
  alternatives parameter of build_snippet_response are not ported.
- A JSON-RPC request with no method gets "Method not found"; the C code
  dereferenced NULL.
- Request durations use cbm_now_ns() (monotonic) instead of
  clock_gettime.

Build and audit wiring: Makefile.cbm and build.zig drop the C sources
and list the Zig files; cbm_zig_c.h gains the headers the ported code
calls. security-audit.sh and its allowlist name the Zig files in place
of cli.c and mcp.c; the MCP file-read audit now counts src/mcp/*.zig
(18 reads, all path-contained or transport reads). Test and smoke
scripts that grep the old C files now grep the Zig files.

Net change: -3,536 lines (src/cli -1,700, src/mcp -1,909).

Signed-off-by: ata-sesli <atasesli@protonmail.com>
@ata-sesli
ata-sesli merged commit bc4f3db into main Nov 5, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant