trace-mcp Configuration

Configuration is optional — trace-mcp works out of the box for standard projects.

This page is the reference for the file itself: where it lives, how the layers merge, and what the keys you reach for actually do. Several sections of it are big enough to have their own page — the quality gates thresholds, the MCP tracing span exporter, the memory knobs that bound the daemon, and the tweakcc enforcement tier that trace-mcp init writes here on your behalf.

For the exhaustive list — every key the schema accepts, with its type, allowed values and real default, generated from the schema itself — see the config index.


How config works

All trace state lives in ~/.trace/ (with automatic backwards compatibility and fallback from legacy ~/.trace-mcp/):

~/.trace/
  .config.json              # global config + per-project sections
  registry.json             # registered projects
  index/
    my-app-a1b2c3d4e5f6.db  # per-project databases

Pruning of projects

Starting the server in a directory auto-registers it, so both registry.json and the projects[...] map in .config.json grow on their own — and both are read on every server start. The first session that causes a registration says so: the initialize instructions carry a one-off notice naming the directory and trace-mcp remove <path>, and trace-mcp list marks such entries [auto-added on MCP connect]. An hourly sweep bounds them:

Projects you registered yourself with trace add or trace init are exempt from the caps and are only removed by trace remove / trace-mcp doctor --fix. Eviction only deregisters — your directory and its index database are left alone, so an evicted project is re-registered the next time you open it.

Excluding throwaway checkouts from auto-registration

Short-lived checkouts — CI gates, git worktrees off a bare mirror, /tmp clones — otherwise auto-register as persistent projects, cold-index from scratch, and linger as “Missing folder” once deleted. Two knobs in the global config control this:

{
  "auto_register": {
    "mode": "always", // "always" | "never" | "ask" ("ask" currently behaves as "never")
    "exclude": [      // globs matched against the resolved root
      "~/.gate/worktrees/**",
      "/tmp/**",
      "/private/tmp/**",
      "$TMPDIR/**"
    ]
  }
}

Removing one checkout of a same-remote pair (trace-mcp remove <clone>) unregisters only that checkout: a database still claimed by another registered project (or a live process) is kept, and the output says so.

Checkouts that share a git remote share one index database only while they sit on the same commit. A checkout on a different commit registers onto its own database instead, so its branch-only files never land in the canonical checkout’s index. A checkout that diverges after sharing (or was registered before this guard) is served from the owner’s index read-only — no indexing, no file watcher — until its commit matches the owner’s again.

Linked worktrees: files changed on the branch

A linked git worktree is served from the main checkout’s index, so for every file the branch touched the index still holds the main version. trace-mcp computes the difference (git diff --name-status <main HEAD> run in the worktree, plus untracked files, cached for two seconds) and uses it in three places:

Nothing changes in a main checkout. Uncommitted edits in the main checkout itself are not part of the delta, and untracked files are reported but cannot be flagged (they are not in the index).

Linked worktrees: branch index

Flags fix reading, but callers, usages and change impact still walk edges stored in the main index. With worktree_index.enabled: true (opt-in, off by default) the HTTP daemon also keeps a branch index per live worktree: a copy of the main checkout’s index with the worktree delta re-indexed into it.

  1. Copy. On the first session for a worktree (a stdio proxy routing a worktree to its main checkout sends ?worktree=), the daemon copies the main index with SQLite’s online backup, a few MB per event-loop turn through the main project’s own connection: it holds no long lock, never blocks the main index’s writer and picks up its WAL. The copy lives in <data dir>/index/worktrees/ as <name>-<hash of the worktree path>-<main HEAD>-<stamp>.db plus a .json sidecar naming the worktree, the main checkout and the HEAD the main index had indexed when the copy was taken. The copy waits (up to 5 s) for the main index to finish a run and to catch up with its git HEAD, so a pull whose watcher batch is still pending is not copied half-way.
  2. Re-index the delta. The modified and untracked files are re-indexed into the copy with the worktree as root, deleted ones are removed, through the same incremental path a watcher batch takes (edge resolution and the deferred reconcile, embeddings and summaries when AI is on).
  3. Serve. While the copy is being built the session works as described above (main index, stale_on_branch); its first calls wait for the copy, together up to initial_wait_ms counted from the session’s first call. From then on index tools answer from the copy — stale_on_branch is set only for a file edited after the copy last re-indexed it — and get_index_health reports worktree.served_from: "branch_index". Session tools (journal, state, memory, pins, cross-project calls) stay on the session: each tool declares its scope where its annotations live (src/server/tool-annotations.ts), and a tool that declares none stays on the session.

The copy has no file watcher. Each call re-checks the delta (the same two-second cache) and re-indexes what changed, waiting up to sync_wait_ms for the re-index. A call that outruns the budget is not answered as current: until the re-check has worked out which of the delta’s files changed, every one of them is flagged stale_on_branch, afterwards only those still being re-indexed; when git cannot compute the delta, the copy flags everything it knows of. POST /api/projects/reindex-file for a file in the worktree (the PostToolUse hook) writes into the copy, never into the main index. While no copy is ready (still being built, refused by a limit below, main index not ready) the daemon answers 202 with {"status": "no_copy", "reason": …} and queues nothing: the file is in the git delta from then on, so the main index answers flagged until a copy exists, and the copy being built reads the worktree as it is. The copy is rebuilt once the main index has indexed a HEAD past the one it was taken at (the old copy keeps serving, and keeps following the worktree, until the new one is ready), unloaded after idle_unload_minutes without use (the file stays and is reused while the main HEAD matches), deleted by the WorktreeRemove hook (DELETE /api/projects/worktree?project=<worktree root>), and garbage-collected once the worktree directory is gone or git worktree list no longer names it.

Daemon-wide settings, in the global config:

"worktree_index": {
  "enabled": false,         // opt-in; true (or TRACE_MCP_WORKTREE_INDEX=1) turns it on
  "initial_wait_ms": 3000,  // how long the first calls wait for a copy being built
  "sync_wait_ms": 1000,     // how long a call waits for re-indexing recent edits
  "idle_unload_minutes": 30,
  "max_loaded": 3,          // copies held open at once
  "max_snapshots": 8,       // copies kept on disk
  "max_disk_mb": 8192,      // total size of the copies (0: no limit)
  "max_delta_files": 2000   // a larger delta gets no copy
}

A copy costs disk about the size of the main index and, while loaded, one SQLite connection, a pipeline and a tool-host server in the daemon. When max_loaded copies are open, a new worktree takes the slot of the least recently used copy that has been idle for a minute; a copy serving a live session is never evicted, so the newcomer answers from the main index (flagged) and tries again 30 seconds later. Not covered: the stdio local fallback (no daemon) and a worktree registered as a project of its own keep the behaviour above; the Windows WorktreeRemove hook does not call the daemon (GC still removes the copy).

Config merge order

  1. Global defaults — ~/.trace/.config.json (or ~/.trace-mcp/.config.json fallback)
  2. Per-project section — ~/.trace/.config.json → projects["/path/to/project"] (created by trace add)
  3. Local override — .trace.json (or legacy .trace-mcp.json) in the project directory
  4. Zod schema defaults — fallback values

