serena-tool-selection
MANDATORY tool-selection protocol for Serena LSP tools (mcp__serena__*) versus the Claude Code built-in Grep, Search, Read, and Edit tools for code inside the project Serena serves: which Serena tool
Files2
View on GitHubSKILL.mdskills/library/serena-tool-selection/SKILL.md
GitHub<requirement>
Serena Tool Usage
When Serena tools are available, use them for code navigation and symbol-level editing inside the project Serena serves, in preference to Search, Grep, and Read: Serena resolves definitions and usages semantically, following aliases and renamed imports that a text search misses, so a text search for a symbol can return an incomplete set even when it looks faster or simpler. The built-in tools remain the right choice for the cases listed under Step 3, and as the fallback when Serena is unavailable or cannot resolve a symbol.
</requirement> <prohibition>Symbol Searches to Route to Serena
Each row below names a text-search habit and the Serena tool that answers the same question completely, with the canonical call. Every path in a Serena call is relative to the project root, as described under "Name Paths and Relative Paths" below.
| Instead of | Use Instead | Canonical Call |
|---|---|---|
Search(pattern: "def process_data", path: "/project/src") or Grep(pattern: "def process_data") -- any Search/Grep pattern that is a function/class/method name |
find_symbol (definitions) or find_referencing_symbols (usages) |
find_symbol(name_path_pattern="process_data", include_body=True) |
Grep(pattern: "validate_input\\(...") or Search(pattern: "validate_input(", output_mode: "content") -- any search for a symbol's usages/calls |
find_referencing_symbols + Grep cross-validation when completeness matters |
find_referencing_symbols(name_path="validate_input", relative_path="src/validation.py") |
Read entire file to understand structure |
get_symbols_overview |
get_symbols_overview(relative_path="src/module.py") |
Multiple Edit calls to rename a symbol |
rename_symbol |
rename_symbol(name_path="old_func", relative_path="src/module.py", new_name="new_func") |
Finding function boundaries then Edit |
replace_symbol_body |
replace_symbol_body(name_path="fn", relative_path="src/module.py", body="def fn():\n return 42") |
Finding method end line then Edit |
insert_after_symbol |
insert_after_symbol(name_path="existing", relative_path="src/module.py", body="def new():\n pass") |
find_referencing_symbols then manually checking results and deleting via Edit -- manual ref-check + Edit to delete |
safe_delete_symbol |
safe_delete_symbol(name_path_pattern="old_handler", relative_path="src/handlers.py") |
Separate Edit calls file by file across the project -- repeating an identical Edit across many files for one textual change |
replace_in_files (dry run first) |
replace_in_files(needle="old_key", repl="new_key", mode="literal", dry_run=True), review the diff, then apply with expected_count |
This table covers any Grep/Search against .py/.ts/.js/other code files inside the project Serena serves, performed to locate a symbol (definition or usage), whether or not the pattern is the literal symbol name: Serena is semantic -- it finds aliases and renamed imports -- and is faster.
Exceptions -- proceed with Search/Grep: searching inside comments or string literals (not symbol names), and searching code outside the project Serena serves (another checkout, a dependency cache, a system directory), where Serena cannot resolve symbols at all.
</prohibition> <tool_selection_rules>Tool Selection Decision Tree
Step 1: Load the Serena Tool Schemas
Before any code navigation task, look for mcp__serena__find_symbol in your tools list. Claude Code may list MCP tools only by name, as deferred tools whose parameter schemas are not loaded yet, and calling a deferred tool before its schema is loaded fails. When the Serena tools appear only as deferred names, load all of them at once before the first Serena call -- for example ToolSearch(query="+serena", max_results=20) -- and take every parameter name from the loaded schemas. If find_symbol is not listed, even as a deferred name, use built-in tools as the fallback: either no Serena tool is listed at all, or Serena lists serena_repl in its place, which means Serena runs its REPL interface -- typically because the project's own Serena configuration selects it -- and exposes none of the symbol tools this protocol covers.
Step 2: Classify Your Task
| If Your Task Is... | Use |
|---|---|
| Find where a symbol is defined (function/class/method/variable) | find_symbol(name_path_pattern="NAME", include_body=True) |
| Find what one specific occurrence refers to (go to definition from a usage site) | find_declaration(relative_path="FILE_WITH_THE_USAGE", regex=r"obj\.(process)\(") |
| Find implementations of an abstract method/interface (Java/TS/Go/C#/Rust) -- not Python on the default Pyright backend (see Known Limitations) | find_implementations(name_path="Interface/method", relative_path="FILE_DEFINING_IT") |
| Find all usages/calls of a symbol | find_referencing_symbols(name_path="NAME", relative_path="FILE_DEFINING_IT") |
| Understand a file's structure (functions/classes outline) | get_symbols_overview(relative_path="FILE") |
| Get LSP diagnostics (errors/warnings) for a file | get_diagnostics_for_file(relative_path="FILE") |
| Get LSP diagnostics scoped to a specific symbol (opt-in upstream; already enabled -- see Known Limitations) | get_diagnostics_for_symbol(name_path="NAME", reference_file="FILE_DEFINING_IT") |
| Rename a symbol across the codebase | rename_symbol(name_path="NAME", relative_path="FILE_DEFINING_IT", new_name="NEW_NAME") |
| Replace a function's implementation | replace_symbol_body(name_path="NAME", relative_path="FILE_DEFINING_IT", body="...") |
| Insert code after a symbol | insert_after_symbol(name_path="NAME", relative_path="FILE_DEFINING_IT", body="...") |
| Insert code before a symbol | insert_before_symbol(name_path="NAME", relative_path="FILE_DEFINING_IT", body="...") |
| Safely delete a symbol (with reference check) | safe_delete_symbol(name_path_pattern="NAME", relative_path="FILE_DEFINING_IT") |
| Apply the same textual change across many files (non-symbol text) | replace_in_files(needle="OLD", repl="NEW", mode="literal", dry_run=True) |
| A language server hangs, stops responding, or answers from a stale index | restart_language_server() yourself, without asking (see Error Handling) |
Step 3: When Built-in Tools Are the Right Choice
- Searching text in comments or strings (not symbol names)
- Finding files by name pattern (Glob)
- Editing at known line numbers (when you already have exact lines)
- Reading non-code files (YAML, JSON, Markdown, configs)
- Code outside the project Serena serves (another checkout, a dependency cache, a system directory)
Name Paths and Relative Paths
Which project. Serena serves exactly one project, detected when its server starts: the nearest directory at or above the directory Claude Code was launched in that holds .serena/project.yml or .git. A separate checkout -- including a git worktree nested inside that project, which carries its own .git file -- is a different project that Serena does not see, and when no such directory exists Serena serves no project at all.
relative_path is always relative to that project root, written like src/module.py, never as an absolute path. find_symbol accepts a file or a directory (or no relative_path, to search the whole project). The tools that act on one known symbol -- find_referencing_symbols, find_implementations, rename_symbol, replace_symbol_body, insert_after_symbol, insert_before_symbol, and safe_delete_symbol -- take the file that defines the symbol, not a directory. find_declaration takes the file that holds the usage being resolved.
A name path addresses a symbol inside a file the way a filesystem path addresses a file:
processmatches every symbol namedprocess.MyClass/processmatches any symbol whose name path ends inMyClass/process, such as theprocessmethod ofMyClass./MyClass/processis absolute: it matches onlyprocessdirectly inside a top-levelMyClass.MyClass/process[1]selects one overload by its 0-based index, in languages that allow overloading.
find_symbol refines a lookup with depth=1 (also return children, such as a class's methods), substring_matching=True (match the last segment as a substring), max_matches=1 (require a unique match), and include_kinds/exclude_kinds (filter by LSP symbol kind). Every result carries the symbol's exact name_path and relative_path; reuse both verbatim in follow-up calls.
Complete Serena Tool Reference
The Serena tools granted by this deployment, grouped by category.
Read-only (navigation and inspection)
| Tool | Purpose | Canonical Usage |
|---|---|---|
find_symbol |
Find where a symbol (function/class/method/variable) is defined | find_symbol(name_path_pattern="my_function", include_body=True) |
find_declaration |
Resolve one occurrence of a symbol to its definition (LSP go-to-definition from a usage site) | find_declaration(relative_path="src/app.py", regex=r"obj\.(process)\(") |
find_implementations |
Find concrete implementations of an abstract method/interface/protocol | find_implementations(name_path="Runnable/run", relative_path="src/runnable.ts") |
find_referencing_symbols |
Find all places where a symbol is used (calls, imports, references) | find_referencing_symbols(name_path="my_function", relative_path="src/module.py") |
get_symbols_overview |
Outline the top-level symbols of a file (depth=1 adds their children) |
get_symbols_overview(relative_path="src/module.py") |
get_diagnostics_for_file |
Retrieve LSP diagnostics (errors, warnings, hints) for a file, grouped by symbol | get_diagnostics_for_file(relative_path="src/module.py", min_severity=2) |
get_diagnostics_for_symbol |
Retrieve LSP diagnostics scoped to one symbol (and optionally the symbols referencing it) | get_diagnostics_for_symbol(name_path="fn", reference_file="src/module.py", check_symbol_references=True) |
Key notes (Read-only):
find_symbol: ALWAYS setinclude_body=Truewhen you need the implementation (avoids a second query). Without it you get the name path, kind, and location only.find_declaration: Not a name lookup. Theregexhas exactly one capture group around the symbol at a usage site inrelative_path-- for exampler"obj\.(process)\("for the callobj.process(...)-- and the tool returns the symbol that occurrence resolves to. Give the regex enough surrounding context to match exactly one place in the file (or narrow it withcontaining_symbol_name_path). Use it when a name alone is ambiguous: an overloaded or common method name, an aliased import, or a call through an object. To look a symbol up by name, usefind_symbol.find_implementations: Supported for Java, TypeScript, Go, C#, Rust. Not supported for Python on Serena's default Pyright backend (see Known Limitations -- LSP-32601). Usefind_referencing_symbols+code-review-graphinheritors_ofas the Python workaround.find_referencing_symbols: High precision, critically low recall for dynamic imports / runtimesys.path/ attribute chains (see Known Limitations). ALWAYS cross-validate with Grep when completeness matters.get_diagnostics_for_file:min_severitysets the least severe level returned -- 1 for errors only, 2 for errors and warnings, 4 (the default) for everything down to hints;start_line/end_line(0-based) narrow the range.get_diagnostics_for_symbol: Opt-in upstream -- already enabled by this deployment's Serena context. See Known Limitations.
Mutation (editing)
| Tool | Purpose | Canonical Usage |
|---|---|---|
rename_symbol |
Rename a symbol across the entire codebase (atomic, updates refs) | rename_symbol(name_path="old_func", relative_path="src/module.py", new_name="new_func") |
replace_symbol_body |
Replace the whole definition of a function/method/class (symbol-boundary-aware) | replace_symbol_body(name_path="fn", relative_path="src/module.py", body="def fn():\n return 42") |
insert_after_symbol |
Insert new code immediately after an existing class/function/method definition | insert_after_symbol(name_path="existing", relative_path="src/module.py", body="def new():\n pass") |
insert_before_symbol |
Insert new code immediately before an existing symbol (also: a new import before the first symbol) | insert_before_symbol(name_path="existing", relative_path="src/module.py", body="...") |
safe_delete_symbol |
Reference-check-then-delete; aborts and returns refs if any exist | safe_delete_symbol(name_path_pattern="MyClass/old", relative_path="src/calc.py") |
replace_in_files |
Apply one identical literal/regex text replacement across many files | replace_in_files(needle="old_key", repl="new_key", mode="literal", dry_run=True) |
Key notes (Mutation):
rename_symbol: Atomic; updates all references correctly. Preferred over multiple Edit calls, which are error-prone and miss references.replace_symbol_body:bodyis the complete new definition, signature line included (whether a preceding docstring or decorator belongs to it depends on the language). Retrieve the current definition withfind_symbol(..., include_body=True)first, so you know exactly what the body spans.insert_after_symbol/insert_before_symbol: Symbol-aware positioning that survives line-number changes -- no need to compute line numbers. Anchorinsert_after_symbolon a class, function, or method definition, never on an assignment such as a constant or field.- Code you have seen only through Serena counts as unread for the built-in Edit tool, which refuses to edit a file that was not read with Read; edit such code with the Serena editing tools, or Read the file before using Edit.
safe_delete_symbol: Atomic check-then-delete; returns success only when no references exist. If references are found, returns their file/line locations so you can handle them first. Inherits thefind_referencing_symbolslow-recall limitation (see Known Limitations). Cross-validate with Grep before calling when dynamic imports may exist.replace_in_files: Text-based, not symbol-aware -- correct only for applying one identical textual change across many files in a single atomic call (a renamed config key, an import path fragment, a string constant). ALWAYS run withdry_run=Truefirst and review the returned per-occurrence diff; then apply the reviewed subset viaoccurrence_ids, or setexpected_countso the apply aborts if the match count changed. Inmode="regex", refer to capture groups inreplas$!1,$!2. For symbol renames userename_symbol, for one symbol's implementation usereplace_symbol_body, and for a single-file edit the built-in Edit tool remains correct. Scope withrelative_pathandpaths_include_glob/paths_exclude_globrather than defaulting to the whole project.
Admin
| Tool | Purpose |
|---|---|
restart_language_server |
Restart the language servers when one hangs, stops responding, or answers from a stale index; call it yourself, without asking the user |
Restarting touches the language servers and nothing else. It changes no source or configuration file, so it needs no confirmation: call it yourself whenever a language server hangs, stops responding, answers from a stale index, or keeps failing. It rebuilds the servers from the project configuration Serena loaded when its MCP server started, and it re-reads neither that configuration nor Serena's context, so an edited .serena/project.yml or a changed context takes effect only after the Serena MCP server itself restarts, which means restarting Claude Code. Serena's default description of this tool says to restart only on the user's request or after confirmation. This deployment's Serena context replaces that wording; if the description you see still carries it, the installed context is out of date, and you still restart without asking.
Known Limitations
Three Serena tools carry limitations that change how you must use them: find_referencing_symbols has a recall gap on certain import patterns, find_implementations does not work for Python on the default backend, and get_diagnostics_for_symbol depends on a deployment-level opt-in. Read known-limitations.md in this skill's own directory before relying on any of the three in a way where being wrong has a cost -- dead-code conclusions, deletions, cross-language implementation lookups, or diagnostics troubleshooting.
find_referencing_symbolshas low recall for dynamic imports, runtimesys.pathmanipulation, and attribute chains (0-10% recall in the worst cases) -- cross-validate with Grep whenever completeness matters, and never conclude "zero callers" from it alone;safe_delete_symbolinherits this same gap.find_implementationsreturns LSP error-32601for Python on Serena's default Pyright backend (Pyright does not advertise the capability) -- this is protocol-correct, not a defect; usefind_referencing_symbolsor thecode-review-graphinheritors_ofquery pattern instead.get_diagnostics_for_symbolis opt-in upstream; this deployment's Serena context already opts it in underincluded_optional_tools, so a "tool not found" error means the installed context is stale, not that the opt-in is missing -- re-run the environment setup and restart Claude Code.
Error Handling
If Serena Tool Fails
- Retry once -- the language server may need a moment, for example right after files changed on disk.
- Check the call against the loaded schema -- a wrong parameter name, an absolute path, or a directory where a file is required fails regardless of the language server's state.
- If the language server hangs, keeps failing, or keeps answering from a stale index, call
restart_language_serveryourself, without asking the user, then retry the call. The exception is an error saying no language servers are available: a restart cannot fix it, so follow "If Serena Reports No Language Servers" below instead. - Document the failure in your response.
- Fall back to built-in tools only after documenting the Serena failure.
If Serena Reports No Language Servers
No language servers available in the manager, or a Cannot extract symbols from file ... error ending in Active language servers: [], means Serena never started a language server for this project, because its project configuration lists none. The language server is not down, and restarting it cannot help: restart_language_server rebuilds from the configuration Serena loaded when it started, which is still empty. The empty list typically comes from Serena itself: it writes language_servers: [] when it creates .serena/project.yml before the repository holds any source file it recognizes, and it never re-detects languages once that file exists.
- Set the language servers. In the
.serena/project.ymlat the root of the project Serena serves, setlanguage_serversto the ids of the repository's languages, for example[python]; the comments above that key list the valid ids. When.serena/is gitignored, the edit changes nothing tracked; when the file is tracked, the edit is a repository change like any other. - Tell the user that Claude Code needs a restart. Serena reads
.serena/project.ymlonly when its MCP server starts, so the language servers come up after Claude Code restarts, not in the current session. - Use the built-in tools until then, and report that the project had no language server configured -- not that the language server is down.
If Serena Tools Are Not Available
- Rule out deferred tools first -- tools listed only by name are available once loaded (see Step 1); they are not missing.
- Note unavailability -- "Serena tools not available, using built-in alternatives".
- Use built-in tools as documented under "Step 3: When Built-in Tools Are the Right Choice".
- No protocol violation -- falling back when tools are genuinely unavailable is correct.