Skip to content

About

Deterministic, local-first context optimization for AI coding agents. SDK + MCP server. No cloud, no LLM, no telemetry.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Repository files navigation

mcp-context-optimizer

Keep critical context within token limits for AI coding agents — without losing errors, decisions, or file references.

mcp-context-optimizer takes the raw exhaust of a coding session (conversation messages, tool outputs, errors, stack traces, file snapshots, git diffs, test results, recorded decisions) and produces a compact, task-specific context that keeps the facts needed to solve the task — with full provenance for every retained fragment and a reproducible report of what was kept, merged, and omitted, and why.

It is not a truncation or summarization utility. The value comes from:

  • deterministic normalization and classification,
  • exact + conservative near-duplicate detection,
  • structured extraction (paths, symbols, stack frames, test failures, requirements),
  • explainable, weighted relevance scoring,
  • budget-aware selection that never silently drops mandatory facts,
  • evidence-preserving rendering with source references for every fragment.

Effectiveness is demonstrated by a reproducible benchmark suite (see Benchmarks) — not by claims.

Install

npm install mcp-context-optimizer                             # core SDK (zero runtime deps)
npm install @modelcontextprotocol/server zod                 # only for the MCP server
  • Node.js >= 20, ESM only.
  • The MCP adapter is optional: it lives behind the mcp-context-optimizer/mcp export and its dependencies are optional peer dependencies. The core SDK never imports the MCP SDK, never opens sockets, and never writes to disk.

Quick start (SDK)

import { ContextOptimizer } from 'mcp-context-optimizer';

const optimizer = new ContextOptimizer({ tokenBudget: 12_000 });

const result = await optimizer.buildContext({
  task: 'Fix authentication after an access policy change',
  messages: [
    { role: 'user', text: 'Login fails with 401 after the policy change in src/auth/policy.ts' },
  ],
  files: [
    { path: 'src/auth/policy.ts', content: 'export function checkPolicy(user) { /* ... */ }' },
  ],
  errors: [
    { message: 'Error: 401 Unauthorized\n    at checkPolicy (src/auth/policy.ts:2:10)' },
  ],
  decisions: [
    { text: 'Policy checks must stay server-side only', status: 'accepted' },
  ],
});

console.log(result.context);   // rendered, compact context
console.log(result.sources);   // selected fragments + provenance + score breakdowns
console.log(result.omitted);   // dropped fragments + reasons
console.log(result.report);    // reproducible machine-readable report
console.log(result.stats);     // estimated input/output tokens + reduction ratio

Identical inputs and configuration always produce byte-identical outputs.

Quick start (CLI, no MCP client needed)

# Build a compact context from a records file
npx mcp-context-optimizer build --task "Fix auth" --records session.json

# Pipe records from stdin
cat session.json | npx mcp-context-optimizer build --task "Fix auth" --stdin

# Write the result to a file
npx mcp-context-optimizer build --task "Fix auth" --records session.json --output context.txt

Before / after

A typical debugging session is full of repeated tool outputs, chit-chat, and stale context. Here is what happens with a realistic session:

Before — raw session dump (4992 tokens, over budget):

# Task: Stop the duplicate-charge bug in src/billing/charge.ts

message #1 (user): Customers get charged twice when retrying; the duplicate
originates in src/billing/charge.ts
message #2 (assistant): Chattering about hypothesis 0: maybe the cache, maybe
the clock, maybe the moon phase. Let us look at more logs.
... (40 lines of chattering)
message #42 (assistant): Chattering about hypothesis 39: maybe the cache...
tool_call #43: npm test -- billing
RUN v2.1.0
 ❯ src/billing/charge.test.ts (1 test)
   × duplicate charge
     → DuplicateKeyError: duplicate key value violates unique constraint
 Test Files  1 failed (1)
... (8 identical copies of the same test output)
error #52: DuplicateKeyError: duplicate key value violates unique constraint
    at chargeOnce (src/billing/charge.ts:88:11)
decision #53: Idempotency keys must be generated per logical order, not per
retry attempt
file #54: src/billing/charge.ts
export async function chargeOnce(order, retry) {
  const key = `ord-${order.id}-retry-${retry.attempt}`;
  return stripe.charges.create({ amount: order.total, idempotencyKey: key });
}

After — optimized context (328 tokens, 93% reduction, all required facts retained):