Global config example

~/.trace/.config.json:

{
  // Global defaults (apply to all projects)
  "ai": {
    "enabled": true
    // provider defaults to "onnx" — local embeddings, no API keys
  },
  "security": {
    "max_file_size_bytes": 524288
  },

  // Per-project settings (created by `trace add`)
  "projects": {
    "/Users/me/projects/my-app": {
      "root": ".",
      "include": ["app/**/*.php", "routes/**/*.php", "src/**/*.{ts,vue}"],
      "exclude": ["vendor/**", "node_modules/**"]
    },
    "/Users/me/projects/api": {
      "root": ".",
      "include": ["src/**/*.ts"],
      "exclude": ["node_modules/**", "dist/**"]
    }
  }
}

Per-project config file (optional)

You can place a config file at .trace/.config.json in your project root to override settings without editing the global config:

// /path/to/project/.trace/.config.json
{
  "include": ["src/**/*.ts", "lib/**/*.ts"],
  "exclude": ["node_modules/**", "dist/**", "coverage/**"],
  "ignore": {
    "directories": ["generated", "proto"],
    "patterns": ["**/fixtures/**", "**/*.generated.ts"],
    // Respect the project's root .gitignore when walking (default: true).
    // Set false to index git-ignored trees too — vendored and generated code
    // then competes with your own in every search result.
    "gitignore": true
  }
}

Alternative locations (checked in order): .trace/.config.json, .trace.json, .trace-mcp/.config.json, .trace-mcp.json, .trace-mcp, .config/trace.json, .config/trace-mcp.json, package.json (under "trace-mcp" key). The trace-mcp-spelled names keep working permanently — nothing is removed.


Migration from trace-mcp to trace

The project is trace-mcp; the command is trace. The rename lives at exactly that boundary — the CLI verb, the MCP server key written into client configs, and the local state directory shorten to trace, and nothing else does: not the npm package, not this repo, not the domain, not the registry entry. The reason is ergonomics, not efficiency — the same shape as rg for ripgrep or kubectl for kubernetes. The measured token saving from the shorter tool prefix (mcp__trace__<tool> vs. mcp__trace-mcp__<tool>) is real but small: 66–366 tokens per turn depending on tokenizer and preset, 0.74–1.23% of a tool list that already costs 8k–45k tokens.

The npm package keeps its name. It is trace-mcp and stays trace-mcp: trace on npm is an unrelated package by another author, so npm install -g trace would install someone else’s code. Install with npm install -g trace-mcp or npx -y trace-mcp@latest.

  Before After
npm package trace-mcp trace-mcp (unchanged)
Command trace-mcp <cmd> trace <cmd> — trace-mcp kept as an alias
MCP server key in client configs trace-mcp trace
State directory ~/.trace-mcp/ ~/.trace/, falling back to ~/.trace-mcp/
Project config .trace-mcp.json .trace.json, then .trace-mcp.json
Plugin / registry ids trace-mcp trace-mcp (unchanged)

trace init and trace upgrade rename an existing mcpServers["trace-mcp"] entry to mcpServers["trace"] in every client they can write to, preserving whatever else you configured on that entry. Nothing is deleted, and an entry you leave spelled trace-mcp keeps connecting — it just costs the longer tool prefix. Existing indexes are not rebuilt.


.traceignore

Place a .traceignore file in your project root to exclude files and directories from indexing. It uses the same syntax as .gitignore:

# Skip generated code
generated/
**/generated/**

# Skip protobuf definitions
proto/

# Skip test fixtures
tests/fixtures/

# Skip specific file patterns
*.generated.ts
*.pb.go

# Negation — re-include something
!proto/important.proto

Difference from .gitignore

  .gitignore .traceignore
Effect Files are indexed for the dependency graph, but source content is hidden from AI output Files are completely skipped — not indexed at all
Use case Secrets, credentials, env files Generated code, vendored deps, large data files

Built-in skip directories

These directories are always skipped (no configuration needed):

node_modules, .git, dist, build, .next, __pycache__, .venv, vendor, .trace-mcp, coverage, .turbo

You can add more directory names to skip via .traceignore or the ignore.directories config key — both only add to the skip list, they don’t remove anything from it.

trace-mcp add / trace-mcp index print a “Skipped top-level folders” line after indexing listing every top-level directory that got skipped this way, so a folder missing from the index isn’t a silent surprise.

Getting a skipped folder indexed

Two different things can leave a folder out of the index — check which one applies:

  1. The folder name collides with a built-in skip dir (e.g. you have your own vendor/ or build/ with real source in it). There’s currently no per-project way to un-skip a built-in name — rename the folder, or open an issue if this collision is common enough to warrant a config override.
  2. The folder just isn’t a built-in skip dir, but nothing in include matches its files. The default include covers every extension a registered language plugin claims, except the pure data formats — JSON, XML (.xml, .svg, .csproj, …) and INI (.ini, .conf, .properties, …) — which are left out because lockfiles and fixtures would swamp the index. Add an explicit pattern for those:

    // .trace/.config.json
    {
      "include": ["schemas/**/*.json"]
    }
    

    include in a per-project config file replaces the built-in list rather than adding to it (config merge is shallow) — copy the defaults from src/config.ts alongside your addition if you still want the rest of the project indexed.


Options

Option Type Default Description
root string "." Project root directory
include string[] Auto-detected Glob patterns for files to index
exclude string[] Common exclusions Glob patterns to skip
follow_symlinks boolean false Follow directory symlinks during file discovery. Leave off unless you know the tree is free of symlink cycles — enabling it on a tree with a cycle (e.g. Ansible Molecule’s roles/<role>/molecule/<scenario>/roles/<role> -> ../../../ layout) can silently truncate traversal. Symlinked files are always skipped regardless of this setting.
ignore.directories string[] [] Extra directory names to skip (added to built-in list)
ignore.patterns string[] [] Extra gitignore-style patterns to exclude from indexing
plugins string[] [] Paths to custom plugins — see development for the plugin interface
security.secret_patterns string[] Common patterns Regex patterns for secret filtering
security.max_file_size_bytes number 524288 Max file size to index (bytes)

Framework-specific options

{
  "frameworks": {
    "laravel": {
      "artisan": {
        "enabled": true,    // Enable artisan integration
        "timeout": 10000    // Command timeout in ms
      },
      "graceful_degradation": true  // Continue if artisan fails
    }
  }
}

AI configuration

AI features enable semantic search (vector embeddings) and optional LLM-powered summarization. trace-mcp supports three embedding providers, with a zero-config local option as the default.

Provider overview

