Skip to content

feat(mcp): add zift mcp subcommand exposing scanner, prompt, and Rego primitives over stdio#21

Merged
boorad merged 3 commits intomainfrom
feat/mcp-server
Apr 29, 2026
Merged

feat(mcp): add zift mcp subcommand exposing scanner, prompt, and Rego primitives over stdio#21
boorad merged 3 commits intomainfrom
feat/mcp-server

Conversation

@boorad
Copy link
Copy Markdown
Contributor

@boorad boorad commented Apr 29, 2026

Summary

Adds zift mcp — Tier 1 deep-scan transport from the deep-mode plan. Runs Zift as an MCP (Model Context Protocol) server over stdio so any agent host (Claude Code, Cursor, Continue, Cline, Zed, …) can call Zift's authz primitives. The agent host owns the model; Zift owns the authz expertise — no LLM client lives in Zift on this path.

Hand-rolled JSON-RPC 2.0 over stdio (~250 lines) instead of pulling in rmcp — the codebase is fully blocking and forcing tokio fragmentation wasn't worth it. Protocol version pinned to 2024-11-05. The MCP server is a true transport: every primitive (system prompt, output schema, candidate kind, context expansion, Rego rendering, regorus validator) is imported verbatim from the existing deep::* / rego::* modules — no parallel implementations.

Tools exposed

Tool Purpose
scan_authz Structural scan of a path inside --scan-root; returns findings + enforcement-point count
get_finding_context Expand a finding's surrounding code window (5 before / 15 after + first 20 import lines)
list_rules Enumerate the rule library (filter by language / category)
get_rule Fetch a rule's full definition (tree-sitter query, predicates, Rego template, tests)
suggest_rego Render a Rego stub for a finding (template-driven or category default; confidence-wrapped)
validate_rego Parse a Rego policy with the embedded regorus engine
analyze_snippet Render the deep-scan prompt + JSON Schema without calling any model — agent host's model produces the response

Resources exposed

URI Content
prompt://system The canonical SYSTEM_PROMPT constant (round-tripped verbatim — asserted in tests)
prompt://schema The canonical output_schema() JSON Schema
category://<auth_category> Category definition + canonical examples × 7
rule://<rule_id> One rule's full definition × every loaded rule

Notable decisions

  • Path containment: every tool that resolves a relative path canonicalizes against --scan-root and rejects results outside it. Same defense as deep::expand_finding.
  • prompt://system / prompt://schema round-trip the canonical constants verbatim — drift between the deep-scan path and the MCP path becomes a test failure, not silent inconsistency.
  • validate_rego uses embedded regorus — the dep was already present, so zero-install UX was free.
  • Streaming responses deferred — a single scan_authz payload fits comfortably in one response; revisit if multi-thousand-finding scans become a real workload.
  • No-unwrap rule for the mcp module (introduced after self-review): clippy.toml with allow-unwrap-in-tests = true, plus #![warn(clippy::unwrap_used)] on mcp/mod.rs. Production code uses .expect("…") with messages documenting why each is infallible.

