Rust MCP server giving LLMs IDE-grade code intelligence — symbol-level navigation, semantic search, git integration. Inspired by Serena.
You are a proficient Rust developer. You follow all known good/scalable patterns. You are honest and recognize your limits and your mistakes, you own them. If you are not sure, you always ask me for feedback.
See codescout memory development-commands for the full command reference.
Always run cargo fmt, cargo clippy, and cargo test before completing any task.
To test changes via the live MCP server, always run cargo build --release first, then restart the server with /mcp. The MCP server runs the release binary — dev builds are not picked up.
docs/TODO-tool-misbehaviors.md is a living document. You MUST maintain it.
- Before starting any task, read it to know current tool limitations.
- While working, watch for: wrong edits, corrupt output, silent failures, misleading errors from codescout's own MCP tools.
- When you notice anything unexpected, add an entry to that file before continuing — even a one-liner. Capture: what you did, what you expected, what happened, and a probable cause.
- Do not wait until you finish the task. Log it immediately while context is fresh.
This applies to ALL unexpected tool behavior: edit_file, rename_symbol, replace_symbol, find_symbol, semantic_search, etc.
This is a public repo. Do not push incomplete or untested work.
masteris protected. Only cherry-picked, thoroughly tested commits land here.- All experimental work goes on the
experimentsbranch (or a dedicated feature branch). Iterate freely there. - Cherry-pick to
masteronly after: all tests pass, clippy clean, manually verified via MCP (cargo build --release+/mcprestart). - Never commit directly to
masterfor in-progress or exploratory work.
When adding a feature commit to experiments, you MUST include documentation in the same commit:
- Create
docs/manual/src/experimental/<feature-name>.md— written as final user-facing docs with a single> ⚠ Experimental — may change without notice.callout at the top. - Add a line to
docs/manual/src/experimental/index.mdlinking to the new page.
Only features, not bug fixes. Bug fixes need no experimental doc.
If a feature is removed from experiments (reverted or abandoned), delete its page and
remove its entry from index.md in the same commit.
The experimental docs stay on experiments only. master's experimental/index.md
just points to the experiments branch on GitHub — it does not list features directly.
This means no cherry-picking of docs to master; the full pages are visible to anyone
browsing the experiments branch.
When cherry-picking a feature to master, use --no-commit to bundle the doc graduation
into the same commit:
git cherry-pick --no-commit <sha>
# then make the four graduation changes:
# 1. Move docs/manual/src/experimental/<feature-name>.md to its target chapter
# 2. Remove the `> ⚠ Experimental` callout from the top of the page
# 3. Add the page to docs/manual/src/SUMMARY.md in the right place
# 4. Remove the feature's entry from docs/manual/src/experimental/index.md
git commit -m "feat(...): <description>"The experimental doc page already exists on experiments — step 1 is a git mv, not a
rewrite. The ⚠ callout and the experimental/index.md entry are the only things to remove.
Rebase note: Because the graduation commit on master includes additional doc changes,
its patch differs from the original experiments commit. Git will not auto-skip it
during the subsequent git rebase master on experiments. After rebasing, drop the
now-superseded original commit manually:
git checkout experiments
git rebase master # the original feature commit will NOT be auto-dropped
git rebase -i master # drop the original feature commit from the listFull release checklist — run from master, never from experiments or feature branches.
# 1. Bump version in Cargo.toml
# Edit version = "X.Y.Z" in Cargo.toml
# 2. Build release binary and verify
cargo build --release
cargo test
cargo clippy -- -D warnings
# 3. Commit the version bump
git add Cargo.toml Cargo.lock
git commit -m "chore: bump version to X.Y.Z"
# 4. Tag the release
git tag vX.Y.Z
# 5. Publish to crates.io
CARGO_REGISTRY_TOKEN=$(grep CARGO_REGISTRY_TOKEN .env | cut -d= -f2) cargo publish
# 6. Push commit + tag
git push
git push --tags
# 7. Create GitHub release with release notes
gh release create vX.Y.Z --title "vX.Y.Z" --notes "release notes here"
# 8. Rebase experiments on the new master
git checkout experiments && git rebase masterNotes:
- Token is stored in
.env(gitignored):CARGO_REGISTRY_TOKEN=... - Use semver: patch for bug fixes, minor for new features, major for breaking changes
- Release notes should list features, dep upgrades, and doc changes
- Always rebase
experimentsafter the release push
When a bug fix or tested feature on experiments is ready to land in master:
# 1. Commit on experiments (tests passing, clippy clean)
git add <files> && git commit -m "..."
# 2. Cherry-pick to master and push
git checkout master
git cherry-pick <commit-sha>
git push
# 3. Rebase experiments back on master (drops the cherry-picked commit automatically)
git checkout experiments
git rebase masterThis is the default workflow for all completed work. The rebase step keeps experiments
clean — git detects the cherry-pick and skips the duplicate commit automatically.
- Batch related changes into a single well-tested commit rather than committing every incremental step.
- Only commit when the full fix/feature is working — all tests pass, clippy clean, manually verified if applicable.
- Do not push after every commit. Accumulate local commits during a work session; push once when the work is solid.
- When iterating on a fix, keep working locally until the fix is confirmed, then commit the final state — not every intermediate attempt.
Progressive Disclosure & Discoverability — Every tool defaults to the most
compact useful representation. Details are available on demand via
detail_level: "full" + pagination. When results overflow, responses include
actionable hints and file distribution maps (by_file). See
docs/PROGRESSIVE_DISCOVERABILITY.md for the canonical patterns and
anti-patterns — read it before adding or modifying any tool.
Token Efficiency — The LLM's context window is a scarce resource. Tools minimize output by default: names + locations in exploring mode, full bodies only in focused mode. Overflow produces actionable guidance ("showing N of M, narrow with..."), not truncated garbage.
No Echo in Write Responses — Mutation tools (create_file, edit_file,
replace_symbol, etc.) must never echo back what the LLM just sent. The caller
already knows the path, content, and size — reflecting them wastes tokens with
zero information gain. The only new information after a write is success/failure.
Return json!("ok") for writes; reserve richer responses for cases where the
tool discovers genuinely new information (e.g. LSP diagnostics after a write).
Two Modes — Exploring (default): compact, capped at 200 items. Focused:
full detail, paginated via offset/limit. Enforced via OutputGuard
(src/tools/output.rs), a project-wide pattern not per-tool logic.
Tool Selection by Knowledge Level — Know the name → LSP/AST tools
(find_symbol, list_symbols, goto_definition, hover). Know the concept →
semantic search first, then drill down. Know nothing → list_dir +
list_symbols at top level, then semantic search.
Agent-Agnostic Design — Tool descriptions, error messages, and server instructions are the primary interface for LLMs. They must feel natural for Claude Code (our primary consumer) but work for any MCP client (Gemini CLI, Cursor, custom agents). In particular:
- Error hints should name codescout tools (
replace_symbol,insert_code), not host-specific tools (Edit,Write). The LLM should never be tempted to sidestep codescout by falling back to its host's native file editing. - The companion plugin (
codescout-companion) adds Claude Code–specific enforcement (PreToolUse hooks) but the server itself must be self-contained: its gate logic, error messages, and instructions should guide any LLM toward the right tool without relying on external hooks.
Cache-invalidation tests use a three-query sandwich — not two. The structure is:
- Query → record baseline state
- Mutate the underlying data (disk, cache, external system) without going through the normal notification path
- Query again → assert result is stale (same as baseline) — this proves the bug exists
- Trigger the invalidation (e.g.
did_change, cache flush) - Query again → assert result is fresh (reflects the mutation)
A two-query test (baseline → post-invalidation) only confirms the happy path. The stale-assertion in step 3 is what makes it a regression test — it will fail if the underlying system ever changes to eagerly re-read on every query, alerting you that the invalidation logic has become wrong or unnecessary.
See did_change_refreshes_stale_symbol_positions in src/lsp/client.rs for the canonical example.
Load-bearing rules I keep getting wrong otherwise:
RecoverableErrorfor expected, input-driven failures →isError: false(sibling calls survive)anyhow::bail!for genuine tool failures →isError: true(fatal)- Write tools return
json!("ok")— never echo content back call_content()is the MCP entry point, NOTcall()— it handles buffer routing
The project has three prompt surfaces that reference tool names:
src/prompts/server_instructions.md— injected every MCP requestsrc/prompts/onboarding_prompt.md— one-time onboardingbuild_system_prompt_draft()insrc/prompts/builders.rs— generated per-project
When tools get renamed/consolidated, all three need coordinated updates. Files
closer to the change get updated; distant ones accumulate stale refs ("distance
from change" problem). The test
server::tests::prompt_surfaces_reference_only_real_tools catches stale
tool-name mentions across all three surfaces at build time — if it fails,
either fix the stale reference or (if the token is a non-tool identifier like
a param name) add it to the test's allowlist.
Any change to tool behavior or signatures requires a prompt surface review. This includes: adding new tools, renaming tools, changing parameter semantics, adding new error/fallback modes, or modifying response shapes. Ask yourself: "Does the LLM need to know about this change to use the tool correctly?" If yes, update all three surfaces in the same commit.
When modifying system prompt surfaces, bump ONBOARDING_VERSION in
src/tools/onboarding.rs. This triggers automatic system prompt refresh for all
projects onboarded with the previous version.
Bump when the generated system prompt would reference tool names, parameters, or workflows that no longer exist:
- Tool names change (rename, consolidate)
- Tool parameter semantics change
- Server instructions (
server_instructions.md) change significantly - Onboarding prompt templates change in ways that affect the generated system prompt
Do NOT bump for:
- Bug fixes that don't change tool behavior
- Internal refactors
- Memory template changes (memories are re-read during refresh anyway)
Style guide for server_instructions.md / onboarding_prompt.md edits:
see src/prompts/README.md for the 7 writing rules (rule caps, repetition
budget, caching, etc.) and links to the research behind them. Load that only
when actually editing a prompt surface — it's not needed otherwise.
This project has a companion Claude Code plugin at ../claude-plugins/codescout-companion/ that is always active when working on codescout. You must be aware of it.
What it does:
SessionStarthook (hooks/session-start.sh) — injects tool guidance + memory hints into every sessionSubagentStarthook (hooks/subagent-guidance.sh) — same for all subagentsPreToolUsehook onGrep|Glob|Read(hooks/semantic-tool-router.sh) — blocks native Read/Grep/Glob on source files, redirecting to codescout MCP tools
Critical implication for working on this codebase:
The PreToolUse hook will block any attempt to use the native Read, Grep, or Glob tools on source code files (.rs, .ts, .py, etc). You will see PreToolUse:Read hook error if you try.
You MUST use codescout's own MCP tools to read source code:
mcp__codescout__list_symbols(path)— see all symbols in a file/dirmcp__codescout__find_symbol(name, include_body=true)— read a function bodymcp__codescout__search_pattern(pattern)— regex searchmcp__codescout__semantic_search(query)— concept-level searchmcp__codescout__read_file(path)— for non-source files (markdown, toml, json)
Configuration:
- Auto-detects codescout from
.mcp.jsonor~/.claude/settings.json - Can be overridden via
.claude/code-explorer-routing.json block_reads: falsein that config to disable blocking (dev/debug use)
See codescout memory gotchas (LSP section) for Kotlin multi-instance conflicts,
cold start behavior, circuit breaker, and LSP mux details.
Tracking: docs/issues/2026-03-24-kotlin-lsp-concurrent-instances.md
Files:
docs/PROGRESSIVE_DISCOVERABILITY.md— Canonical guide for output sizing, overflow hints, and agent guidance patterns. READ THIS before adding or modifying any tool.docs/ARCHITECTURE.md— Component details, tech stack, design principlesdocs/ROADMAP.md— Quick status overviewCONTRIBUTING.md— Contributor-facing setup + PR checklist
Memories (Claude auto-loads these; listed for reference):
architecture— 8-project workspace map, cross-project deps, CI/shared infra; per-project: module structure, key abstractions, data flowsconventions— Commit style, branch strategy, error handling rules, pre-commit requirements; per-project patternsdevelopment-commands— Full command reference (cargo, scripts, release)language-patterns— Rust anti-patterns and idiomatic patternsgotchas— Cross-project path resolution pitfalls, find_symbol truncation, Kotlin LSP, embedding model restrictions, memory leakdomain-glossary,project-overview,system-prompt,onboarding— project self-description