# Task

Stop the duplicate-charge bug in src/billing/charge.ts

## Errors
- [error] DuplicateKeyError: duplicate key value violates unique constraint
  "charges_idempotency_key_key"
  src/billing/charge.ts:88:11  [source: fr_a1b2c3 | record r52]

## Decisions
- [decision] Idempotency keys must be generated per logical order, not per retry
  attempt  [source: fr_d4e5f6 | record r53]

## Files
- [file] src/billing/charge.ts
  export async function chargeOnce(order, retry) {
    const key = `ord-${order.id}-retry-${retry.attempt}`;
    return stripe.charges.create({ amount: order.total, idempotencyKey: key });
  }  [source: fr_g7h8i9 | record r54]

What was dropped: 40 lines of chattering, 7 duplicate test runs, noise notes. What was kept: the error, the accepted decision, the relevant file — all with source references so you can trace every line back to its origin.

Run the benchmark yourself:

npm run bench

How it works: the pipeline

Each stage is a separate, unit-tested, pure function:

Stage Module What it does
1. Normalize & classify normalize.ts Validates records, assigns deterministic ids, normalizes line endings, collapses pathological repeated log lines (never file contents/diffs), keeps original data untouched.
2. Deduplicate dedupe.ts (a) exact duplicates via sha256; (b) repeated tool/test runs — keeps the latest run in full and preserves prior runs' exit codes/timestamps as a compact history line; (c) conservative near-duplicates via trigram containment (same kind only; files/diffs only same path; never on error-type similarity alone). Every merge is explained in the report.
3. Structured extraction extract.ts File paths (unix + windows), code symbols (regex families per language, conservative), Node/Python stack frames, error signatures, test failures, explicit requirements ("must", "do not", ...), evidence-based outcomes (exit codes, markers). Never infers success from an attempt.
4. Relevance scoring score.ts Weighted, explainable scoring (see below). Recency uses the max timestamp in the data — never Date.now() — and has a floor for constraints/decisions so old but binding decisions stay important.
5. Budget-aware selection select.ts Groups fragments by record (structured records are never split), includes mandatory fragments first, then fills remaining budget greedily by score/token density with deterministic tie-breaking. If mandatory content exceeds the budget, it is still included and the conflict is reported (BUDGET_CONFLICT), never silently discarded.
6. Render & report render.ts Fixed-order sections; every fragment gets a [source: fr_xxx | record rN | path] tag. Machine-readable metadata stays separate from the rendered text.

Scoring weights (defaults, all configurable)

Signal Weight Meaning
lexicalOverlap 3 IDF-weighted overlap between task/query tokens and fragment tokens.
identifierMatch 4 Fragment mentions file paths/symbols that appear in the task (full path 1.0, basename 0.6, symbol 0.8).
errorRelevance 3 Fragment is an error/failing test that matches the task identifiers.
constraint 5 Fragment states a requirement, constraint or accepted decision.
failedAttempt 2 Fragment documents a failed attempt or unresolved problem.
recency 1.5 Exponential decay, half-life 7 days; floored at 0.7 for constraints/decisions.
role 1 user/system 1.0 > assistant 0.6 > tool 0.4.
redundancyPenalty 2 Negative: trigram containment against earlier-ranked fragments.
callerPriority 1 Explicit priority hint on a record (default 0.5).

Every fragment's score.components exposes value, weight and contribution per signal so selection is fully auditable.

MCP server (optional)

The adapter speaks the Model Context Protocol over stdio using the official TypeScript SDK (@modelcontextprotocol/server v2). It exposes:

Tool Purpose
ingest_records Add structured records to the server's in-memory store.
search_context Rank fragments for a query with score breakdowns and provenance.
get_decisions Retrieve recorded technical decisions and constraints.
get_relevant_errors Retrieve errors, failed attempts, failing tests (optionally ranked).
get_file_history Retrieve snapshots/diffs/errors associated with a file path.
build_context Build a compact context under a token budget; returns context + sources + omissions + report.

Client configuration

Claude Code:

claude mcp add mcp-context-optimizer -- node /absolute/path/to/mcp-context-optimizer/dist/mcp/cli.js

Cursor / Claude Desktop / any MCP client (mcp.json):