Provider Embeddings LLM (summarization) Requires Setup
onnx (default) ✓ local, offline ✗ @huggingface/transformers (optional dep) Zero-config — model auto-downloads (~23 MB) on first use
ollama ✓ via Ollama ✓ via Ollama Running Ollama instance Install Ollama + pull models
lmstudio ✓ via LM Studio ✓ via LM Studio LM Studio server running OpenAI-compatible, no API key
openai ✓ ✓ API key api_key or OPENAI_API_KEY env
anthropic ✗ (no embeddings API) ✓ API key api_key or ANTHROPIC_API_KEY env
gemini ✓ ✓ API key Google Generative Language API (consumer) — api_key (AIza…) or GEMINI_API_KEY env
vertex ✓ ✓ OAuth token + GCP project Google Vertex AI (GCP) — api_key = access token, plus vertex_project + vertex_location
voyage ✓ (code-tuned) ✗ API key Voyage AI embeddings only — pair with another provider for inference
mistral / groq / together / deepseek / xai ✓ ✓ API key OpenAI-compatible endpoints — per-provider *_API_KEY env

Minimal setup — local embeddings (no API keys)

{
  "ai": {
    "enabled": true
    // provider defaults to "onnx"
    // model defaults to Xenova/all-MiniLM-L6-v2 (384 dims, Apache 2.0)
    // auto-downloads ~23 MB on first embed_repo or semantic search
  }
}

This enables semantic/hybrid search and query_by_intent with zero configuration. No API keys, no external services, works fully offline after first model download.

Full setup — Ollama (embeddings + LLM summarization)

{
  "ai": {
    "enabled": true,
    "provider": "ollama",
    "base_url": "http://localhost:11434",
    "inference_model": "gemma4:e4b",
    "fast_model": "gemma4:e4b",
    "embedding_model": "qwen3-embedding:0.6b",
    "embedding_dimensions": 1024,
    "summarize_on_index": true,
    "summarize_batch_size": 20,
    "summarize_kinds": ["class", "function", "method", "interface", "trait", "enum", "type"],
    "concurrency": 4
  }
}

Ollama embedding dimensions — match your model. Ollama embedding models vary in output dimensionality (nomic-embed-text → 768, qwen3-embedding:0.6b → 1024, mxbai-embed-large → 1024, etc.). When ai.embedding_dimensions is omitted, trace-mcp auto-detects the real dimension by probing the model on first use, so you don’t have to set it. When you do set it, the value MUST equal the model’s real dimension — a wrong value makes every vector insert fail with a dimension mismatch, and embed_repo then returns status: "error" (dimension_mismatch) rather than a silent 0-coverage “completed”. If you switch to a model with a different dimension, either update embedding_dimensions to match (or remove it) and re-run embed_repo({ force: true }).

Full setup — OpenAI

{
  "ai": {
    "enabled": true,
    "provider": "openai",
    "api_key": "sk-...",
    "inference_model": "gpt-4o-mini",
    "embedding_model": "text-embedding-3-small",
    "embedding_dimensions": 1536,
    "summarize_on_index": true
  }
}

Full setup — Google Gemini (consumer API)

Uses the Google Generative Language API (generativelanguage.googleapis.com) with a simple AIza… API key from ai.google.dev. For GCP-governed workloads, use the vertex provider instead.

{
  "ai": {
    "enabled": true,
    "provider": "gemini",
    "api_key": "AIza...",
    "inference_model": "gemini-2.5-flash",
    "embedding_model": "text-embedding-004",
    "embedding_dimensions": 768
  }
}

Full setup — Google Vertex AI (GCP)

Uses Vertex AI with a short-lived OAuth2 access token (~1h TTL). Generate via gcloud auth print-access-token — you’re responsible for refreshing it.

{
  "ai": {
    "enabled": true,
    "provider": "vertex",
    "api_key": "ya29....",                // `gcloud auth print-access-token`
    "vertex_project": "my-gcp-project",
    "vertex_location": "us-central1",
    "inference_model": "gemini-2.5-flash",
    "embedding_model": "text-embedding-005",
    "embedding_dimensions": 768
  }
}

Environment variables: GOOGLE_ACCESS_TOKEN, GOOGLE_CLOUD_PROJECT, GOOGLE_CLOUD_LOCATION are honored when the config fields are unset.

Full setup — Voyage AI (embeddings only)

Voyage specializes in retrieval-grade embeddings. voyage-code-3 is tuned for source code and is the recommended default for this project. Voyage has no inference API — keep features.inference disabled, or layer Voyage embeddings on top of Anthropic/OpenAI/Ollama for summarization by switching providers per-capability in your own setup.

{
  "ai": {
    "enabled": true,
    "provider": "voyage",
    "api_key": "pa-...",                   // or VOYAGE_API_KEY env
    "embedding_model": "voyage-code-3",
    "embedding_dimensions": 1024,
    "features": { "embedding": true, "inference": false, "fast_inference": false }
  }
}

All options

