A stateful MCP server that remembers what your AI agent has already seen.
Project Oracle sits between Claude Code and your codebase, caching file reads, command results, and git state across sessions. When the agent re-reads an unchanged file, Oracle returns "No changes since last read" instead of the full content — saving hundreds of tokens per call and cutting repeat-read costs by 95%+.
- The Problem
- How It Works
- Token Savings
- Tools
- Installation
- Configuration
- Architecture
- Chunkhound Integration
- Development
- License
AI coding agents waste tokens on three patterns that compound over long sessions:
- Re-reading files — Agent reads a file, context compacts, agent reads the same unchanged file again. Every re-read costs hundreds to thousands of tokens for content already processed.
- Multi-step tool choreography —
git status→git diff→ read 3 files → grep for something. Five round trips when one structured query would suffice. - Rediscovering project structure — Every new session: glob for files, read configs, figure out the tech stack, find test commands. The agent pays this cost repeatedly for knowledge that rarely changes.
These compound across sessions. Project Oracle eliminates the redundancy.
Oracle is a Model Context Protocol (MCP) server that acts as a smart proxy between the agent and your project:
Agent (Claude Code)
│
│ calls oracle_read("src/auth.py")
│
▼
Project Oracle (MCP server)
│
├─ Cache miss? → Read from disk, compress with zstd, store in SQLite, return full content
├─ Cache hit, unchanged? → Return "No changes since last read (2m ago)" [~3 tokens]
└─ Cache hit, changed? → Compute unified diff, return only the delta
Three layers of agent integration:
| Layer | Mechanism | What it does |
|---|---|---|
| Passive learning | PostToolUse hooks | When the agent uses built-in Read/Grep/Bash, hooks silently feed the results to Oracle's cache |
| Active nudging | PreToolUse AYLO hooks | Before the agent re-reads a file, a question nudges it toward oracle_read instead |
| Direct tools | 7 MCP tools | oracle_status, oracle_ask, oracle_run, oracle_read, oracle_grep, oracle_forget, oracle_stats |
State persists in per-project SQLite databases, so the agent picks up where it left off across sessions.
Oracle tracks which files have been returned with full content during the current session (an in-memory _session_seen set). This is the key to how savings work:
- First read of a file in a session: Always returns full content. If the file is already in the SQLite cache, Oracle validates the stored SHA-256 against the file on disk — a cache hit skips the disk read and decompression, but the agent still gets the complete text it needs to work.
- Second+ reads of the same file in a session: The file is in
_session_seen. If unchanged, Oracle returns"No changes since last read"(~3 tokens). If changed, it returns a compact unified diff. - Mid-session: Most of the working set is in
_session_seen. Every re-read costs 3 tokens instead of 800. Agents re-read files constantly — after context compaction, after editing other files, after switching tasks — so this adds up fast. - Cross-session: The SQLite cache persists between sessions. When a new session starts,
_session_seenis empty, so the first read of each file returns full content (the agent needs it). But the cache validates files via SHA-256 without redundant disk I/O.oracle_statusandoracle_runreturn cached project state and command results instantly — no re-runninggit status, no re-discovering the tech stack.
Not yet benchmarked. Per-operation math is straightforward (3-token cache hit vs. 800-token file re-read). We'll measure real session-level savings from
agent_logdata once the server is deployed.
| Scenario | Without Oracle | With Oracle | Projected Savings |
|---|---|---|---|
| Re-read unchanged 200-line file | ~800 tokens | ~3 tokens | ~99% |
| Re-read file with 5 lines changed | ~800 tokens | ~50 tokens | ~94% |
Repeat git status (no changes) |
~100 tokens | ~2 tokens | ~98% |
| Repeat grep (same results) | ~300 tokens | ~6 tokens | ~98% |
| Project overview (cached) | ~500 tokens | ~80 tokens | ~84% |
Use at session start instead of running git status, git branch, and config globs separately. Returns one cached snapshot: language stack, package manager, git branch, clean/dirty state, and cached file count — built from data Oracle already has, so it costs nothing to refresh.
Replaces: the "what's going on with this project?" multi-tool dance (git log, read configs, grep for entry points, ask the agent to summarize) with one intent-routed call. A keyword classifier maps the question to the cheapest handler that can answer it — no LLM is used for routing.
"what changed?" → git cache (free)
"are we ready to push?" → readiness check (free)
"are tests passing?" → command cache (free)
"what's the tech stack?"→ project overview (free)
"find auth middleware" → chunkhound or grep (free)
"explain this pattern" → Claude Haiku fallback (~$0.001)
This is the intent-grouped front door modeled on the "search/execute" MCP pattern. When you don't know which specific tool you need, ask first.
Use instead of running pytest / ruff / mypy (etc.) directly through Bash when you might be re-running against unchanged code. Oracle keys results by source-file SHA-256, so when nothing relevant has changed since the last invocation, you get the cached output back in milliseconds rather than waiting for the command to actually execute. Arbitrary shell commands are rejected.
Default allowlist: pytest, ruff, mypy, go test, go build, npm test, pnpm test, eslint, tsc, cargo test, cargo build
Tells you what changed since last read. On the first read of a file in a session, this tool returns the full content with no token savings vs. the built-in Read — you pay one MCP round trip in exchange for caching the file for next time. The savings show up on the second and subsequent reads in the same session:
- Repeat, unchanged:
"No changes since last read (2m ago)"— about 3 tokens - Repeat, changed: Returns only the unified diff of what changed
Reach for this when you suspect a file may have changed and want to confirm cheaply, not as a blanket replacement for Read on first contact.
Tells you whether a previously run grep would now return different matches. Use this to re-check a search you already ran earlier in the session — Oracle compares the current matches against the cached result and surfaces the delta. This is cache introspection, not a Grep replacement: for a brand-new pattern with no prior cache entry, the built-in Grep is just as good and avoids the MCP round trip.
Clear the cache for a specific file. The next oracle_read returns full content. Use when you need a guaranteed fresh read.
Returns an adoption and savings scorecard for the current session and cumulatively. You get the cache hit rate with the underlying counts (e.g., 25% (5/20 oracle calls)), tokens saved this session, and an oracle-vs-built-in adoption breakdown for read / grep / run with per-category call counts. When prior sessions exist, it also reports how this session's hit rate and adoption rate compare to the recent-session average. Call it mid-session to check whether your tool choices are paying off, or at session end to capture cumulative savings before the context clears.
# Clone the repository
git clone https://github.com/mikelane/project-oracle.git
cd project-oracle
# Install with uv (recommended)
uv sync
# Or with pip
pip install -e .# Should print server info and exit
uv run project-oracle --helpAdd to your Claude Code settings (~/.claude/settings.json):
{
"mcpServers": {
"project-oracle": {
"type": "stdio",
"command": "uv",
"args": ["run", "--directory", "/path/to/project-oracle", "project-oracle"]
}
}
}
⚠️ Warning: register Oracle at user scope onlyRegister Oracle in
~/.claude/settings.json(user scope) as shown above. Do not register Oracle in a project-level.mcp.jsonfile.Due to upstream bug anthropics/claude-code#13898, custom subagents cannot reach MCP servers configured at project scope. Instead of erroring, they silently hallucinate plausible-looking results — meaning custom subagents will return fabricated Oracle results with no error indicator. Made-up
oracle_readdeltas, made-uporacle_statussnapshots, made-up cache hits. The subagent reports success and the agent acts on the fabricated data.What works correctly:
- User-scope registration in
~/.claude/settings.json(the example above).- The built-in
general-purposesubagent — use it when subagent invocation is required. It is unaffected by this bug.There is no Oracle-side workaround; the bug is in Claude Code's subagent MCP plumbing.
Last verified: 2026-04-26. Remove this warning when anthropics/claude-code#13898 is closed AND a Claude Code release notes entry confirms the fix.
Copy the hook scripts and register them in your Claude Code settings:
# Copy hooks to your Claude config
cp hooks/lights-on-oracle-pre.sh ~/.claude/hooks/
cp hooks/lights-on-oracle-post.sh ~/.claude/hooks/
chmod +x ~/.claude/hooks/lights-on-oracle-pre.sh
chmod +x ~/.claude/hooks/lights-on-oracle-post.shAdd to ~/.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read",
"hooks": [{ "type": "command", "command": "bash ~/.claude/hooks/lights-on-oracle-pre.sh" }]
},
{
"matcher": "Grep",
"hooks": [{ "type": "command", "command": "bash ~/.claude/hooks/lights-on-oracle-pre.sh" }]
},
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "bash ~/.claude/hooks/lights-on-oracle-pre.sh" }]
}
],
"PostToolUse": [
{
"matcher": "Read",
"hooks": [{ "type": "command", "command": "bash ~/.claude/hooks/lights-on-oracle-post.sh" }]
},
{
"matcher": "Grep",
"hooks": [{ "type": "command", "command": "bash ~/.claude/hooks/lights-on-oracle-post.sh" }]
},
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "bash ~/.claude/hooks/lights-on-oracle-post.sh" }]
}
]
}
}Only needed if you want the oracle_ask Haiku fallback for unroutable questions:
export ANTHROPIC_API_KEY="sk-ant-..."| Variable | Default | Description |
|---|---|---|
ORACLE_DIR |
~/.project-oracle |
Root directory for all Oracle state |
ANTHROPIC_API_KEY |
— | Required only for oracle_ask Haiku fallback |
┌─────────────────────────────────────────────────┐
│ Agent (Claude Code) │
│ Calls oracle_* tools or built-in tools │
└──────────┬──────────────────────────────────────┘
│ MCP stdio
┌──────────▼──────────────────────────────────────┐
│ Project Oracle Server (FastMCP) │
│ │
│ ┌────────────────────────────────────────────┐ │
│ │ Tools │ │
│ │ oracle_read → FileCache → full or delta │ │
│ │ oracle_grep → ripgrep wrapper │ │
│ │ oracle_status → GitCache + StackInfo │ │
│ │ oracle_run → CommandCache (allowlisted) │ │
│ │ oracle_ask → intent router │ │
│ │ oracle_forget → cache invalidation │ │
│ └───────────────┬────────────────────────────┘ │
│ │ │
│ ┌───────────────▼────────────────────────────┐ │
│ │ Caches │ │
│ │ FileCache — zstd compression + SHA-256 │ │
│ │ GitCache — branch, status, log deltas │ │
│ │ CommandCache — allowlisted cmd results │ │
│ └───────────────┬────────────────────────────┘ │
│ │ │
│ Storage: SQLite (WAL mode) per project │
│ ~/.project-oracle/projects/<hash>/state.db │
└──────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────┐
│ Claude Code Hooks (run in parallel) │
│ │
│ PreToolUse: AYLO nudges → "use oracle instead" │
│ PostToolUse: passive ingest → feed to cache │
└──────────────────────────────────────────────────┘
Oracle auto-detects project roots by walking up from file paths, looking for .git, package.json, pyproject.toml, go.mod, or Cargo.toml. Each detected project gets its own SQLite database. No configuration needed.
Once a project root is found, Oracle identifies the language and package manager:
| Marker | Language | Package Manager Detection |
|---|---|---|
pyproject.toml / setup.py |
Python | uv.lock → uv, poetry.lock → poetry, else pip |
package.json |
Node.js | pnpm-lock.yaml → pnpm, yarn.lock → yarn, else npm |
go.mod |
Go | go |
Cargo.toml |
Rust | cargo |
A filesystem watcher (watchfiles, Rust-backed) monitors each project root:
- File modified → cached entry marked stale via
disk_sha256update .git/HEADchanged → git state refreshed- File deleted → removed from cache
The watcher filters out .git, .venv, node_modules, __pycache__, and .mypy_cache.
- Files not read in 30 days → evicted
- Command results older than 24 hours → evicted
- Per-project cache exceeds 50 MB → LRU eviction by
last_read
~/.project-oracle/
├── registry.json # project root → ID mapping
├── ingest/ # file queue from PostToolUse hooks
│ └── *.json
└── projects/
└── a1b2c3d4/ # SHA-256(project_root)[:8]
├── state.db # SQLite — all cached state
└── meta.json # stack info, last session timestamp
Oracle optionally delegates code understanding queries to chunkhound's AST-based semantic indexing:
Claude Code → (stdio) → Project Oracle → (stdio) → Chunkhound MCP
| Concern | Owner |
|---|---|
| AST parsing, semantic chunking, vector search | Chunkhound |
| File caching, delta diffing, agent interaction history | Oracle |
| "What imports X?" / "Find auth code" | Chunkhound |
| "Has this changed since I last looked?" | Oracle |
Chunkhound understands code. Oracle understands the agent's relationship to the code.
If chunkhound is not installed or fails to start, Oracle degrades gracefully — code understanding queries fall back to keyword-based grep, and unroutable questions fall back to Claude Haiku. The agent never sees an error.
git clone https://github.com/mikelane/project-oracle.git
cd project-oracle
uv sync --all-groupsThe project uses strict TDD with multiple testing layers:
# Run all tests
uv run pytest
# Run with coverage (95% minimum enforced)
uv run coverage run --branch -m pytest
uv run coverage report --fail-under=95
# Run mutation testing
uv run pytest --gremlins src/oracle/cache/file_cache.py
# Run BDD scenarios
uv run behave
# Type checking
uv run mypy src/
# Linting
uv run ruff check src/ tests/Tests follow Google test size classification:
| Size | Constraints | Marker |
|---|---|---|
| Small (default) | No I/O, no network, no sleep, single thread | None |
| Medium | Localhost only, threads OK | @pytest.mark.medium |
| Large | No constraints | @pytest.mark.large |
src/oracle/
├── server.py # FastMCP entry point, tool definitions
├── project.py # Project root + stack detection
├── registry.py # Path → ProjectState mapping
├── intent.py # Keyword-based intent classifier
├── ingest.py # File queue processing from hooks
├── watcher.py # FS watcher for cache invalidation
├── cache/
│ ├── file_cache.py # zstd compression + delta diffing
│ ├── git_cache.py # Git state snapshots + deltas
│ └── command_cache.py# Allowlisted command result caching
├── tools/
│ ├── read.py # oracle_read handler
│ ├── grep.py # oracle_grep handler
│ ├── status.py # oracle_status handler
│ ├── run.py # oracle_run handler
│ ├── ask.py # oracle_ask intent router
│ └── forget.py # oracle_forget handler
├── integrations/
│ └── chunkhound.py # MCP client to chunkhound subprocess
└── storage/
└── store.py # SQLite persistence layer (WAL mode)
hooks/
├── lights-on-oracle-pre.sh # PreToolUse AYLO nudges
└── lights-on-oracle-post.sh # PostToolUse passive ingest
features/
├── file_caching.feature # BDD: file read/delta behavior
├── git_state.feature # BDD: git status caching
├── command_caching.feature # BDD: command result caching
└── natural_language.feature # BDD: oracle_ask routing
MIT