{
  "mcpServers": {
    "mcp-context-optimizer": {
      "command": "node",
      "args": ["/absolute/path/to/mcp-context-optimizer/dist/mcp/cli.js"]
    }
  }
}

During development, run it directly from source with npx tsx src/mcp/cli.ts, or inspect it locally with npx @modelcontextprotocol/inspector node dist/mcp/cli.js.

Important limitation

MCP tool availability does not automatically intercept or rewrite model requests. A compatible client must explicitly call these tools (e.g. the agent decides to call build_context before answering a task). Automatic context replacement would require a native integration inside the coding agent (hooking its prompt assembly).

Persistence (opt-in)

By default the store is in-memory per process lifetime — restarting the server starts from an empty store. To keep records across restarts, pass --persist <path> to the CLI or set persistPath in McpAdapterOptions:

mcp-context-optimizer --persist ~/.cache/mcp-context-optimizer/context.json
import { createContextOptimizerServer } from 'mcp-context-optimizer/mcp';

const server = createContextOptimizerServer({
  persistPath: './context-store.json',
});

Records are written to a local JSON file; no network access is used. The file stays on the local machine. Implement the ContextStore interface for custom backends (e.g. SQLite).

Privacy & local operation

  • No network requests, ever. (Tested: fetch is stubbed to throw in the test suite.)
  • No API keys, no external AI models, no telemetry or analytics.
  • No source files are read or written; the SDK only sees data you pass in.
  • The default store is in-memory; implementing the ContextStore interface is the explicit opt-in path if you want local persistence (e.g. a JSON file) — the SDK itself never writes to disk.

The default estimator is a character heuristic (ceil(chars / 4)). It is an estimate: real tokenizers vary by a factor of 1.5–3× on code and unicode. The report labels the estimator used (report.tokens.estimator) and emits a TOKEN_ESTIMATE_ONLY warning. For exact counts, plug in your own tokenizer:

import { ContextOptimizer, createTokenEstimator } from 'mcp-context-optimizer';

const optimizer = new ContextOptimizer({
  tokenBudget: 12_000,
  tokenEstimator: createTokenEstimator('my-tok', (text) => myTokenizer.encode(text).length),
});

Built-in tokenizer presets

For convenience, the SDK ships with presets tuned for popular model families:

import { ContextOptimizer, claudeEstimator, gpt4Estimator } from 'mcp-context-optimizer';

const optimizer = new ContextOptimizer({
  tokenBudget: 12_000,
  tokenEstimator: claudeEstimator, // ~3 chars/token, tuned for Claude
});

// Or look up a preset by name:
import { getPreset } from 'mcp-context-optimizer';
const est = getPreset('claude-3');
Preset Heuristic Best for
default ceil(chars / 4) Default; good for prose-heavy
claude-3 ceil(chars / 3) Claude code/context; denser tokens
gpt-4 ceil(chars / 4) GPT-4 family
mistral ceil(chars / 4) Mistral family

Benchmarks

A reproducible benchmark suite compares three strategies across 9 coding-session scenarios:

  • Baseline A — original, unoptimized context (raw).
  • Baseline B — exact deduplication + head truncation to budget.
  • Candidate C — ContextOptimizer (this package).

Metrics: context size reduction, required-fact retention, retrieval precision/recall, budget compliance, and source-attribution integrity. Latency and memory are machine-local measurements.

Running benchmarks

npm run bench            # run all scenarios and print a comparison table
npm run bench -- --write  # also write results to benchmarks/results/latest.json

Latest results

