code-context-gate
# code-context-gate
An MCP server that acts as a **context-aware code retrieval broker** for AI coding agents. It sits between the agent and your codebase, enforcing backpressure so the agent never receives more context than it can use.
---
## Why use it?
Without a gate, AI agents call raw `grep`/`find`/`cat` on source files. On a large codebase that means:
- A single broad search returns 800 lines from 60 files — flooding the context window
- Irrelevant comments, tests, and vendored code push the signal out of the model's attention
- There is no token budget, no relevance ranking, and no guidance on how to narrow a bad query
`code-context-gate` solves this with four disciplined tools that **rank**, **gate**, and **budget** every result before it reaches the agent. Over-broad queries get a structured `refine` refusal with a per-directory hit distribution and actionable suggestions — so the next call is deterministic.
| Scenario | Without gate | With gate |
|---|---|---|
| Broad search ("auth") | 800 hits across 60 files, full context dump | Refusal with distribution; agent narrows to `src/auth/` in one round-trip |
| Large codebase (500+ files) | Agent reads several wrong files before finding the right one | `locate` returns the 3 most relevant paths |
| Cross-file relationships | Must read every candidate file to infer callers | `explore` returns the call chain directly |
| Path traversal | No protection | `resolveSafe()` hard-blocks any path outside project root |
### Relationship to codebase-memory-mcp
`code-context-gate` can connect to a graph backend (`codebase-memory-mcp` or `codegraph`) for semantic symbol lookup and architecture overviews. But the gate's core value — ranking, backpressure, token budgeting, block expansion — is its **own**, independent of any graph backend. Even with `--graph-backend none`, it outperforms raw file access on any codebase where result volume is a concern.
```
codebase-memory-mcp ← semantic understanding (what code MEANS)
↑ consumed by
code-context-gate ← budget discipline (HOW MUCH reaches the agent)
↑ consumed by
AI agent
```
---
## How it works
Every tool call flows through the same pipeline:
```
Tool call (locate / read / explore / search)
└─ Dispatcher — routes by query shape (symbol → graph, phrase → merge, pattern → fileio)
├─ Graph adapter — codebase-memory-mcp or codegraph (optional)
└─ FileIO adapter — async line-by-line file scan with gitignore support
└─ Shaper — BM25 lexical scoring + kind weighting → ranked ResultItem[]
└─ Gatekeeper — enforces maxFiles / maxHits / maxTokens / minRelevance
└─ OkEnvelope (results fit budget)
└─ RefineEnvelope (too broad — includes distribution + suggestions)
```
**Query shape routing** in the dispatcher:
| Query shape | Example | Strategy |
|---|---|---|
| Symbol (single word) | `OrderHandler` | Graph first → FileIO fallback on miss |
| Phrase (has spaces) | `payment retry logic` | FileIO + graph merged, deduped by file:line |
| Pattern (regex chars) | `retry.*count` | FileIO regex scan only |
---
## Installation
```bash
npx code-context-gate --project-root /path/to/your/project
```
Or install globally:
```bash
npm install -g code-context-gate
code-context-gate --project-root .
```
---
## MCP Configuration
Add to your MCP client config (e.g. Claude Desktop `claude_desktop_config.json` or Claude Code `.mcp.json`):
```json
{
"mcpServers": {
"code-context-gate": {
"command": "npx",
"args": ["code-context-gate", "--project-root", "/path/to/project"]
}
}
}
```
### With a graph backend
```json
{
"mcpServers": {
"code-context-gate": {
"command": "npx",
"args": [
"code-context-gate",
"--project-root", "/path/to/project",
"--graph-backend", "codebase-memory-mcp"
]
}
}
}
```
---
## CLI flags
| Flag | Default | Description |
|---|---|---|
| `--project-root` | _(see below)_ | Root directory to search within |
| `--config` | _(none)_ | Path to a JSON config file |
| `--graph-backend` | `codebase-memory-mcp` | `codebase-memory-mcp`, `codegraph`, or `none` |
| `--debug` | `false` | Write structured JSON logs to stderr |
### Project root resolution order
1. `--project-root` CLI flag
2. `CODE_CONTEXT_GATE_ROOT` environment variable
3. `process.cwd()` — the working directory of the process that launched the server
For stdio-mode MCP clients (Claude Code CLI, Cursor, etc.) option 3 already points at the correct project root. For GUI clients (Claude Desktop) set the env var in your MCP config:
```json
{
"mcpServers": {
"code-context-gate": {
"command": "npx",
"args": ["code-context-gate"],
"env": { "CODE_CONTEXT_GATE_ROOT": "/path/to/your/project" }
}
}
}
```
---
## Tools
### `locate` — find where something lives
Returns file paths and line ranges only — no source bodies. Use first, before `read`.
```json
{ "query": "OrderHandler", "kind": "definition", "scope": "src/" }
```
| Parameter | Type | Description |
|---|---|---|
| `query` | string | Symbol name or concept |
| `kind` | `definition` \| `reference` \| `any` | Filter by result kind (default: `any`) |
| `scope` | string | Subdirectory to restrict the search |
| `max_results` | number | Override the default hit cap |
### `read` — get the actual source
Reads a symbol by name or an explicit `file:start-end` range. Block-expands to enclosing function/class boundaries. Size-bounded so a single call never dumps an entire file.
```json
{ "target": "OrderHandler.handle" }
{ "target": "src/payments/OrderHandler.ts:142-179", "expand": false }
{ "target": "src/config.ts" }
```
| Parameter | Type | Description |
|---|---|---|
| `target` | string | Symbol name or `file:start[-end]` |
| `expand` | boolean | Expand to enclosing block (default: `true`) |
| `max_tokens` | number | Token budget for the response |
### `search` — keyword or regex search
The sanctioned replacement for `grep`. Always ranked, gated, and token-budgeted.
```json
{ "pattern": "PathDeniedError", "scope": "src/" }
{ "pattern": "export.*function", "regex": true, "max_hits": 20 }
```
| Parameter | Type | Description |
|---|---|---|
| `pattern` | string | Keyword or regex pattern |
| `regex` | boolean | Treat pattern as regex (default: `false`) |
| `scope` | string | Subdirectory to restrict the search |
| `exclude` | string[] | Subdirectories to skip |
| `max_hits` | number | Override the default hit cap |
| `max_tokens` | number | Token budget for snippets |
| `force` | boolean | Bypass gating (use sparingly) |
### `explore` — understand structure and relationships
Answers structural questions: callers, callees, dependencies, blast radius, architecture overview. Requires a graph backend for full results; falls back to a file scan summary when no graph is available.
```json
{ "question": "what calls resolveSafe", "anchor": "resolveSafe", "relation": "callers" }
{ "question": "overview of the payments module", "relation": "overview" }
{ "question": "what would break if I change UserSession", "relation": "impact" }
```
| Parameter | Type | Description |
|---|---|---|
| `question` | string | Natural-language structural question |
| `anchor` | string | Symbol to anchor the query |
| `relation` | `callers` \| `callees` \| `dependencies` \| `impact` \| `overview` \| `auto` | Relationship type (default: `auto`) |
---
## Backpressure
When a query is too broad, tools return a `refine` envelope instead of overwhelming the agent:
```json
{
"status": "refine",
"reason": "breadth",
"summary": { "totalFound": 340, "files": 48, "estTokens": 28000, "topScore": 0.21 },
"distribution": [
{ "dir": "src/auth", "hits": 120 },
{ "dir": "src/core", "hits": 80 }
],
"suggestions": [
{ "action": "narrow_scope", "arg": "src/auth", "wouldYield": 120 },
{ "action": "use_force", "hint": "Set force:true to bypass gating" }
],
"refusalCount": 1
}
```
Refusal reasons:
| Reason | Condition |
|---|---|
| `breadth` | Unique files > `maxFiles` (default 25) |
| `volume` | Hit count > `maxHits` (120) or estimated tokens > `absoluteMaxTokens` (24 000) |
| `relevance` | Top BM25 score below `minRelevance` (0.35) |
| `compound` | Two or more of the above |
**Deadlock cap:** after `maxRefusals` (default 3) consecutive refusals, the gate opens automatically.
**Compound margin:** single-axis near-misses within 15% of a threshold pass through without refusal.
---
## Config file
Place a JSON file anywhere and pass it with `--config`:
```json
{
"gates": {
"maxFiles": 25,
"maxHits": 120,
"maxTokens": 6000,
"minRelevance": 0.35
},
"scan": {
"excludeDirs": ["node_modules", "dist", ".git"],
"respectGitignore": true
}
}
```
Full config schema (all fields optional — shown with defaults):
```json
{
"gates": {
"maxFiles": 25,
"maxHits": 120,
"maxTokens": 6000,
"minRelevance": 0.35,
"compoundMargin": 0.15
},
"limits": {
"maxRefusals": 3,
"absoluteMaxTokens": 24000
},
"estimation": {
"charsPerToken": 3.5
},
"scoring": {
"weights": {
"kind": 0.30,
"lexical": 0.25,
"proximity": 0.20,
"shape": 0.15,
"recency": 0.10
}
},
"backend": {
"graph": "codebase-memory-mcp"
},
"scan": {
"excludeDirs": ["node_modules", "dist", "build", "target", ".venv", "vendor", ".git"],
"respectGitignore": true
}
}
```
---
## Logging
All tool activity is written as newline-delimited JSON to `code-context-gate.log` in the project root. Pass `--debug` to also stream logs to stderr.
### Log events
| Event | Source | Key fields | What it tells you |
|---|---|---|---|
| `tool_call` | `server.ts` | `tool`, `args` | Every inbound MCP tool call with its arguments |
| `dispatch` | `dispatcher.ts` | `query`, `shape`, `graphAvailable` | Query shape classification and whether graph is reachable |
| `fileio_scan` | `fileio.ts` | `scope`, `patternType` | When a file walk starts, what directory and pattern type (`string`\|`regex`) |
| `graph_search` | `graph.ts` | `backend`, `count` | Graph search completed; how many results the backend returned |
| `graph_search_error` | `graph.ts` | `backend`, `error` | Graph search failed silently (previously swallowed) |
| `graph_architecture` | `graph.ts` | `backend`, `hasResult` | Architecture call completed and whether a non-empty summary was returned |
| `graph_architecture_error` | `graph.ts` | `backend`, `error` | Architecture call failed |
| `dispatch_result` | `dispatcher.ts` | `strategy`, `count` | Final strategy used (`graph_symbol`, `fileio_only`, `fileio+graph_merged`, etc.) and total hits |
| `dispatch_graph_miss` | `dispatcher.ts` | `query`, `shape` | Graph returned 0 results for a symbol query; falling back to FileIO |
| `gate` | `server.ts` | `tool`, `pass`, `reason` | Gate decision — passed or refused, and why |
| `tool_result` | `server.ts` | `tool`, `returned`, `estTokens` | How many results were returned and their estimated token cost |
| `tool_error` | `server.ts` | `tool`, `error` | Unhandled tool exception |
### Reading the log
A typical symbol lookup with graph hit:
```jsonl
{"ts":"...","event":"tool_call","tool":"locate","args":{"query":"resolveSafe","kind":"definition"}}
{"ts":"...","event":"dispatch","query":"resolveSafe","shape":"symbol","graphAvailable":true}
{"ts":"...","event":"graph_search","backend":"codebase-memory-mcp","count":2}
{"ts":"...","event":"dispatch_result","query":"resolveSafe","strategy":"graph_symbol","count":2}
{"ts":"...","event":"gate","tool":"locate","pass":true,"reason":null}
{"ts":"...","event":"tool_result","tool":"locate","returned":2}
```
A broad search that triggers a refusal:
```jsonl
{"ts":"...","event":"tool_call","tool":"search","args":{"pattern":"export"}}
{"ts":"...","event":"dispatch","query":"export","shape":"symbol","graphAvailable":true}
{"ts":"...","event":"graph_search","backend":"codebase-memory-mcp","count":148}
{"ts":"...","event":"dispatch_result","query":"export","strategy":"graph_symbol","count":148}
{"ts":"...","event":"gate","tool":"search","pass":false,"reason":"breadth"}
{"ts":"...","event":"tool_result","tool":"search","returned":0}
```
---
## Source structure (`src/`)
| File | Role |
|---|---|
| `cli.ts` | Entry point — parses CLI flags, loads config, starts the server |
| `server.ts` | MCP server — registers the four tools, wires `dispatch → shape → gate → respond` |
| `dispatcher.ts` | Routes queries by shape to graph and/or FileIO; merges and deduplicates results |
| `shaper.ts` | BM25 lexical scoring + kind weighting → ranked `ResultItem[]`; `trim()` for token budgeting |
| `gatekeeper.ts` | Enforces backpressure limits; returns `RefineEnvelope` with distribution and suggestions on failure |
| `config.ts` | Zod-validated config loader; resolves project root from flag → env var → CWD |
| `logger.ts` | Append-only JSON line logger to `code-context-gate.log`; never crashes the server on write failure |
| `types.ts` | Shared types and Zod input schemas for all four tools |
| `adapters/fileio.ts` | Filesystem adapter — `scan()`, `readLines()`, `expandBlock()`, `resolveSafe()` |
| `adapters/graph.ts` | Graph adapters — `CodebaseMemoryAdapter`, `CodegraphAdapter`, `NullAdapter`; `createGraphAdapter()` factory |
### Result scoring
Each result gets a composite score before ranking:
```
score = kindScore × 0.30 + BM25lexical × 0.25 + graphScore × 0.45
```
Kind weights:
| Kind | Weight |
|---|---|
| `definition` | 1.0 |
| `call`, `reference` | 0.7 |
| `unknown` | 0.5 |
| `test` | 0.3 |
| `comment`, `string` | 0.2 |
---
## Graph backends
| Backend | Launch command | Adapter |
|---|---|---|
| `codebase-memory-mcp` | `npx -y codebase-memory-mcp --project-root <root>` | `CodebaseMemoryAdapter` — calls `search_code` and `get_architecture` |
| `codegraph` | `npx -y codegraph <root>` | `CodegraphAdapter` — calls `search` and `architecture` |
| `none` | _(not launched)_ | `NullAdapter` — FileIO only, all tools still work |
Both graph backends are optional. Without one, all four tools work using FileIO only.
---
## Agent Instructions (CLAUDE.md)
This repo ships a `CLAUDE.md` that Claude Code and compatible agents load automatically. It routes all code discovery through `code-context-gate` and prohibits direct calls to lower-level tools like `codebase-memory-mcp`.
When an agent session starts it first probes `code-context-gate` with a lightweight `locate` call:
- **Server responds** → the four tools are used for all code access
- **Server unavailable** → the agent stops and prompts the user to enable `code-context-gate` or re-enable the `cbm-session-reminder` fallback hook
### Re-enabling the fallback hook
If you want agents to fall back to raw `codebase-memory-mcp` calls when `code-context-gate` is not running, add to `~/.claude/settings.json`:
```json
{
"hooks": {
"SessionStart": [
{ "matcher": "startup", "hooks": [{ "type": "command", "command": "~/.claude/hooks/cbm-session-reminder" }] },
{ "matcher": "resume", "hooks": [{ "type": "command", "command": "~/.claude/hooks/cbm-session-reminder" }] },
{ "matcher": "clear", "hooks": [{ "type": "command", "command": "~/.claude/hooks/cbm-session-reminder" }] },
{ "matcher": "compact", "hooks": [{ "type": "command", "command": "~/.claude/hooks/cbm-session-reminder" }] }
]
}
}
```
---
## Requirements
- Node.js 20+
- No native build tools required
---
## License
MIT © 2026 wisdom rock — see [LICENSE](LICENSE) for full text.
TDQS
Scored across 4 tools
Each tool has a clearly distinct purpose: locate finds definitions/references without source bodies, read retrieves source content, explore answers structural questions like callers/dependencies, and search performs text pattern matching. Descriptions cleanly separate these concerns.
All tool names are single, lowercase imperative verbs (locate, read, explore, search), following a consistent and predictable convention. No mixing of styles or ambiguous naming.
Four tools is well-scoped for a code-context gate. Each tool serves a distinct and necessary function in code navigation and understanding, with no obvious redundancy or missing essential operations.
The set covers core code context needs: finding symbols (locate), reading code (read), exploring relationships (explore), and searching text (search). A minor gap is the absence of a dedicated tool for listing all symbols in a file or repository overview, though explore may partially address this.