Tests

  • 35 unit tests under src/mcp/* (jsonrpc framing, server dispatch, every tool, every resource).
  • 6 stdio integration tests in tests/mcp_stdio_integration.rs that spawn zift mcp as a subprocess and drive the protocol over real pipes — catches the class of regressions in-process tests can't (e.g. an accidental println! to stdout from anywhere on the call path, or a tracing subscriber that writes to stdout instead of stderr).

Test plan

  • cargo fmt --check
  • cargo clippy --all-targets -- -D warnings
  • cargo test — 253 lib + 18 + 6 (mcp_stdio_integration), all passing
  • Smoke test against a real agent host:
    echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05"}}' | cargo run -- mcp
  • Wire into a Claude Code / Cursor MCP config and call tools/list interactively

Summary by CodeRabbit

  • New Features

    • Added an MCP server mode and new CLI subcommand to run Zift as an MCP tool provider over stdio.
    • Exposes agent-facing tools for authz scanning, rule listing/lookup, Rego suggestion/validation, and snippet analysis.
    • Publishes resources for system prompts, schema, category taxonomy, and per-rule URIs.
  • Documentation

    • README updated with MCP usage, example server config, and initialization/expectations.
  • Tests

    • Added stdio integration and unit tests covering protocol, tools, and resources.

boorad added 2 commits April 29, 2026 10:02
… primitives over stdio

Implements PR 2 from plans/done/02-pr2-mcp-server.md — a Tier-1 deep-scan
transport that lets agent hosts (Claude Code, Cursor, Continue, Cline,
Zed, …) drive Zift over the Model Context Protocol. The agent host owns
the model; Zift owns the authz expertise.

Hand-rolled JSON-RPC 2.0 over stdio in src/mcp/jsonrpc.rs rather than
pulling rmcp + tokio into a fully-blocking codebase. Pinned to MCP
protocol version 2024-11-05.

Tools (7): scan_authz, get_finding_context, list_rules, get_rule,
suggest_rego, validate_rego, analyze_snippet — the last is the headline
primitive: it returns the rendered prompt + JSON Schema so the agent
host's model produces the response and Zift never sees it.

Resources: prompt://system, prompt://schema, category://<slug>,
rule://<id>. prompt://system and prompt://schema round-trip the canonical
crate::deep::prompt constants verbatim — drift between deep-scan and MCP
paths becomes a test failure.

Tests: 35 unit tests across the mcp/ module plus 6 stdio integration
tests in tests/mcp_stdio_integration.rs that spawn zift mcp as a
subprocess and drive real pipes (catches accidental println! to stdout
from anywhere on the call path).
… read loop, no-unwrap rule

- suggest_rego: build_template_vars now aliases the first extracted
  literal under plan_value, perm_value, and permission, so rules using
  those placeholders (e.g. ts-feature-gate-check, *-shiro-*, *-permissions)
  render fully instead of leaving literal {{plan_value}} in the output.
  New regression test asserts no `{{` survives.
- jsonrpc::read_request: loop over blank-line keepalives instead of
  recursing, so a peer pumping blanks can't grow the stack.
- resolve_inside_scan_root / ensure_inside_scan_root: drop redundant
  scan_root.canonicalize() on every call. The stored ctx.scan_root is
  canonicalized once at startup; trust the invariant.
- ServerContext: remove dead rules_dir field (set but never read).
- Introduce a no-unwrap-in-production rule for the mcp module:
  - Add clippy.toml with allow-unwrap-in-tests / allow-expect-in-tests.
  - Add #![warn(clippy::unwrap_used)] to mcp/mod.rs with a docstring.
  - Convert 8 production .unwrap() calls to .expect("…") with messages
    documenting why each is infallible (5 in server.rs handler dispatch,
    3 in resources.rs pretty-print).

All checks clean: cargo fmt, cargo clippy --all-targets -- -D warnings,
cargo test (253 + 18 + 6, +1 from the placeholder regression test).
@boorad boorad self-assigned this Apr 29, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 29, 2026

📝 Walkthrough

Walkthrough

Adds a stdio JSON-RPC MCP (Model Context Protocol) server to Zift, exposing seven tools, resources (system/schema prompts, category taxonomy, per-rule URIs), protocol initialization, tool/resource listing and dispatch, CLI plumbing, and integration tests for stdio operation.

Changes

Cohort / File(s) Summary
Documentation & config
README.md, clippy.toml, plans/done/02-pr2-mcp-server.md, plans/todo/00-deep-mode-overview.md
Document MCP usage and MCP zift mcp details; update cross-references; allow unwrap/expect in tests.
CLI & command wiring
src/cli.rs, src/commands/mod.rs, src/commands/mcp.rs, src/lib.rs
Add mcp subcommand with --rules-dir and --scan-root; argument parsing tests; dispatch to mcp::execute; export mcp module.
JSON-RPC framing
src/mcp/jsonrpc.rs
Implement newline-delimited JSON-RPC 2.0 wire types, read/write framing, EOF/blank-line handling, parse errors, and response constructors; includes unit tests.
MCP module & protocol types
src/mcp/mod.rs, src/mcp/protocol.rs
Declare MCP module, constants (MCP_PROTOCOL_VERSION, SERVER_NAME, SERVER_VERSION), and define initialization/tools/resources payload types and content blocks.
Resources provider
src/mcp/resources.rs
Implement list_resources and read_resource: prompt singletons, category taxonomy URIs, per-rule rule:// URIs, URI dispatch and canonical outputs; unit tests included.
Tools implementation
src/mcp/tools.rs
Implement seven tools (scan_authz, get_finding_context, list_rules, get_rule, suggest_rego, validate_rego, analyze_snippet), tool descriptors, dispatch, Rego stub rendering/validation, snippet expansion, path containment checks, and tests.
Server runtime
src/mcp/server.rs
Single-client stdio JSON-RPC loop: read/dispatch/write, handlers for initialize, ping, tools/*, resources/*, error mapping to JSON-RPC codes, and tests; defines ServerContext.
Integration tests
tests/mcp_stdio_integration.rs
Subprocess-based stdio integration test exercising initialize, tools list/call, resources list/read, validate_rego/analyze_snippet, and unknown-method error handling.

Sequence Diagram(s)

sequenceDiagram
    participant Agent as Agent
    participant Stdio as Zift MCP Server (stdio)
    participant Dispatcher as Dispatcher
    participant Tools as Tools Module
    participant Resources as Resources Module

    Agent->>Stdio: initialize (JSON-RPC)
    Stdio->>Dispatcher: parse & handle initialize
    Dispatcher-->>Stdio: initialize response

    Agent->>Stdio: tools/list (JSON-RPC)
    Stdio->>Dispatcher: dispatch -> list_tools()
    Dispatcher->>Tools: list_tools()
    Tools-->>Dispatcher: ToolsListResult
    Dispatcher-->>Stdio: tools/list response

    Agent->>Stdio: tools/call validate_rego (JSON-RPC)
    Stdio->>Dispatcher: dispatch -> validate_rego
    Dispatcher->>Tools: validate_rego(args)
    Tools->>Tools: validate via embedded regorus
    Tools-->>Dispatcher: ToolsCallResult
    Dispatcher-->>Stdio: tools/call response

    Agent->>Stdio: resources/list (JSON-RPC)
    Stdio->>Dispatcher: dispatch -> list_resources()
    Dispatcher->>Resources: list_resources(ctx)
    Resources-->>Dispatcher: ResourcesListResult
    Dispatcher-->>Stdio: resources/list response

    Agent->>Stdio: resources/read (JSON-RPC)
    Stdio->>Dispatcher: dispatch -> read_resource(uri)
    Dispatcher->>Resources: read_resource(ctx, uri)
    Resources-->>Dispatcher: ResourceContent
    Dispatcher-->>Stdio: resources/read response

    Agent->>Stdio: close stdin (EOF)
    Stdio->>Stdio: clean shutdown
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hop on pipes of newline light,

tools and prompts to help you write,
JSON-RPC in tidy rows,
rules and schemas, tip-toe grows,
stdio hums — a rabbit's delight.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely summarizes the main change: adding a new zift mcp subcommand that implements an MCP (Model Context Protocol) server exposing core scanner, prompt, and Rego functionality over stdio.
Docstring Coverage ✅ Passed Docstring coverage is 80.39% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Review rate limit: 4/5 reviews remaining, refill in 12 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@amazon-q-developer amazon-q-developer Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR implements a comprehensive MCP (Model Context Protocol) server for Zift, exposing authz scanning primitives over JSON-RPC 2.0/stdio. The implementation is well-architected with proper security measures and extensive test coverage.

Key strengths:

  • Proper path traversal protection via canonicalization checks in scan_root boundaries
  • Clean separation between transport layer (MCP module) and business logic (scanner/deep modules)
  • Comprehensive error handling with graceful degradation (continues serving after parse errors)
  • No-unwrap policy enforced with .expect() + justification messages
  • Extensive test coverage (35 unit tests + 6 stdio integration tests)
  • Protocol compliance verified with real subprocess integration tests

Test coverage highlights:

  • JSON-RPC framing and error handling
  • All 7 tools with path containment validation
  • Resources (prompt://system, prompt://schema) round-trip canonical constants
  • Subprocess integration catches stdout contamination regressions

The code is production-ready and follows Rust best practices throughout.


You can now have the agent implement changes and create commits directly on your pull request's source branch. Simply comment with /q followed by your request in natural language to ask the agent to make changes.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Line 127: The README's initialize smoke-test expectation is incorrect: it
asserts that "the seven tools are listed in the capabilities" though the
initialize response only contains capability flags; tool descriptors are
returned by the tools/list endpoint. Update the wording around the initialize
expectation (the sentence mentioning serverInfo.name == "zift") to state that
initialize returns capability flags (not tool descriptors), and add/clarify that
the seven tool descriptors are available from the tools/list API (referencing
tools/list) rather than from initialize.

In `@src/mcp/resources.rs`:
- Around line 43-50: The ResourceDescriptor for category URIs advertises
mime_type "text/plain" but read_resource returns ResourceContent with
"application/json", causing inconsistency; update the descriptor creation in the
loop over ALL_CATEGORIES (the ResourceDescriptor constructed with uri:
format!("category://{}", category_slug(*category)), name: format!("AuthCategory:
{category}"), description: category_description(*category).to_string(),
mime_type: "text/plain") to use mime_type "application/json" to match
read_resource's ResourceContent output (and ensure any consumers expecting JSON
are satisfied), or alternatively change read_resource's returned ResourceContent
mime_type to "text/plain" if the content is actually plain text — pick the JSON
option and make the mime_type values consistent between ResourceDescriptor and
read_resource.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d44ce492-94c6-4d09-89b0-48388689485b

📥 Commits

Reviewing files that changed from the base of the PR and between 828b29a and a5cfb18.

📒 Files selected for processing (15)
  • README.md
  • clippy.toml
  • plans/done/02-pr2-mcp-server.md
  • plans/todo/00-deep-mode-overview.md
  • src/cli.rs
  • src/commands/mcp.rs
  • src/commands/mod.rs
  • src/lib.rs
  • src/mcp/jsonrpc.rs
  • src/mcp/mod.rs
  • src/mcp/protocol.rs
  • src/mcp/resources.rs
  • src/mcp/server.rs
  • src/mcp/tools.rs
  • tests/mcp_stdio_integration.rs

Comment thread README.md Outdated
Comment thread src/mcp/resources.rs
…IME type

- README: clarify that `initialize` returns capability flags, not tool
  descriptors; tool list comes from `tools/list`.
- resources.rs: align ResourceDescriptor mime_type for category:// URIs
  with the application/json content returned by read_resource.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/mcp/resources.rs (1)

119-223: Consider consolidating category metadata into a single static table.

ALL_CATEGORIES, category_slug, category_from_slug, category_description, and category_examples are tightly coupled and can drift over time. A single metadata source would reduce maintenance risk.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp/resources.rs` around lines 119 - 223, The current code duplicates
category data across ALL_CATEGORIES, category_slug, category_from_slug,
category_description, and category_examples which can drift; replace these with
a single static metadata table (e.g., a static array/slice of a CategoryMeta
struct containing fields: kind: AuthCategory, slug: &'static str, description:
&'static str, examples: &'static [&'static str]) and update the functions
ALL_CATEGORIES (or remove and derive it from the table), category_slug,
category_from_slug, category_description, and category_examples to read from
that table (lookup by AuthCategory or slug) so all metadata lives in one source
of truth and the utilities simply index into it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/mcp/resources.rs`:
- Around line 119-223: The current code duplicates category data across
ALL_CATEGORIES, category_slug, category_from_slug, category_description, and
category_examples which can drift; replace these with a single static metadata
table (e.g., a static array/slice of a CategoryMeta struct containing fields:
kind: AuthCategory, slug: &'static str, description: &'static str, examples:
&'static [&'static str]) and update the functions ALL_CATEGORIES (or remove and
derive it from the table), category_slug, category_from_slug,
category_description, and category_examples to read from that table (lookup by
AuthCategory or slug) so all metadata lives in one source of truth and the
utilities simply index into it.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b86f38c2-3f9a-43a5-b903-7b6bf060eded

📥 Commits

Reviewing files that changed from the base of the PR and between a5cfb18 and cc464d7.

📒 Files selected for processing (2)
  • README.md
  • src/mcp/resources.rs

@boorad boorad merged commit 54e94bd into main Apr 29, 2026
2 checks passed
@boorad boorad deleted the feat/mcp-server branch April 29, 2026 16:12
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