scenario                      strategy               tok-in   tok-out   reduct   facts   prec    rec   budget   attrib        ms
------------------------------------------------------------------------------------
repeated-tool-outputs         A_raw                     829       829       0%    100%    30%   100%       ok        -       0.0
repeated-tool-outputs         B_dedupe_truncate         829       829       0%    100%    30%   100%       ok        -       0.0
repeated-tool-outputs         C_optimizer              829       187      77%    100%   100%   100%       ok     100%       1.3
long-debugging-session        A_raw                    4992      4992       0%    100%     5%   100%   OVER        -       0.0
long-debugging-session        B_dedupe_truncate        4992      2481      50%    67%     5%    50%       ok        -       0.0
long-debugging-session        C_optimizer             4992       328      93%    100%    80%   100%       ok     100%       4.6
errors-and-stack-traces       A_raw                     300       300       0%    100%    60%   100%       ok        -       0.0
errors-and-stack-traces       B_dedupe_truncate        300       300       0%    100%    60%   100%       ok        -       0.0
errors-and-stack-traces       C_optimizer              300       295       2%    100%    60%   100%       ok     100%       0.6
previous-failed-attempts      A_raw                     260       260       0%    100%    80%   100%       ok        -       0.0
previous-failed-attempts      B_dedupe_truncate        260       260       0%    100%    80%   100%       ok        -       0.0
previous-failed-attempts      C_optimizer              260       243       7%    100%    80%   100%       ok     100%       0.7
decisions-and-constraints     A_raw                     306       306       0%    100%    50%   100%       ok        -       0.0
decisions-and-constraints     B_dedupe_truncate        306       306       0%    100%    50%   100%       ok        -       0.0
decisions-and-constraints     C_optimizer             306       250      18%    100%    60%   100%       ok     100%       0.8
multi-file-dependencies       A_raw                     424       424       0%    100%    71%   100%       ok        -       0.0
multi-file-dependencies       B_dedupe_truncate        424       424       0%    100%    71%   100%       ok        -       0.0
multi-file-dependencies       C_optimizer             424       431      -2%    100%    71%   100%       ok     100%       1.1
stale-information             A_raw                     248       248       0%    100%    40%   100%       ok        -       0.0
stale-information             B_dedupe_truncate        248       248       0%    100%    40%   100%       ok        -       0.0
stale-information             C_optimizer             248       197      21%    100%    50%   100%       ok     100%       0.5
tight-budget                  A_raw                    2089      2089       0%    100%    40%   100%   OVER        -       0.0
tight-budget                  B_dedupe_truncate       2089         98      95%    100%   100%   100%       ok        -       0.0
tight-budget                  C_optimizer             2089       159      92%    100%    50%   100%       ok     100%       2.6
impossible-budget             A_raw                      79        79       0%    100%    50%   100%   OVER        -       0.0
impossible-budget             B_dedupe_truncate         79        44      44%    100%   100%   100%       ok        -       0.0
impossible-budget             C_optimizer                79        58      27%    100%    50%   100%       ok     100%       0.1

=== Aggregate (Candidate C) ===
required facts fully retained in every scenario: YES
budget compliance: 9/9 scenarios within budget
attribution integrity (all source tags resolve): YES
avg reduction: C=42%  B=21%
avg retrieval precision(C)=71%  recall(C)=100%

Candidate C retains 100% of required facts in all 9 scenarios, stays within budget, and achieves 42% average reduction (vs 21% for simple dedupe+truncation). On tight budgets it preserves mandatory constraints while Baseline B loses them.

Testing

npm test              # run unit + integration tests (vitest)
npm run test:watch    # watch mode during development
npm run typecheck     # TypeScript type checking
npm run lint          # ESLint + formatting checks
npm run build         # compile to dist/

See test/ for the test suite covering: deterministic output, duplicate handling, similar-but-non-identical errors, required-fact preservation, source-reference integrity, stable ranking, empty/malformed input, impossible budgets, MCP tool validation, and local-only operation (no network).

Custom tokenizers

By default, token counts use the chars/4 heuristic (deterministic, zero dependencies). For production use with a specific model, plug in a real tokenizer:

import { ContextOptimizer, type TokenEstimator } from 'mcp-context-optimizer';
import { encoding_for_model } from 'tiktoken'; // npm install tiktoken (optional dep)

const enc = encoding_for_model('gpt-4');
const tokenizer: TokenEstimator = {
  name: 'tiktoken/gpt-4',
  estimate: (text: string) => enc.encode(text).length,
};

const optimizer = new ContextOptimizer({
  tokenBudget: 12_000,
  tokenEstimator: tokenizer,
});

The estimate function must be deterministic — the same string must always return the same number. This keeps buildContext output byte-identical for identical inputs, which is required for caching and reproducibility.

See src/tokens.ts for the TokenEstimator interface and the built-in defaultTokenEstimator / createTokenEstimator helpers.

Versioning & SemVer

mcp-context-optimizer follows SemVer.

