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:
- a root that no longer exists (after a 7-day grace, so an unmounted drive keeps its registration) and one-shot agent-run workdirs are dropped;
- at most 100 auto-registered projects are kept, least recently indexed first;
- at most 100 config sections that no registry entry claims are kept, oldest first — and only sections that still look exactly as trace-mcp generated them. A section you edited by hand (any extra key, or a comment inside it) is never evicted, however old.
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/**"
]
}
}
excludeextends the built-in one-shot patterns. An excluded root is never written toregistry.json— its index lives in the ephemeral index dir and is collected by age. When the checkout shares a git remote with an already-indexed project (including bare-mirror worktrees, which share no common dir), read-only lookups route to that canonical index instead of building a redundant one.- Supported glob dialect is
*,**,?, and[...]character classes (including[!...]negation).{a,b}brace expansion is not supported — a pattern using braces never matches. Write patterns as absolute paths (after~/$VARexpansion): a relative pattern such asgate/**matches at any depth system-wide, so prefer~/gate/**. On macOS list both/tmp/**and/private/tmp/**(/tmpis a symlink; matching resolves both sides best-effort, but an explicit pair never depends on it). mode: "never"disables every implicit registration; the daemon answers new roots with an honest “not indexed” error instead of a cold index. Deliberatetrace-mcp add/trace-mcp initalways still works.TRACE_MCP_AUTO_REGISTER_MODEandTRACE_MCP_AUTO_REGISTER_EXCLUDE(comma-separated globs) override the file — useful for gates that cannot edit the global config.TRACE_MCP_REPO_ROOTredirects the session root for bothserveand the shim’s proxy path.
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:
- Tool results (including
batchsub-calls) whosepath/file/file_path/filePathnames a changed file carrystale_on_branch: true, and the response gets one_warningsline listing them: read those files from disk. get_index_healthandGET /api/projects/worktree?project=<worktree root>return the delta (modified,deleted,untracked, both HEADs, capped at 200 paths per list).- The guard hook allows
Read(andGrepon a single file) for changed files without a consultation and routes everything else through trace as in a main checkout. Tune the hook’s cache withTRACE_MCP_GUARD_WORKTREE_TTL(seconds, default 5).
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.
- 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>.dbplus a.jsonsidecar 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. - 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).
- 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 toinitial_wait_mscounted from the session’s first call. From then on index tools answer from the copy —stale_on_branchis set only for a file edited after the copy last re-indexed it — andget_index_healthreportsworktree.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
- Global defaults —
~/.trace/.config.json(or~/.trace-mcp/.config.jsonfallback) - Per-project section —
~/.trace/.config.json → projects["/path/to/project"](created bytrace add) - Local override —
.trace.json(or legacy.trace-mcp.json) in the project directory - 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:
- The folder name collides with a built-in skip dir (e.g. you have your own
vendor/orbuild/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. -
The folder just isn’t a built-in skip dir, but nothing in
includematches its files. The defaultincludecovers 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"] }includein a per-project config file replaces the built-in list rather than adding to it (config merge is shallow) — copy the defaults fromsrc/config.tsalongside 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.). Whenai.embedding_dimensionsis 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, andembed_repothen returnsstatus: "error"(dimension_mismatch) rather than a silent 0-coverage “completed”. If you switch to a model with a different dimension, either updateembedding_dimensionsto match (or remove it) and re-runembed_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 failederror 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_healthreportsembedding: { enabled, totalSymbols, embedded, coveragePct, queued, pausedUntil, lastError }and adds a warning — semantic and hybrid search results are incomplete until the provider is reachable.embed_repoignores the pause and retries immediately.Vector coverage is reported whenever symbols exist — not only while a pause is open (TRA-1904).
ai.enableddefaults tofalse, so a project that never configured AI reportsenabled: false, coveragePct: 0with an FTS-only warning instead of an absent block; an undrained backlog with no recorded failure is reported asdegradedwith a backfill hint.
ai.enabledtakes effect per project start. A project already open in the daemon keeps the config it was started with, so flippingai.enabledoff stops embedding for that project only after it restarts.
ONNX provider details: Uses
@huggingface/transformers(installed as optional dependency). The default modelXenova/all-MiniLM-L6-v2is 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 settingembedding_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 4Then 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 serveOption 3 — Persist via shell profile (add to
~/.zshrc):export OLLAMA_NUM_PARALLEL=4Then
source ~/.zshrcand restart Ollama.Set
OLLAMA_NUM_PARALLELto match yourai.concurrencyvalue. 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:
- Tools the host already has are not advertised. A CLI coding agent arrives
with its own content search; offering ours alongside it costs schema tokens
and gives the model two ways to do one thing. Suppression lists are short and
deliberately conservative —
search_textanddiscover_hermes_sessionstoday. - The instructions name the host’s own tools. The routing block is written
for a host it cannot see, so it says “
read,content-match,globmean whatever yours are called”. Once the handshake identifies the host it saysRead/Grep/Globon Claude Code andshell(cat/rg/find) withapply_patchon Codex.
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):
- Cross-client — Cursor/Codex/Windsurf users get the same behavior without CC-specific setup.
- Auto-updates on
npm upgrade trace-mcp— no re-init required to pull new rule wording. - Single source of truth alongside the tool-routing block.
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):
- Subprojects are detected within the project root using these strategies (in order):
- Docker Compose — parses
docker-compose.yml/compose.ymlfor 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
- Docker Compose — parses
- Each detected subproject is registered and bound to the project in
~/.trace/topology.db - API contracts are parsed (OpenAPI, GraphQL SDL, Protobuf) for each subproject
- Code is scanned for HTTP/gRPC client calls (fetch, axios, Http::, requests, etc.)
- Client calls are matched to known endpoints from other subprojects
- 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:
list_projects— lists every project in~/.trace/registry.json(root, name, type, last-indexed timestamp), plus known subprojects when topology is enabled for the current session.call_project_tool { project, tool, args }— runs any of the 182 normal trace-mcp tools against a DIFFERENT registered project’s already-indexed data and returns that tool’s response verbatim.projectmust be a root fromlist_projects; an unregistered root or an unknowntoolname returns a structured{ error: { code, message, data } }payload instead of throwing.
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):
- Base —
CLAUDE.mdblock with tool routing rules. - Standard — Base + PreToolUse guard hooks that intercept built-in Read/Grep/Glob.
- Max — Standard + tweakcc system-prompt patches and strict agent-behavior rules.
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:
- Inside Codex, run
/mcp— thetraceserver should list as connected. - From a shell, run
codex mcp list— same check without opening a session. - From this repo, run
trace-mcp clients status --client codex(add--scope projectfor the<project>/.codex/config.tomllayer) — anything butup_to_datenames the drifted field instaleReason, andtrace-mcp clients update codexrewrites it.
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:
- The
mcpServersentry itself, in every supported client above. - Claude Code / Claw Code permission allowlist entries
(
permissions.allow/permissions.deny) and hookmatcherstrings, in both the globalsettings.jsonand the project-scopedsettings.local.json.
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-mcpsupports--presetbefore rolling this out. The flag shipped after 3.11.0; an older binary exits immediately withunknown 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 withtrace-mcp --help | grep -- --preseton the machine the agent runs on. Same trap applies to acommandpointing 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.
stdio — recommended for per-repo agent sessions
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-httphas no authentication: every/apiroute and/mcpitself 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. On127.0.0.1that 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--hostis 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:
discover_hermes_sessions— MCP tool that lists sessions without mining or indexing them.mine_sessions— if you pass aproject_root, the decision miner also walks every Hermes session it can see and records any decisions it finds under that project. Whenproject_rootis absent Hermes is skipped entirely — global conversations are deliberately not attributed to a guessed project.
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
- Path traversal protection — all file access validated against project root
- Symlink detection — prevents escape from project boundary
- Secret pattern filtering — configurable regex patterns filter out secrets from tool output
- File size limits — per-file byte cap prevents OOM on large files
- Artisan whitelist — only safe artisan commands allowed (when Laravel integration is enabled)
- HTTP rate limiting — 60 req/min per IP on HTTP/SSE transport
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