Skip to content

Latest commit

 

History

History
525 lines (413 loc) · 24.7 KB

File metadata and controls

525 lines (413 loc) · 24.7 KB

CCE MCP — use CCE as a native agent tool (v2.4 · nine tools since v2.5)

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:

  1. "How do I ensure my agent uses CCE?" → real MCP tools (headlined by context_search) plus a CLAUDE.md block that steers the model to prefer them over Read/Grep.
  2. "How do I know it used it?" → every search is a visible tool call and is logged to .cce/metrics.jsonl, so cce dashboard shows 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.


Quick start

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).

The server: cce mcp

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 } }; accepts notifications/initialized.
  • Methods: tools/list, tools/call, ping.
  • Store resolution is exactly the CLI's: --dir / --store / cwd, and --workspace for ecosystems (SPEC-V2.2).
  • Missing/empty index: tools still respond — context_search returns a clear "not indexed — run cce index" message rather than erroring; index_status reports "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, one fs::metadata per call) of the store file(s): a re-index, a re-ingest, or a cce 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 nine tools

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.

1. context_search — the headline

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 call expand_chunk(chunk_id) — do NOT re-issue context_search for a target you already found. Widen to import-graph neighbours with related_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.

2. index_status

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.

3. record_feedback

Record whether a prior context_search result 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.

4. expand_chunk — L7, read the full detail

Read the FULL detail of a chunk context_search already returned, by its chunk_id. … do NOT re-run context_search for a chunk you already have.

Input: { "chunk_id": string (required), "scope": "body" | "file" | "neighbors" (default "body") }.

  • scope=body recovers the exact full body — it round-trips detail:full, so compact-by-default never loses information.
  • scope=file returns every chunk in the same file.
  • scope=neighbors returns 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

5. related_context — L7, widen via the import graph

Given a chunk_id from context_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.

6. set_output_compression — L4, dial answer terseness

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).

7. record_decision — L5, remember a validated decision

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.jsonlnever 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.

8. session_recall — L5, recall remembered decisions

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.

9. summarize_context — L6, compress the session

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…

Workspaces

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.)

Scope large workspaces with package (recommended)

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.


Freshness via CCE Sync

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_pull is on, cce mcp does a best-effort sync pull --latest before serving, bringing the local index up to the canonical main@sha. This never blocks or errors — offline or no-remote just serves the local index.
  • Observable: index_status reports 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).


The opt-in result footer (v2.8)

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 | session

With 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 search event, after all measurement — toggling it never changes a recorded metric, so the dashboard and cce usage stay 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 mcp after changing it.

How to confirm the agent used it

Three independent signals:

  1. Tool-call log — Claude Code shows each context_search call in its tool-call log, with the arguments and the returned chunks.
  2. 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 (--json emits the versioned cce.usage/v1 projection).
  3. Dashboard — every context_search is a search event on cce dashboard (queries, counts, tokens saved, latency). This is proof of use and of value; record_feedback adds 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 event

cce usage and the dashboard are projections of the SAME pure aggregate, so their numbers are always identical for the same log and window.


Notes

  • Read-only and offline-first: cce mcp never 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) and SPEC-V2.5-SAVINGS.md (the six v2.5 tools); the cold-start verification transcript is in docs/VERIFIED.md.