Component Stability Breaking change policy
Public TypeScript API (src/index.ts exports) Stable Minor version bump
Rendered context text format Stable Minor version bump
OptimizationReport JSON shape Stable Minor version bump
PIPELINE_VERSION constant Internal Patch may change if output format changes
MCP tool names / arguments Stable Minor version bump
Scoring weight defaults Internal Patch may change (re-optimize after upgrade)

PIPELINE_VERSION is incremented whenever the pipeline changes in a way that affects output (e.g. new compaction stage, new scoring signal). It is part of the config hash so that cached results are invalidated automatically.

Current version: 0.1.x — pre-1.0. Breaking changes may land in minor releases until 1.0.0. After 1.0.0, breaking changes require a major version bump.

Comparison with alternatives

Approach Deterministic Local-only Zero deps Structured signals Budget control Provenance
mcp-context-optimizer ✅ Yes ✅ Yes ✅ Yes ✅ Paths/symbols/stacks/errors ✅ Hard budget + mandatory ✅ Every fragment tagged
LLM summarization (external API) ❌ No ❌ No ❌ No ⚠️ Partial ⚠️ Soft / best-effort ❌ None
Simple truncation (head/tail) ✅ Yes ✅ Yes ✅ Yes ❌ None ✅ Yes ❌ None
Semantic chunking (embeddings) ⚠️ Model-dependent ❌ Usually remote ❌ Heavy deps ❌ None ⚠️ Soft ❌ None
RAG / vector retrieval ⚠️ Approximate ❌ Usually remote ❌ Heavy deps ❌ None ✅ Yes ⚠️ Source docs only

Key differences:

  • No model calls. Most "context optimizers" are just wrappers around another LLM that summarizes your context — expensive, slow, non-deterministic, and sends your code to a third party. This package does everything locally with pure functions.
  • Structured, not blind. It extracts file paths, symbols, stack frames, test failures, and explicit requirements before scoring, so a 200-line stack trace is reduced to its relevant frames rather than chopped at an arbitrary line.
  • Mandatory facts are never silently dropped. If required content exceeds the budget, the conflict is reported (BUDGET_CONFLICT) instead of silently losing evidence.
  • Full audit trail. Every retained fragment carries a [source: ...] tag, and the machine-readable report lists every merge, omission, and score component.

MCP client integration examples

Claude Code

claude mcp add mcp-context-optimizer -- node /absolute/path/to/dist/mcp/cli.js

Then in Claude Code, the agent can call build_context, search_context, get_decisions, get_relevant_errors, get_file_history, and ingest_records directly.

Cursor / Claude Desktop (mcp.json)

{
  "mcpServers": {
    "mcp-context-optimizer": {
      "command": "node",
      "args": ["/absolute/path/to/mcp-context-optimizer/dist/mcp/cli.js"]
    }
  }
}

Continue.dev

Add to ~/.continue/config.json:

{
  "mcpServers": {
    "mcp-context-optimizer": {
      "command": "node",
      "args": ["/absolute/path/to/mcp-context-optimizer/dist/mcp/cli.js"]
    }
  }
}

MCP registries

The package is listed in MCP registries for one-click installation:

Registry How to add
Smithery See smithery.yaml. The server is configured with --persist so records survive restarts.
Glama Add server via Glama dashboard: command npx mcp-context-optimizer, transport stdio.
MCP.so Add server via MCP.so dashboard: command npx mcp-context-optimizer, transport stdio.

Smithery example — add to your Smithery client config:

{
  "mcpServers": {
    "context-optimizer": {
      "command": "npx",
      "args": [
        "mcp-context-optimizer",
        "--persist",
        "~/.cache/mcp-context-optimizer/context.json"
      ]
    }
  }
}

Claude Code hooks — see claude-code-hooks.json. Register it in Claude Code settings to automatically call build_context on every user prompt (reduces the explicit-invocation friction).

Claude Desktop — see claude-desktop-config.json. Add to your Claude Desktop claude_desktop_config.json.

Important: MCP tool availability does not automatically intercept model requests. The client/agent must explicitly call these tools (e.g. call build_context before answering). Automatic context replacement requires a native integration inside the coding agent.

The MCP server stores data in memory per process lifetime — restarting the server starts from an empty store. Re-ingest records or implement your own ContextStore (see src/store.ts) for persistence. Use --persist for built-in file persistence.

About

Deterministic, local-first context optimization for AI coding agents. SDK + MCP server. No cloud, no LLM, no telemetry.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages