CCE MCP turns the index into something an agent invokes directly. cce mcp is a
Model Context Protocol server over stdio; cce init wires an editor (Claude Code) up so it is plug-and-play. This closes the last
gap between the clean-room CCE and the original Python implementation: the agent
integration.
It answers two questions directly:
- "How do I ensure my agent uses CCE?" → real MCP tools (headlined by
context_search) plus aCLAUDE.mdblock that steers the model to prefer them over Read/Grep. - "How do I know it used it?" → every search is a visible tool call and is
logged to
.cce/metrics.jsonl, socce dashboardshows the agent's queries and token savings.
Since v2.5 the server exposes nine tools (in a fixed order): the three v2.4
tools plus the Savings Layers tools — progressive disclosure
(expand_chunk, related_context), output compression (set_output_compression),
memory (record_decision, session_recall), and turn summarization
(summarize_context). context_search now serves compact chunks by default and
each result carries a chunk_id you expand on demand.
CCE MCP is additive: the CLI and the single-repo conformance.json are
untouched, and it is read-only and offline (no network unless the index was
built with the optional Ollama embedder, exactly as the CLI). Everything persisted
(memory) passes through the v2.1 redactor first, so it stays secret-safe.
cce init . # ensure an index, write .mcp.json + a CLAUDE.md block
# restart Claude Code so it loads .mcp.json
# ask: "where is the password hashed?" → the agent calls context_search
cce dashboard # confirm the agent used it (the search is on the dashboard)cce init writes a minimal, idempotent .mcp.json:
{ "mcpServers": { "cce": { "command": "cce", "args": ["mcp", "--dir", "."] } } }(a workspace gets "args": ["mcp", "--workspace"] instead) and merges a
marker-bounded block into CLAUDE.md. Since v2.5 the block also carries the
leveled Output compression rules (Savings Layer 4, default standard):
<!-- BEGIN CCE MCP -->
## Code Context Engine (CCE)
This project is indexed by CCE, exposed as MCP tools. Prefer them over reading or grepping files.
- **PREFER `context_search`** to locate code, understand behaviour, or answer "where is X / how does Y work". …
- Reserve file reads for opening a specific path `context_search` points you to.
- Use `index_status` to check how fresh the index is, and `record_feedback` to rate a result.
### Output compression
Answer in the fewest words that are correct; when editing code show ONLY the changed lines (a minimal diff), never reprint whole files; no preamble or postamble.
<!-- END CCE MCP -->Re-running cce init is safe: the cce server entry and the block are merged,
never duplicated. Other MCP servers already in .mcp.json and other content in
CLAUDE.md are preserved.
In a git repo, cce init also gitignores cce's own cache (since v2.6.3): it appends
.cce/* + !.cce/workspace.yml to the repo .gitignore (idempotent), so the local
index and metrics log are never committed while a shared .cce/workspace.yml stays
committable.
cce init flags:
| Flag | Meaning |
|---|---|
<dir> |
Project directory to initialise (default: current directory). |
--agent claude |
Target agent. v1 targets Claude Code; Cursor / VS Code / Codex are a documented fast-follow. |
--remote <sync-url> |
Pull the CI-built index from a CCE Sync remote instead of indexing locally (see Freshness). |
--force |
Force the index refresh (a --force sync pull past a sha mismatch). |
cce mcp speaks MCP over stdio, JSON-RPC 2.0 on stdin/stdout (newline-delimited
messages). It pins protocol version 2025-06-18.
- Handshake:
initialize→{ protocolVersion, capabilities: { tools: {} }, serverInfo: { name: "cce", version } }; acceptsnotifications/initialized. - Methods:
tools/list,tools/call,ping. - Store resolution is exactly the CLI's:
--dir/--store/ cwd, and--workspacefor ecosystems (SPEC-V2.2). - Missing/empty index: tools still respond —
context_searchreturns a clear "not indexed — runcce index" message rather than erroring;index_statusreports "not indexed." - Cached across calls (issues #26/#31): the long-lived server loads the
single-repo index, the knowledge store, and each workspace union once and
reuses them across tool calls, instead of re-parsing the store and rebuilding
BM25 per request. Freshness is a cheap fingerprint (
mtime+length, onefs::metadataper call) of the store file(s): a re-index, a re-ingest, or acce sync pull(startup auto-pull or mid-session) is picked up on the very next call, and a deleted store returns the friendly missing-index message — never a stale cached answer. Results are byte-identical warm vs cold. - Secret-safe by construction: it only returns what is already in the store, which was redacted at index time (v2.1). Nothing new to scrub.
You rarely run cce mcp by hand — the editor spawns it from .mcp.json. To drive
it manually (the shape the tests use):
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
'{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"context_search","arguments":{"query":"where is the password hashed","top_k":3}}}' \
| cce mcp --dir .The tool names, input schemas, and output structure are identical in the Ruby
and Rust engines — that cross-language parity is the contract, so an agent gets the
same tools whichever engine serves. tools/list returns them in this fixed
order:
| # | Tool | Layer | What it does |
|---|---|---|---|
| 1 | context_search |
L1/L2 | Ranked, compact code chunks for a query; each carries a chunk_id. |
| 2 | index_status |
— | Is the project indexed, how fresh, and the sync source/sha. |
| 3 | record_feedback |
— | Rate a prior result to feed the dashboard's quality signal. |
| 4 | expand_chunk |
L7 | Read the full body / file / neighbours of a returned chunk. |
| 5 | related_context |
L7 | Import-graph neighbours (imports and consumers) of a chunk. |
| 6 | set_output_compression |
L4 | Dial THIS session's own answer terseness. |
| 7 | record_decision |
L5 | Remember a validated decision (secret-scrubbed, local). |
| 8 | session_recall |
L5 | Precision-filtered search over remembered decisions. |
| 9 | summarize_context |
L6 | Deterministic structured digest of the session so far. |
The core workflow is find → expand → widen: context_search finds compact
chunks; expand_chunk reads a full body when you actually need it;
related_context widens across the import graph. The tool descriptions carry an
expand-first rule — once you have a chunk_id, expand it; do not re-issue
context_search for a target you already found.
Search THIS project's code by meaning, across files. Use it FIRST for any cross-file question — "where is X", "how does Y work", "what calls Z" … Results are COMPACT and each carries a
chunk_id; to read a full body callexpand_chunk(chunk_id)— do NOT re-issuecontext_searchfor a target you already found. Widen to import-graph neighbours withrelated_context(chunk_id).
Input schema:
{
"type": "object",
"properties": {
"query": { "type": "string" },
"top_k": { "type": "integer", "default": 8 },
"package": { "type": "string", "description": "scope to one workspace member (optional)" },
"no_graph": { "type": "boolean", "default": false },
"max_tokens": { "type": "integer", "description": "cap the returned context (optional)" },
"detail": { "type": "string", "enum": ["signature", "compact", "full"], "description": "chunk compression level (optional; default from config, usually compact)" },
"source": { "type": "string", "enum": ["code", "knowledge", "both"], "description": "which pools to search (optional; default `both` when a knowledge store exists, else `code`)" }
},
"required": ["query"]
}detail (Savings Layer 2) picks the compression level — signature, compact
(default), or full; absent ⇒ the project's retrieval.detail config, which
defaults to compact (see savings.md).
source (v2.6.1, Knowledge Sources) picks the pool: code (the
unchanged code path), knowledge (the cce knowledge index store), or both —
code + knowledge candidates merged through the one shared ranking. Absent ⇒ the
knowledge.default_source config (default both) when a knowledge store
exists, else always code — so with no knowledge store the tool is byte-identical
to pre-v2.6. A knowledge hit's header carries its provenance in place of
file:line (type/kind) — [knowledge] <title> — <state> · <updated_at> · <url> —
and staleness weighting (recency, wontfix-drop, merged-PR boost, the 0.30 precision
floor) is applied before the blend; see knowledge.md.
expand_chunk/related_context accept knowledge chunk_ids too.
Output is a text block:
one header line per result — #. [score] file:start-end (chunk_type/kind) #chunk_id — followed by the chunk body served at that detail, then the
expand-on-demand hint and the query_id:
1. [0.864806] auth.py:6-13 (function/function_definition) #04578fe98cec59bd
def hash_password(password: str, salt: str) -> str:
"""Hash a password with a salt using SHA-256.
digest = hashlib.sha256((salt + password).encode()).hexdigest()
… (+5 lines)
2. [0.863693] auth.py:16-18 (function/function_definition) #ae61011b82d7a777
def verify_password(password: str, salt: str, expected: str) -> bool:
"""Return True when the password hashes to the expected digest."""
return hash_password(password, salt) == expected
Bodies shown compact. expand_chunk(chunk_id, scope=body|file|neighbors) for more; related_context(chunk_id) for import-graph neighbours.
query_id: 8c74824599a7
Rate this with record_feedback (query_id="8c74824599a7", helpful=true|false).
In a workspace the header carries the member package: 1. [score] billing · lib/billing.rb:2-4 (method/method) #…. max_tokens trims the returned bodies to a
budget. Each call records a search event to .cce/metrics.jsonl (identical to
the CLI path, carrying the retrieval + chunk_compression savings buckets), and
the printed query_id is what record_feedback targets.
Check whether this project is indexed and how fresh it is.
Input {}. Reports chunk/file counts, per-language and per-kind breakdowns, the
store path, and the sync freshness: the index source (local vs pulled), its sha,
and whether it is behind the remote. In a workspace it reports per-member counts and
the cross-member dependency edges.
When a knowledge store exists at the served root (knowledge.md), the report additionally gains a knowledge block (SPEC-SYNC-KNOWLEDGE §4.4):
knowledge :
corpus : internal-tickets
snapshot : 9f1c2a3b4c5d6e7f
records/chunks : 412 / 1873
data as-of : 2026-07-01T09:00:00Z
remote current : 9f1c2a3b4c5d6e7f
behind remote : no
corpus is the pulled corpus id, or (local ingest) when the current snapshot
did not come from a pull; data as-of is the corpus's max updated_at (- when
no record carries one). remote current / behind remote follow the same
offline-safe rules as the code freshness lines: consulted only when a sync remote
is configured, best-effort, any failure degrades to - / no (behind remote
becomes the actionable yes — run `cce knowledge pull` only when both
snapshots are known and differ) — index_status always answers. With no
knowledge store the report is byte-identical to before.
Record whether a prior
context_searchresult was helpful, to improve the quality signal on the dashboard.
Input: { "query_id": string (required), "helpful": boolean (required), "note": string (optional) }. Appends a feedback event to .cce/metrics.jsonl, closing the
quality loop into the dashboard's retrieval-quality north-star.
Read the FULL detail of a chunk
context_searchalready returned, by itschunk_id. … do NOT re-runcontext_searchfor a chunk you already have.
Input: { "chunk_id": string (required), "scope": "body" | "file" | "neighbors" (default "body") }.
scope=bodyrecovers the exact full body — it round-tripsdetail:full, so compact-by-default never loses information.scope=filereturns every chunk in the same file.scope=neighborsreturns chunks from import-graph-related files.
A stale or unknown chunk_id (e.g. after a re-index) returns a short, actionable
message telling you to re-run context_search — never a crash.
$ expand_chunk("04578fe98cec59bd", scope=body)
def hash_password(password: str, salt: str) -> str:
"""Hash a password with a salt using SHA-256.
This is the single place passwords are hashed; callers never
hash inline. Returns the hex digest.
"""
digest = hashlib.sha256((salt + password).encode()).hexdigest()
return digest
Given a
chunk_idfromcontext_search, return the chunks connected to it through the import graph — both what it imports AND its consumers (reverse edges) — as compact entries.
Input: { "chunk_id": string (required), "top_k": integer (default 8) }. Use it
to trace how a symbol is used or what it depends on across files, instead of
pre-loading whole neighbourhoods; expand any result with expand_chunk. Compact
entries carry chunk_ids of their own.
Set how terse THIS session's answers should be — the output-compression level the agent applies to its OWN replies.
Input: { "level": "off" | "lite" | "standard" | "max" (required) }. It sets an
in-memory session preference only — it does not rewrite CLAUDE.md and
resets when the server restarts. Dial down (max) for terse diffs, or up (off)
for full explanations, mid-session.
$ set_output_compression("max")
Output compression is now `max` for this session (in-memory; CLAUDE.md unchanged).
Remember a VALIDATED decision for future sessions … Do NOT record raw model output, guesses, or unverified answers — memory that replays a bad answer POLLUTES future context.
Input: { "text": string (required), "tags": string[] (optional), "area": string (optional) }. The text is secret-redacted before storage, content-addressed,
and de-duplicated (recording the same decision twice is a no-op returning the same
id). The store is the local .cce/memory.jsonl — never pushed by Sync. Set
memory.enabled=false in .cce/config to make the memory tools a no-op.
$ record_decision("Passwords are hashed only in auth.hash_password …", tags=["security","auth"], area="auth")
Recorded decision #46f3ebd005279048. Retrieve it later with session_recall.
Search THIS project's remembered decisions … Hybrid vector + BM25 search, PRECISION-FILTERED: it returns only high-confidence matches (a small top_k) … which you CHOOSE to use — it is never an auto-injected blob.
Input: { "query": string (required), "top_k": integer (default 5) }. Returns
nothing when there is no confident match (score ≥ 0.30 and a shared query token) —
that is normal and correct; proceed without it rather than forcing a weak memory
into context.
$ session_recall("how are passwords hashed")
Recalled 1 of 1 remembered decision(s):
1. [0.851650] #46f3ebd005279048 area=auth tags=security,auth
Passwords are hashed only in auth.hash_password (SHA-256 + salt); never hash inline.
These are validated decisions you MAY reuse — apply only what fits; they are not auto-injected.
Get a compact, deterministic digest of what THIS session has done so far … It is a STRUCTURED digest built from the server's per-session ledger, NOT an LLM-written summary: the same sequence of tool calls always yields the same bytes.
Input: { "scope": "all" | "files" | "queries" | "decisions" (default "all") }.
The server keeps a wall-clock-free, order-preserving ledger of the session's tool
calls; the digest is a pure function of it — files and chunks touched, queries run,
and decisions recorded, deduped, sorted, and bounded with a … (+N more) marker.
$ summarize_context()
CCE session digest
files (2):
- auth.py
- payments.py
chunks (3):
- 04578fe98cec59bd
- 873decd4dc46ba36
- ae61011b82d7a777
queries (1):
- where is the password hashed
decisions (1):
- #46f3ebd005279048 Passwords are hashed only in auth.hash_password (SHA-256 + s…
Run cce mcp --workspace --dir <root> (this is what cce init writes when a
.cce/workspace.yml is present). context_search then federates over the workspace
members exactly as cce search --workspace does: results are tagged by package, the
package argument scopes to one member, and cross-member dependency edges expand the
search. index_status reports per-member counts and the dependency graph.
Metrics for a workspace session land in the workspace-root .cce/metrics.jsonl, and
cce dashboard --workspace folds that root log into its roll-up (alongside every
member's log), so agent usage across the ecosystem shows up in totals,
recent_searches, and by_source. These federated searches span members, so they
are intentionally not attributed to by_package — that panel stays per-member,
reflecting direct usage of each repo. (Per-package attribution of agent searches is
tracked in issue #28's follow-up options.)
For a large multi-repo workspace, pass package to context_search (and --package
to cce search --workspace) to scope the search to one or more members instead of the
whole union:
{ "name": "context_search",
"arguments": { "query": "how are invoices charged", "package": "billing" } }Why it matters (issue #26). A federated search is the standard retrieval run over
the union of the members' chunks, so its cost scales with the total corpus. Scoping
to package loads and searches only the named members, so latency tracks that
member's size rather than the whole ecosystem's — the single most effective lever on a
large workspace. package accepts a member name or a member's package: field
from workspace.yml; an unknown value returns an actionable error listing the
available members (never a silent empty result). Comma-separate to scope to several
members ("package": "billing,payments").
The long-lived MCP server caches the assembled union per scope, so the first
context_search in a scope pays the federation cost and subsequent calls reuse it — a
warm workspace search is as fast as a single-repo one. Each cached union carries the
combined fingerprint (mtime+length) of its member store files (issue #31): a member
re-index or a mid-session cce sync pull is picked up on the very next call, with no
restart needed.
CCE MCP is the biggest beneficiary of CCE Sync: Sync keeps the agent's context fresh without the agent — or the developer — paying local indexing cost. The two compose, as a soft dependency:
- Plug-and-play team context:
cce init --remote <cache-repo>pulls the CI-built index (seconds, not a full re-index), writes.mcp.json+CLAUDE.md, and the agent immediately searches fresh, team-shared context. - Warm on startup: if a sync remote is configured and
sync.auto_pullis on,cce mcpdoes a best-effortsync pull --latestbefore serving, bringing the local index up to the canonicalmain@sha. This never blocks or errors — offline or no-remote just serves the local index. - Observable:
index_statusreports the source (local vs pulled), the sha, and whether the local index is behind the remote's latest.
MCP does not hard-require Sync. With no remote configured, every tool works fully on the local index, offline. A failed or absent Sync never degrades MCP below "use the local index."
Repo-less agent context (consumer mode). The agent's corpus does not have to be
a checkout at all: cce sync pull --all --into ctx --remote <cache-url> turns a
bare directory into a synthesized, federated workspace of every repo in the cache,
and cce mcp --workspace --dir ctx serves it — full context_search,
expand_chunk (whole-file reconstruction from the pulled index), and cross-member
graph expansion, with zero source checkouts. Re-run the pull to refresh;
cce sync verify --checksum-only --dir ctx integrity-checks the stores offline.
See sync.md §7.
Config lives in .cce/config (see docs/sync.md); the relevant key is
sync.auto_pull (bool, default off).
Want the savings visible in the conversation? Set the per-project
.cce/config key (SPEC-USAGE-VISIBILITY.md §3):
mcp:
result_footer: "off" # off (default) | on | sessionWith on, every context_search result ends with one byte-pinned line:
cce: 5 results from 38,628 chunks · served ~1,204 tok vs ~9,880 baseline · saved ~8,676 (88%)
session appends a running total for THIS server session:
· session: 42 searches, ~310k saved.
- Off by default — context hygiene. Printing savings into every tool result
costs the agent's own context window, so the default keeps the result lean;
the aggregate surfaces (
cce usage, the dashboard) carry the numbers. - Pure projection. The footer renders values already on the recorded
searchevent, after all measurement — toggling it never changes a recorded metric, so the dashboard andcce usagestay honest whatever the setting. - Config-only, deliberately. There is no per-call argument and no runtime
tool to flip it: the agent must not toggle its own observability. Read at
server startup; restart
cce mcpafter changing it.
Three independent signals:
- Tool-call log — Claude Code shows each
context_searchcall in its tool-call log, with the arguments and the returned chunks. cce usage— the one-shot terminal summary of the same log: the agent (mcp) vs human (cli) split, tokens saved, quality, latency, and the recent queries. CI-friendly (--jsonemits the versionedcce.usage/v1projection).- Dashboard — every
context_searchis asearchevent oncce dashboard(queries, counts, tokens saved, latency). This is proof of use and of value;record_feedbackadds the quality signal.
cce usage --since 24h # "how much did the agent lean on CCE since yesterday?"
cce dashboard # open the loopback, read-only dashboard
# or, non-interactively:
cat .cce/metrics.jsonl # one JSON line per search / feedback eventcce usage and the dashboard are projections of the SAME pure aggregate, so
their numbers are always identical for the same log and window.
- Read-only and offline-first:
cce mcpnever mutates source or the store, and makes no network calls unless the index used the optional Ollama embedder (localhost). The one thing it writes is memory (record_decision) — local-only, secret-scrubbed, and never pushed by Sync. - The nine tools are the full Savings Layers surface; the token deltas
they produce roll up into the seven-bucket ledger you read with
cce savings. - The design specs are
SPEC-MCP.md(the server + first three tools) andSPEC-V2.5-SAVINGS.md(the six v2.5 tools); the cold-start verification transcript is indocs/VERIFIED.md.