Option Default Description
ai.enabled false Enable AI features
ai.provider "onnx" onnx, ollama, lmstudio, openai, anthropic, gemini, vertex, voyage, mistral, groq, together, deepseek, xai
ai.base_url — Custom API endpoint (providers that honor it)
ai.api_key — API key, or OAuth access token for vertex. Env fallbacks: OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY, GOOGLE_ACCESS_TOKEN, VOYAGE_API_KEY, etc.
ai.vertex_project — Vertex only — GCP project ID (or GOOGLE_CLOUD_PROJECT env)
ai.vertex_location us-central1 Vertex only — GCP region (or GOOGLE_CLOUD_LOCATION env)
ai.inference_model — LLM for explanations and reviews (ollama/openai only)
ai.fast_model — Faster LLM for lightweight tasks (ollama/openai only)
ai.embedding_model auto per provider "Xenova/all-MiniLM-L6-v2" (onnx), "qwen3-embedding:0.6b" (ollama), "text-embedding-3-small" (openai)
ai.embedding_dimensions auto per provider 384 (onnx), 768 (ollama), 1536 (openai)
ai.summarize_on_index false Auto-summarize symbols after indexing (requires ollama/openai with LLM model)
ai.summarize_batch_size 20 Symbols per summarization batch
ai.summarize_kinds ["class", "function", ...] Symbol kinds to summarize
ai.summarize_max_tokens 100 Output-token budget per symbol summary — raise (e.g. 1024) for reasoning models that otherwise return empty summaries (GH#1423)
ai.rerank_max_tokens 200 Output-token budget per rerank call — same reasoning-model caveat; an empty rerank response degrades every semantic search to RRF order while still paying full rerank latency. Scaled up automatically as max(base, n*6+32) for large candidate lists (GH#1442)
ai.rerank_max_candidates 100 Max documents sent to the LLM per rerank call — the RRF-fused head is kept, the tail fills back in RRF order so large limits still return full rows (GH#1442)
ai.concurrency 1 Max parallel requests to AI provider (1–32)
ai.reranker_model — Model for search result reranking (ollama/openai only)

When the embedding endpoint is unreachable: connection-level failures (ECONNREFUSED/ENOTFOUND — e.g. LM Studio or Ollama not running) fail fast without retries and log exactly one warn summary per daemon process (“Embeddings unavailable — …, search is FTS-only”). The circuit breaker trips on the first such failure and then pauses for 10 minutes. Other failures (HTTP 5xx, timeouts, mid-run disconnects after successful batches) keep the retry + Embedding batch failed error path — those are real regressions. The pause is stored in the project DB, so it survives a daemon restart instead of re-attempting the whole backlog on every start. While it is open, get_index_health reports embedding: { enabled, totalSymbols, embedded, coveragePct, queued, pausedUntil, lastError } and adds a warning — semantic and hybrid search results are incomplete until the provider is reachable. embed_repo ignores the pause and retries immediately.

Vector coverage is reported whenever symbols exist — not only while a pause is open (TRA-1904). ai.enabled defaults to false, so a project that never configured AI reports enabled: false, coveragePct: 0 with an FTS-only warning instead of an absent block; an undrained backlog with no recorded failure is reported as degraded with a backfill hint.

ai.enabled takes effect per project start. A project already open in the daemon keeps the config it was started with, so flipping ai.enabled off stops embedding for that project only after it restarts.

ONNX provider details: Uses @huggingface/transformers (installed as optional dependency). The default model Xenova/all-MiniLM-L6-v2 is Apache 2.0 licensed, produces 384-dimensional L2-normalized mean-pooled vectors, and weighs ~23 MB. The model is cached locally after first download. You can use any ONNX-compatible model from HuggingFace by setting embedding_model.

Ollama parallelism: When setting concurrency > 1, you must also configure Ollama to handle parallel requests. The desktop app UI does not expose this setting — use one of these methods:

Option 1 — Environment variable for the desktop app (macOS):

launchctl setenv OLLAMA_NUM_PARALLEL 4

Then quit and reopen the Ollama app. The variable persists until logout.

Option 2 — Run from terminal instead of the desktop app:

OLLAMA_NUM_PARALLEL=4 ollama serve

Option 3 — Persist via shell profile (add to ~/.zshrc):

export OLLAMA_NUM_PARALLEL=4

Then source ~/.zshrc and restart Ollama.

Set OLLAMA_NUM_PARALLEL to match your ai.concurrency value. Higher parallelism uses more VRAM/RAM — start with 2–4 and increase if your hardware allows.


LSP enrichment

trace-mcp can optionally use Language Server Protocol (LSP) servers to enrich call graph edges with compiler-grade type resolution. This resolves dynamic dispatch, interface polymorphism, generics, and other cases that tree-sitter AST analysis alone cannot handle.

Disabled by default — opt-in via configuration. When enabled, LSP runs as a post-indexing enrichment pass (Pass 3) after the standard tree-sitter indexing completes. If an LSP server is not installed or fails to start, indexing continues normally without LSP edges.

{
  "lsp": {
    "enabled": true,              // default: false — must opt-in
    "auto_detect": true,          // default: true — auto-detect available LSP servers
    "max_concurrent_servers": 2,  // default: 2 — limit parallel LSP processes
    "enrichment_timeout_ms": 120000, // default: 120000 — overall enrichment timeout
    "batch_size": 100,            // default: 100 — symbols per batch
    "servers": {                  // optional: override auto-detected server commands
      "typescript": {
        "command": "npx",
        "args": ["typescript-language-server", "--stdio"],
        "timeout_ms": 30000
      }
    }
  }
}
Option Default Description
lsp.enabled false Enable LSP enrichment pass
lsp.auto_detect true Auto-detect available LSP servers based on project files
lsp.max_concurrent_servers 2 Maximum number of LSP servers running simultaneously
lsp.enrichment_timeout_ms 120000 Overall timeout for the entire LSP enrichment pass
lsp.batch_size 100 Number of symbols to process per batch
lsp.servers.<lang>.command — Override the LSP server command for a language
lsp.servers.<lang>.args [] Arguments for the LSP server command
lsp.servers.<lang>.timeout_ms 30000 Per-request timeout for this server
lsp.servers.<lang>.initializationOptions — Custom LSP initialization options

Auto-detected servers

Language Server Detection
TypeScript/JavaScript typescript-language-server tsconfig.json or package.json exists
Python pyright-langserver pyproject.toml, requirements.txt, or setup.py exists
Go gopls go.mod exists
Rust rust-analyzer Cargo.toml exists

Servers are only started if the corresponding language has files in the index AND the server binary is available on PATH.


Tool exposure & agent behavior

The tools.* section controls what the MCP server injects into every session — tool set, instruction verbosity, and optional agent behavior rules.

{
  "tools": {
    "preset": "standard",                // "full" | "standard" | "minimal" | "review" | "architecture" | "dev" | "security" | "design" | "perf" | "router"
    "description_verbosity": "full",     // "full" | "minimal" | "none"
    "instructions_verbosity": "full",    // "full" | "minimal" | "none" — controls the tool-routing block
    "client_profile": "auto",            // "auto" | "off" | "claude-code" | "codex" | "cursor" | "vscode" | "generic"
    "agent_behavior": "off",             // "strict" | "minimal" | "off" — see below
    "meta_fields": true,                 // true | false | ["_hints", "_budget_warning", ...]
    "compact_schemas": false             // strip advanced params from tool schemas (saves tokens)
  }
}
Option Default Description
tools.preset "minimal" Tool preset — the number is the upper bound on the tool surface; framework-gated tools only appear when the framework is detected. minimal (29 tools, default), standard (61 tools — covers >99% of real-world tool calls per session-log mining), review (33 tools), architecture (42 tools), dev (44 tools), security (36 tools), design (26 tools), perf (35 tools), router (10 tools — see The router preset), or full (every registered tool, opt-in). A preset is a deferral, not a restriction: everything outside it is registered but hidden, and load_tools pulls any of it in mid-session. tools.exclude remains a hard restriction that load_tools cannot undo.
tools.include — Whitelist specific tools by name
tools.exclude — Blacklist specific tools by name
tools.description_verbosity "full" Per-tool description length. minimal = first sentence. none = empty
tools.instructions_verbosity "full" Server-level instructions (the tool-routing block). full ~2K tokens, minimal ~200
tools.client_profile "auto" Tailors the advertised surface to the connected host — see Client profiles. auto detects it from the initialize handshake, a profile name pins it, off disables the layer. Env override: TRACE_MCP_CLIENT_PROFILE
tools.agent_behavior "off" Behavior rules appended to instructions — see Agent behavior rules
tools.meta_fields true Meta fields in responses (_hints, _budget_warning, etc.). Set false or list to narrow
tools.compact_schemas false Strip advanced/optional params from tool schemas. Cuts schema size ~42% (measured 2026-08-29)

Progressive tool disclosure

A preset used to be permanent: a tool outside it was never registered, so the session that saved schema tokens also lost the tool for the rest of its life. That made the small presets a bad trade, and most sessions ran full and paid the whole surface up front.

Since v3.3 a preset is a deferral. Tools outside it are registered but disabled — absent from tools/list, so you don’t pay their schemas — and the always-available load_tools pulls any of them in mid-session:

load_tools()                                  # list what this session deferred
load_tools({ tools: ["taint_analysis"] })     # load one
load_tools({ preset: "architecture" })        # load a preset's worth
load_tools({ preset: "full" })                # load everything deferred

Configs written before v3.3 pinned "preset": "full" — that was the default at the time, not a choice, and it kept those installs paying for the whole surface long after the default moved. Upgrading rewrites that one value to the shipped default, once and without asking, and records that it did so; set full (or any other preset) yourself afterwards and no later upgrade will touch it. To go back to the old behaviour, set "preset": "full" or run load_tools({ preset: "full" }).

Loading emits notifications/tools/list_changed, and clients that honour it (Claude Code among them) re-read the larger surface and can call the new tools directly. Clients that ignore the notification are not stuck: load_tools returns each loaded tool’s full JSON schema in its response, and the loaded tool is immediately reachable through batch — batch({ calls: [{ tool, args }] }) — which is in every preset.

batch dispatches by name against the whole registry, deferred tools included, so a deferred tool is callable through it without loading it first. That is deliberate (it is what the router preset below is built on), and the price is one round-trip’s worth of schema you never see: you have to know the tool’s arguments, or read them from load_tools. tools.exclude is not reachable this way — the exclusion is checked on the inner call names too, on both the local and the daemon-backed path.

What escalation cannot do is widen tools.exclude. Exclusion stays a hard restriction; load_tools reports those names under blocked and leaves them off. If you want a tool gone, exclude it — don’t rely on the preset.

A preset name that doesn’t resolve — a typo, or a preset added in a version newer than the one installed — falls back to minimal and logs a warning naming the available presets. Before v3.12 it fell back to full, which turned a typo in a flag set to save tokens into a 36.3k-token surface instead of a 7.8k one. Failing toward the cheap surface costs at most one load_tools round-trip.

Measured tools/list cost of each preset on this repo (serialized chars, then o200k tokens, 2026-09-01; router added 2026-09-02): router 7.1k / 1.6k, design 21.9k / 5.0k, perf 32.3k / 7.5k, minimal 34.0k / 7.8k, review 37.3k / 8.6k, security 41.5k / 9.6k, architecture 44.3k / 10.2k, dev 54.5k / 12.6k, standard 64.6k / 14.9k, full 157.7k / 36.3k. Against full, that is a 67% cut on the widest role preset (dev) and 86% on the narrowest (design) — framework-gated tools are excluded, so a project that detects the matching framework pays more. load_tools itself is 0.9k of that — the price of making the other 123k optional.

The router preset

"preset": "router" advertises no code-intelligence tools at all — only the session meta-tools that are never gated, load_tools and batch among them. Ten tools, 1.6k tokens, 95.6% below full and 79.2% below the minimal default.

It is usable rather than crippled because the two halves cover each other: load_tools() names the ~150 deferred tools (names only — the schemas are what you are not paying for), and batch calls any of them directly. So the session pays for a catalog instead of a surface, and there is no escalation round-trip unless you want the schemas.

{ "tools": { "preset": "router" } }      // or: trace-mcp --preset router

It is opt-in and will not become the default. On a host that already defers tool schemas itself — Claude Code’s ToolSearch, which keeps only the 15 ALWAYS_LOAD_TOOLS eagerly loaded — router saves ~2.9k tokens and takes away exactly the first-five-minutes tools that stamp exists to protect. On a host without such a mechanism it saves ~6.2k per session, every session. Take it if your client has no tool deferral of its own, or if you are running many short sessions where the surface is most of what you pay. For what other MCP servers charge for the same surface, see how trace-mcp compares.

Client profiles

The preset decides how much capability a session advertises. The client profile decides what that particular host does not need to be told about.

The connected client names itself in the initialize handshake, so trace-mcp knows whether it is talking to Claude Code, Codex, Cursor, VS Code, or something it has never seen. Two things follow from that:

The profile composes after the preset — it only removes names from whatever the preset already advertised — and it never removes capability. A suppressed tool stays callable by name, load_tools({ tools: ["search_text"] }) puts it back on tools/list, and "client_profile": "off" (or TRACE_MCP_CLIENT_PROFILE=off) disables the layer entirely. An unrecognised host resolves to generic, which suppresses nothing.

Measured on a live initialize + tools/list round-trip against the built server, default minimal preset (serialized chars, 2026-08-30):

Client instructions tools/list advertised handshake total
generic (or client_profile: "off") 7,669 36,523 28 44,192
claude-code 7,933 34,621 27 42,554 (−3.7%)
codex 7,927 34,621 27 42,548 (−3.7%)
cursor 7,928 34,621 27 42,549 (−3.7%)

The instructions grow slightly because the profile appends one line naming what it hid and how to get it back — without it a suppressed tool is invisible, since load_tools() lists what the preset deferred and a suppressed tool is inside the preset.

Every tools.* option works from a project-local config file (.trace/.config.json) as well as the global one — none of them are global-only. The tool surface is built once per MCP session, so a change takes effect on the next session (restart the MCP client); the daemon does not need restarting.

Agent behavior rules

tools.agent_behavior appends generic discipline rules (anti-sycophancy, anti-fabrication, goal-driven execution, 2-strike session hygiene, no drive-by refactors) to the server instructions. These are client-agnostic — every MCP-compatible client (Claude Code, Cursor, Codex, Windsurf, …) receives them.

Value What ships When to use
"off" (default) Nothing Default — you already manage agent behavior elsewhere (CLAUDE.md, tweakcc), or don’t want opinionated rules
"minimal" One rule: never fabricate paths/symbols/APIs — call search/get_symbol/run the command Minimal nudge tied to trace-mcp tool use, no personality prescription
"strict" 8 rules: no flattery, disagree on wrong premises, never fabricate, stop when confused, goal-driven execution, verify before reporting “done”, 2-strike rule, surgical changes only Max-tier default — aligns agent behavior across a team

Auto-set by trace-mcp init: picking the Max enforcement level writes "agent_behavior": "strict" to your global config. Picking Base/Standard writes "off". Re-run init to change tiers — the value updates idempotently.

Why it lives in MCP instructions (not CLAUDE.md or tweakcc):

If you want to override in one project without affecting others, put "agent_behavior": "off" (or any other value) in that project’s .trace/.config.json — per-project config takes precedence over global.

4-tier resolution system

Every edge in the call graph carries a resolution_tier indicating how it was resolved:

Tier Source Confidence
scip_resolved Offline SCIP index ingestion (opt-in) Compiler-grade (highest)
lsp_resolved LSP call hierarchy Compiler-grade
ast_resolved Tree-sitter + module resolution Static AST (default)
ast_inferred Heuristic inference from imports Medium
text_matched Name/text similarity matching Lowest

The get_call_graph tool reports a resolution_tiers summary showing the distribution across all edges, so you can see how much of the graph has compiler-grade confidence.


Topology & subprojects

trace-mcp includes a topology layer for cross-service analysis and a subproject layer for linking dependency graphs across subprojects within a project.

A subproject is any working repository that is part of your project’s ecosystem: microservices, frontends, backends, shared libraries, CLI tools, etc. Each directory with its own root marker (package.json, composer.json, go.mod, etc.) is a subproject. A project contains one or more subprojects; the project itself is not a subproject. Subprojects can live inside the project directory (e.g. project/frontend/) or outside it (added manually via subproject add).

Both topology and subprojects are enabled by default — every indexed project auto-detects its subprojects.

{
  "topology": {
    "enabled": true,           // default: true — enable topology + subproject tools
    "auto_discover": true,     // default: true — auto-detect and register subprojects on indexing
    "auto_detect": true,       // default: true — auto-detect from Docker Compose
    "repos": [],               // additional repo paths to include in topology
    "contract_globs": []       // explicit contract file patterns (e.g. ["api/openapi.yaml"])
  }
}
Option Default Description
topology.enabled true Enable topology and subproject tools
topology.auto_discover true Auto-detect and register subprojects on every index
topology.auto_detect true Auto-detect subprojects from Docker Compose / workspace structure
topology.repos [] Additional repo paths to include in the topology graph
topology.contract_globs — Explicit paths to API contract files (relative to project root)

Auto-discovery flow

When a project is indexed (via serve, serve-http, or index):

  1. Subprojects are detected within the project root using these strategies (in order):
    • Docker Compose — parses docker-compose.yml / compose.yml for service definitions
    • Flat workspace — scans first-level subdirectories for root markers (package.json, composer.json, go.mod, etc.). Requires ≥2 found (e.g. project/frontend/ + project/backend/)
    • Grouped workspace — scans two levels deep (root/group/service/). Requires ≥2 found (e.g. project/org/service-a/ + project/org/service-b/)
    • Monolith fallback — treats the project root as a single subproject
  2. Each detected subproject is registered and bound to the project in ~/.trace/topology.db
  3. API contracts are parsed (OpenAPI, GraphQL SDL, Protobuf) for each subproject
  4. Code is scanned for HTTP/gRPC client calls (fetch, axios, Http::, requests, etc.)
  5. Client calls are matched to known endpoints from other subprojects
  6. Cross-subproject edges are created

This is non-blocking — the server starts immediately, and subproject syncs in the background.

Disabling

To disable auto-discovery while keeping topology tools:

{ "topology": { "enabled": true, "auto_discover": false } }

To disable everything:

{ "topology": { "enabled": false } }

Subproject CLI

# Add a subproject (can be inside or outside project dir)
trace-mcp subproject add --repo=../service-b --project=. [--contract=openapi.yaml] [--name=my-service]
trace-mcp subproject remove <name-or-path>
trace-mcp subproject list [--project=.] [--json]
trace-mcp subproject sync
trace-mcp subproject impact --endpoint=/api/users [--method=GET] [--service=user-svc]

Supported contract formats

Format Auto-detected files
OpenAPI / Swagger openapi.yml, openapi.yaml, openapi.json, swagger.yml, swagger.yaml, swagger.json, api-spec.yml, api-spec.yaml, api-spec.json
GraphQL SDL schema.graphql, schema.gql
Protobuf / gRPC *.proto

Supported client call patterns

The scanner detects HTTP/gRPC/GraphQL calls in 12+ patterns across all supported languages:

Pattern Languages Example
fetch() JS/TS fetch('/api/users')
axios.*() JS/TS axios.get('/api/users')
Http::*() PHP/Laravel Http::post('/api/orders')
requests.*() Python requests.get('https://api.example.com/users')
http.Get/Post() Go http.Get("http://svc/api/users")
RestTemplate.*() Java/Kotlin .getForObject("/api/users")
gRPC stubs All client.GetUser()
GraphQL operations All query GetUser { ... }

Cross-project tools

Every MCP session is still attached to exactly one project (see stdio vs HTTP) — but two tools let an agent reach across to any OTHER project already registered with trace-mcp, without opening a second MCP connection:

This is read-only relay wiring — it never starts indexing or a file watcher for the target project. In the HTTP daemon, a target project already warm in memory is served directly; a cold one is opened on demand via the same read-mostly path used for on-demand subprojects. Under stdio (no daemon), the target project’s existing index database is opened directly; a project that has never been indexed cannot be relayed to.

No existing tool’s schema changes because of this — call_project_tool dispatches to the target project’s own registered handler for tool, so that tool’s contract is exactly what it is when called directly.


Supported MCP clients

trace init (or trace-mcp init) detects installed MCP clients and writes a trace server entry into each one’s native config format (with trace-mcp legacy compatibility preserved). Pick clients interactively, or pass --mcp-client <name> for non-interactive runs. For an exhaustive audit of guard hooks, routing blocks, and client config files written by this command, see what trace init installs.

Client Config path Format Top-level key Notes
Claude Code ~/.claude.json, <project>/.mcp.json JSON mcpServers Supports Base / Standard / Max enforcement tiers (hooks, tweakcc)
Claw Code ~/.claw/settings.json, <project>/.claw.json JSON mcpServers Same enforcement tiers as Claude Code
Claude Desktop ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) JSON mcpServers Quit Claude.app before init — it overwrites foreign keys on preference flush
Cursor ~/.cursor/mcp.json, <project>/.cursor/mcp.json JSON mcpServers Also writes .cursor/rules/trace.mdc
Windsurf ~/.windsurf/mcp.json, <project>/.windsurf/mcp.json JSON mcpServers Also writes .windsurfrules
Continue ~/.continue/mcpServers/mcp.json JSON mcpServers  
Junie ~/.junie/mcp/mcp.json JSON mcpServers  
JetBrains AI Assistant IDE-internal XML — — Manual: Settings → Tools → AI Assistant → MCP. Use “Import from Claude” if Claude Desktop is configured
Codex ~/.codex/config.toml, <project>/.codex/config.toml TOML [mcp_servers.trace] A legacy [mcp_servers.trace-mcp] table is detected and renamed on init. Restart Codex after init/update — it reads config.toml once at startup. See Verifying the Codex connection
Hermes Agent $HERMES_HOME/config.yaml (default ~/.hermes/config.yaml) YAML mcp_servers Always global; also writes AGENTS.md and pre-allowlists hooks
AMP (Sourcegraph) ~/.config/amp/settings.json[c], <project>/.amp/settings.json[c] JSON / JSONC amp.mcpServers (literal dot in key) Comments and formatting preserved via jsonc-parser. Also writes AGENTS.md
Warp Cloud-synced storage (no writable file) — — Manual: Settings → Agents → MCP servers → + Add → paste JSON. If Claude Code is also configured, enable “File-based MCP servers” so Warp inherits trace from ~/.claude.json. Also writes AGENTS.md
Factory Droid ~/.factory/mcp.json, <project>/.factory/mcp.json JSON mcpServers (entries need type: "stdio") Also writes AGENTS.md
Cline <VS Code User>/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json JSON mcpServers VS Code extension; global-only (globalStorage has no per-project variant). Detected only when the extension’s settings dir exists
Kilo Code <VS Code User>/globalStorage/kilocode.kilo-code/settings/mcp_settings.json JSON mcpServers Legacy VS Code extension config. The newer Kilo CLI (≥ v7) uses a non-standard ~/.config/kilo/kilo.jsonc shape (mcp key, command as array) that trace does not write — configure that manually if you use the CLI
Antigravity (Google) ~/.gemini/config/mcp_config.json JSON mcpServers Global-only (no documented per-project config as of mid-2026). Lives next to the deprecated Gemini CLI’s ~/.gemini/settings.json, which trace no longer reads or writes — the two do not share entries
MiniMax Code (MiniMax, open-source) ~/.minimax/mcp.json (legacy ~/.mavis/mcp.json, ~/.mavis/mcp/mcp.json also read) JSON mcpServers Global-only. Their resolver migrates ~/.mavis itself; the writer prefers an existing file. Restart the app and verify the server appears (their v3.0.48 runtime has a known non-builtin pickup bug)
Zed ~/.config/zed/settings.json, <project>/.zed/settings.json (%APPDATA%\Zed\settings.json on Windows) JSON context_servers (entries stamped source: "custom") Comments and formatting preserved via jsonc-parser. Restart Zed (or reload the window) and verify the green dot in Settings → AI → MCP Servers
Kimi Code CLI (Moonshot) ~/.kimi/mcp.json JSON mcpServers Global-only; format is compatible with other MCP clients
OpenCode ~/.config/opencode/opencode.json[c], <project>/opencode.json[c] JSON / JSONC mcp (type: "local", command: ["trace", "serve"]) Comments and formatting preserved via jsonc-parser. Also writes AGENTS.md

<VS Code User> is ~/Library/Application Support/Code/User (macOS), %APPDATA%\Code\User (Windows), or ~/.config/Code/User (Linux).

Enforcement tiers (Claude Code / Claw / Desktop only):

For all other clients only the Base tier applies — there is no equivalent of Claude Code hooks or tweakcc in those tools.

Verifying the Codex connection

Codex reads config.toml once at startup, so restart it after every init or clients update — otherwise the entry sits on disk while the running session still uses the old config. Then verify from either side:

Renaming trace-mcp → trace: what init can and can’t reach

MCP clients advertise every tool prefixed with the server key, so the rename also renames the tool prefix a client-side config may reference in full: mcp__trace-mcp__search becomes mcp__trace__search. trace init (and the post-update migration that runs automatically when the daemon starts after an upgrade) rewrites this everywhere it owns the file:

What stays manual. Anything you wrote yourself outside those specific fields is not touched — most commonly, tool names spelled out in your own prose inside CLAUDE.md / AGENTS.md (init only rewrites the routing block it generated, not arbitrary text you added), or a permission/hook config in a file trace-mcp doesn’t manage the shape of. If a tool call stops matching a hook, or an allowlisted tool starts re-prompting for approval after upgrading, grep your own config for mcp__trace-mcp__ and replace it with mcp__trace__.

Multica workspace agents

Multica agents don’t run trace-mcp init — each agent’s MCP wiring is set directly with multica agent update --mcp-config-file <json> (or agent create --mcp-config-file on first setup), scoped to that one agent. An agent with no mcp_config inherits the machine’s own MCP setup, so its preset is whatever tools.preset says in ~/.trace/.config.json (minimal unless you changed it).

Setting mcp_config does not override the inherited trace-mcp entry by name — Multica launches that agent with --strict-mcp-config, so it gets only the servers you list. Everything else the machine provides (user-scope servers, plugin servers, hosted connectors) disappears for that agent. List every server the role still needs, not just trace-mcp:

{
  "mcpServers": {
    "trace-mcp": { "command": "trace-mcp", "args": ["serve", "--preset", "dev"] },
    "context7": { "command": "context7-mcp", "args": [] }
  }
}

--preset is an option of the serve subcommand — args without serve starts nothing.

multica agent update <agent-id> --mcp-config-file ./trace-mcp-dev.json

Check that the agent’s installed trace-mcp supports --preset before rolling this out. The flag shipped after 3.11.0; an older binary exits immediately with unknown option '--preset', and because a dead MCP server is silent, the agent simply runs with zero trace-mcp tools instead of a smaller set. Verify with trace-mcp --help | grep -- --preset on the machine the agent runs on. Same trap applies to a command pointing at a build inside a per-task working directory — those get cleaned up.

The role → preset matrix used in the trace-mcp workspace itself (adjust to your own roles):

Agent role Preset Why
Independent code review review rename-safety, quality gates, risk/impact assessment — no refactor or design tools
Security audit security scan_security, taint_analysis, SBOM, config audit — no refactor/design tools
Design/UX review design component tree, screens, navigation, state — no security/perf tools
Implementation & bugfixing dev refactor + codemod tools (apply_rename, extract_function, change_signature)
Performance analysis perf analyze_perf, complexity/coupling trends, risk hotspots

multica agent update --mcp-config* replaces the agent’s entire mcp_config — it does not merge. If the agent already has other private MCP servers configured, read them back first (only a workspace owner/admin can; mcp_config reads redacted for agent actors) and include them in the new payload, or the update will silently remove them.

If you can’t read it back (agent actors can’t), you can still recover the set of servers a past run had from Claude Code’s own connection logs on the machine that ran it — ~/Library/Caches/claude-cli-nodejs/<encoded-workdir>/mcp-logs-<server>/ has one directory per connected server. Under --strict-mcp-config that listing is exactly the config’s server set; only each server’s argv is still unknown.

The desktop app’s MCP Clients screen shows this wiring read-only (Multica agents section): workspace-library assignment, custom-config presence, and the preset when the raw config is readable. Writing mcp_config from the app is intentionally not offered — use the multica CLI above.


stdio vs HTTP — choosing your setup

trace speaks MCP over two transports. Which one to pick depends on whether you want a process per repo or one long-lived daemon serving many projects.

trace serve runs the server over stdio. Your MCP client launches one process per session with the working directory set to the repo you opened, so the project is auto-detected from there — no URL, no ?project=, and each session is isolated to its own repo. No daemon is required: it runs in-process, and if a daemon happens to be running it transparently reuses the warm index.

Wire it up without touching a committed file:

# Across all your projects (stored in ~/.claude.json, not committed):
claude mcp add --scope user trace -- npx -y trace-mcp@latest serve

# Just one project, not committed (local scope):
claude mcp add --scope local trace -- npx -y trace-mcp@latest serve

To share one config with the whole team, commit a portable stdio entry to .mcp.json — it carries no machine-specific URL or path:

{
  "mcpServers": {
    "trace": { "command": "npx", "args": ["-y", "trace-mcp@latest", "serve"] }
  }
}

Each developer’s session spawns its own per-repo process; nothing is shared or hardcoded. Avoid committing an absolute HTTP URL such as http://127.0.0.1:3741/mcp?project=/Users/you/... — that path only exists on your machine and would break for everyone else.

HTTP daemon — one warm index shared across many projects

trace serve-http runs a long-lived daemon (default 127.0.0.1:3741) that holds warm indexes for several registered projects and backs the desktop app. MCP clients connect with the target project in the URL:

http://127.0.0.1:3741/mcp?project=/absolute/path/to/repo

Holding several indexes warm is what the daemon costs you in RAM: daemon memory breaks the resident set down region by region and names the knob that bounds each one.

The daemon multiplexes projects — one process serves all of them — but each MCP registration is bound to a single project via ?project= (or, for clients that cannot append a query string, the X-Trace-Project header or params._meta["traceMcp/projectRoot"]). Pick this when you want one warm index reused across sessions and tools and you’re comfortable managing the per-registration URL. For one-session-per-repo workflows, stdio is simpler.

The daemon’s trust boundary is loopback. serve-http has no authentication: every /api route and /mcp itself trust the caller, and ?project= / X-Trace-Project / params._meta["traceMcp/projectRoot"] can name any directory on the machine — the daemon will index and serve it. On 127.0.0.1 that grants nothing extra, because anything able to reach the port already runs as you and can read those files directly. Binding elsewhere hands that power to the network, so a non-loopback --host is refused unless you also pass --allow-remote, and even then you are expected to put your own authentication (SSH tunnel, reverse proxy, VPN) in front of the port.

One project per session. Both transports resolve exactly one project per MCP session — stdio from the working directory, HTTP from ?project=. A single session cannot query across repositories today; to work with several repos, register each (trace add <path>) and add one MCP entry per repo (HTTP) or open one session per repo (stdio). Cross-repo queries inside a single session — useful for multi-repo/pseudo-monorepo setups — are tracked as an enhancement in #199.

  stdio HTTP daemon
Process model one per session one shared daemon
Project scope auto-detected from cwd ?project= per registration
Config command, no URL URL with absolute path
Warm-index reuse per session (shared if a daemon is running) always shared
Best for per-repo agent sessions, committed team config desktop app, many projects, one shared index

Why daemon-backed is the default. stdio sessions prefer a running daemon (auto_spawn_daemon, on by default) because the trade that once argued against it no longer holds: proxied sessions stay thin, the first answer is served from the on-disk index snapshot without waiting for the daemon, and write work (reindexing) happens once in the daemon instead of once per session. A session on its own is fine for one-shot or sandboxed use (TRACE_MCP_NO_DAEMON=1); past a couple of concurrent sessions the shared daemon is cheaper on both memory and CPU. The resident-set breakdown behind this is in daemon memory.


Hermes Agent sessions

Hermes Agent (NousResearch) stores conversations in a SQLite database at $HERMES_HOME/state.db (default ~/.hermes/state.db) plus one DB per profile under <home>/profiles/<name>/state.db. trace reads these read-only and exposes them through:

Hermes sessions are global (no per-project binding in the upstream schema). Do not expect project scoping on the provider side.

{
  "hermes": {
    "enabled": "auto",       // "auto" (default) | true | false
    "home_override": null,   // override $HERMES_HOME / ~/.hermes resolution
    "profile": null          // scope discovery to <home>/profiles/<name>/
  }
}

With enabled: "auto" the provider is registered at boot; discovery returns an empty list when no state.db exists, so there is no penalty on machines that don’t use Hermes.


Environment variables

Variable Description
TRACE_MCP_LOG_LEVEL Log level (debug, info, warn, error)
HERMES_HOME Override for Hermes Agent storage root (default ~/.hermes). Read by discover_hermes_sessions and the Hermes session provider.
TRACE_MCP_COMPUTE_TIMEOUT_MS Wall-clock ceiling for one heavy graph traversal (default 15000).
TRACE_MCP_COMPUTE_RSS_MB Resident-memory ceiling checked during a traversal (default 3000).
TRACE_MCP_COMPUTE_MAX_ITERATIONS Iteration ceiling for one traversal (default 2000000).
TRACE_MCP_NO_COMPUTE_GUARD Set to 1 to disable all compute ceilings (debugging).

Compute ceilings

get_call_graph, get_change_impact, get_dependency_diagram, get_circular_imports, find_usages and get_pagerank tick a per-call guard inside their traversal loops. When a ceiling is hit the tool returns the partial result with a _budget_exceeded field naming the ceiling, the limit, and the elapsed time — never a tool-call error — and the daemon log gets one Compute budget exceeded line with the tool name.

The defaults come from scripts/bench-compute-guard.ts: a deliberately heavy get_call_graph (37,449 symbols, depth 5) consumes 79,577 ticks in ~85 ms, so every ceiling sits one to two orders of magnitude above anything a real query reaches. Measured tick overhead on that same query is within run-to-run noise (−2.7% … +2.1% across five runs).


CLI

(All commands support both trace <command> and legacy trace-mcp <command> aliases.)

# Setup
trace init                 # One-time global setup (MCP clients, hooks, CLAUDE.md)
trace add [dir]            # Register a project for indexing
trace list                 # List all registered projects
trace upgrade [dir]        # Upgrade all projects (or specific one) — migrations + reindex

# Server
trace serve                # Start MCP server (stdio transport)
trace serve-http           # Start HTTP/SSE server (default: 127.0.0.1:3741)
  -p, --port <port>        # Custom port
  --host <host>            # Custom host (loopback only unless --allow-remote)
  --allow-remote           # Permit a non-loopback --host (see the trust boundary note)

# Manual indexing
trace index <dir>          # Index a project directory
  -f, --force              # Force reindex all files

# Subprojects (= services bound to projects)
trace subproject add       # Add a subproject to a project
  --repo <path>            # Subproject/service path (required)
  --project <path>         # Project this subproject belongs to (required)
  --contract <paths...>    # Explicit contract file paths
  --name <name>            # Display name
trace subproject remove <name-or-path>   # Remove a subproject
trace subproject list                    # List subprojects
  --project <path>         # Filter to a specific project
  --json                   # Output as JSON
trace subproject sync                    # Re-scan all subprojects
trace subproject impact                  # Cross-subproject impact analysis
  --endpoint <path>        # Endpoint path pattern
  --method <method>        # HTTP method filter
  --service <name>         # Service name filter
  --json                   # Output as JSON

# Hooks
trace setup-hooks          # Install guard hook (blocks Read/Grep/Glob/Bash on code + Agent(Explore))
  --global                 # Install globally
  --uninstall              # Remove hook

# What trace-mcp gave back (local, measured — see docs/perf/response-tokens.md)
trace savings              # Input tokens this install got back, and what that is worth
  --json                   # Output as JSON
                           # Also printed by `trace doctor`.

# Analytics (see docs/analytics.md)
trace analytics sync       # Parse session logs into analytics DB
  --full                   # Force full rescan
trace analytics report     # Token usage report
  --period <p>             # today, week, month, all (default: week)
trace analytics optimize   # Optimization recommendations
trace analytics savings    # Real savings analysis
trace analytics benchmark  # Synthetic token efficiency benchmark
  --queries <n>            # Queries per scenario (default: 10)
  --format <fmt>           # text, json, markdown
trace analytics coverage   # Technology coverage report
trace analytics trends     # Daily usage trends
  --days <n>               # Number of days (default: 30)

Security

What counts as a failing security finding in your own code is a separate setting — quality_gates.rules.max_security_critical_findings, documented with this project’s own calibrated numbers under quality gates.

Last updated: October 13, 2026