Stateful MCP server over real language servers. 50 tools, 30 CI-verified languages, 20 agent workflows. Persistent sessions, speculative execution, single Go binary.
Cross-repository analysis — find all callers of a library symbol in one or more consumer repos. Use when refactoring a shared library and need to understand how consumers use it.
> Requires the agent-lsp MCP server.
# lsp-cross-repo
Multi-root cross-repo caller analysis for library + consumer workflows. Finds all
usages of a library symbol across one or more consumer codebases in a single call.
Read-only — does not modify any files.
## When to use
- Before changing a library API: find all callers in every consumer
- Before deleting a symbol: verify it has no cross-repo dependents
- When a change in repo A might break repo B or C
- Auditing how internal packages are used across services
Use `/lsp-impact` instead for single-repo blast-radius analysis.
## Workflow
### Step 1 — Initialize the primary workspace
Start the language server on the library root if not already running:
```
mcp__lsp__start_lsp({ "root_dir": "/path/to/library" })
```
### Step 2 — Locate the library symbol
Find the symbol's definition to get `file_path`, `line`, and `column`:
```
mcp__lsp__get_workspace_symbols({ "query": "<symbol-name>" })
```
Pick the result in the library repo (not a test file).
### Step 3 — Find all cross-repo references (primary step)
Call `get_cross_repo_references` with the symbol location and all consumer repo
roots. This adds each consumer as a workspace folder, waits for indexing, runs
`get_references` across all roots, and returns results partitioned by repo:
```
mcp__lsp__get_cross_repo_references({
"symbol_file": "/abs/path/to/library/file.go",
"line": <line>,
"column": <column>,
"consumer_roots": [
"/abs/path/to/consumer-a",
"/abs/path/to/consumer-b"
]
})
```
Returns:
- `library_references` — usages within the library itself
- `consumer_references` — a map of `consumer-root → [file:line ...]`
- `warnings` — any roots that could not be indexed (check these manually)
**Decision after Step 3:**
| Result | Action |
|--------|--------|
| No consumer refs | Safe to change — verify `warnings` is empty first |
| Consumer refs found | Run `/lsp-impact` on each call site before editing |
| `warnings` non-empty | Re-add that root manually and retry Step 3 |
### Step 4 — Callers and implementations (optional)
For a deeper look at how consumers call the symbol:
```
mcp__lsp__call_hierarchy({
"file_path": "<library-file>",
"line": <line>,
"column": <column>,
"direction": "incoming"
})
```
For interfaces — all consumer-side implementations:
```
mcp__lsp__go_to_implementation({
"file_path": "<library-file>",
"line": <line>,
"column": <column>
})
```
## Output format
```
## Library-internal references
- file:line — brief context
## Consumer references
### /path/to/consumer-a
- file:line — brief context
### /path/to/consumer-b
- file:line — brief context
```
## Decision guide
| Situation | Action |
|-----------|--------|
| No consumer refs, warnings empty | Safe to change |
| Consumer refs found | Run `/lsp-impact` on each call site before editing |
| `warnings` lists a consumer root | That root failed indexing — check LSP logs |
| Consumer uses interface, not concrete type | Use `go_to_implementation` to find all implementors |
## Example
```
# Refactoring ParseConfig in a shared config library used by 3 services
start_lsp(root_dir="/repos/config-lib")
get_workspace_symbols(query="ParseConfig") # find definition → file:42:6
get_cross_repo_references(
symbol_file="/repos/config-lib/pkg/config/parser.go",
line=42, column=6,
consumer_roots=["/repos/api-service", "/repos/worker-service", "/repos/batch-job"]
)
# → library_references: 2
# → consumer_references: {api-service: [main.go:14, app.go:31], worker-service: [runner.go:8]}
# → warnings: []
```Enumerate exported symbols in a file and surface those with zero references across the workspace. Use when auditing for dead code, cleaning up APIs, or checking which exports are safe to remove.
> Requires the agent-lsp MCP server.
# lsp-dead-code
Audit an exported symbol list for zero-reference candidates. Calls
`get_document_symbols` to enumerate symbols, then checks each exported
symbol with `get_references` to find callers. Produces a classified report.
## When to Use
Use this skill when you want to identify dead code in a file — exported
symbols that are defined but never called anywhere in the workspace. Common
use cases:
- Cleaning up APIs before a release
- Identifying legacy exports that can be safely removed
- Auditing a package for unused public surface area
**Important:** This skill surfaces *candidates*. Always review results
manually before deleting anything. See the Caveats section below.
## What counts as "exported"
| Language | Exported means... |
|------------|------------------------------------------------------------------------|
| Go | Identifier starts with an uppercase letter (e.g. `MyFunc`, `MyType`) |
| TypeScript | Has `export` keyword; or is a public class member (no `private`) |
| Python | Not prefixed with `_`; or explicitly listed in `__all__` |
| Java/C# | Has `public` or `protected` visibility modifier |
| Rust | Has `pub` keyword |
## Prerequisites
If LSP is not yet initialized, call `mcp__lsp__start_lsp` with the
workspace root first:
```
mcp__lsp__start_lsp({ "root_dir": "/your/workspace" })
```
agent-lsp supports auto-inference from file paths, so explicit start is
only required when switching workspaces or on a cold session.
## Step 0 — Verify indexing is complete (mandatory)
**Do not skip this step.** An under-indexed workspace returns `[]` for
symbols that ARE referenced, producing false dead-code candidates.
Pick one symbol you know is actively used (e.g. the primary constructor,
a widely-called utility function). Call `get_references` on it:
```
mcp__lsp__get_references({
"file_path": "/abs/path/to/file.go",
"line": <known-active symbol line>,
"column": <known-active symbol column>,
"include_declaration": false
})
```
**If this returns `[]`:** the workspace is not indexed. Wait 3–5 seconds
and retry. Do not proceed until a known-active symbol returns ≥1 reference.
If it never returns results after 15 seconds, restart the LSP server with
`mcp__lsp__restart_lsp_server` and re-open the target file.
## Step 1 — Open the file and enumerate symbols
Open the file so the language server tracks it, then fetch all symbols:
```
mcp__lsp__open_document({ "file_path": "/abs/path/to/file.go" })
mcp__lsp__get_document_symbols({ "file_path": "/abs/path/to/file.go" })
```
Collect the full symbol list. Filter to **exported symbols only** using the
language-appropriate rule from the table above.
**Coordinate note:** `get_document_symbols` returns 1-based coordinates.
Pass `selectionRange.start.line` and `selectionRange.start.character`
directly to `get_references` — no conversion needed.
**"no identifier found" error:** This means the column points to whitespace
or a keyword rather than the identifier name. This happens with methods
whose receiver prefix shifts the name rightward (e.g. `func (c *Client) MethodName`
— the name starts at column 21, not column 1). Fix: grep the declaration
line for the symbol name to find its exact column:
```
grep -n "MethodName" file.go
# count characters to find the 1-based column of the name
```
Then retry `get_references` with the corrected column.
## Step 2 — Check references for each exported symbol
For each exported symbol, call `get_references` with
`include_declaration: false` so the definition site itself is excluded
from the count. A count of 0 means no callers, not no occurrences.
```
mcp__lsp__get_references({
"file_path": "/abs/path/to/file.go",
"line": <selectionRange.start.line>,
"column": <selectionRange.start.character>,
"include_declaration": false
})
```
Record the result for each symbol:
```
{ symbol_name, kind, line, reference_count, locations[] }
```
**Batching note:** For files with many exported symbols (>20), process in
batches of 5–10 to avoid overwhelming the LSP server.
**Zero-reference cross-check (required before classifying as dead):**
When `get_references` returns `[]` for a symbol that looks foundational
(a handler, a constructor, a type used as a field), do not trust LSP alone.
LSP can miss references made through value-passing, interface satisfaction,
or function registration patterns (e.g. `server.AddResource(HandleFoo)`).
Before classifying as dead, run a text search in the primary wiring files:
```
grep -r "SymbolName" main.go server.go cmd/ internal/
```
If grep finds the name in a registration or assignment context, the symbol
is active — LSP just couldn't resolve the indirect reference. Update your
classification accordingly.
## Step 3 — Classify and report
Classify each exported symbol by reference count:
- **Zero references (LSP + grep)** — confirmed dead candidate. Flag with WARNING.
- **Zero LSP, found by grep** — active via registration/value pattern. Mark as ACTIVE.
- **1–2 references** — review manually. May be test-only usage.
- **3+ references** — active symbol. Not dead code.
For test-only references: if all locations are in `_test.go` files (Go) or
files named `*.test.*` / `*.spec.*`, mark the symbol as "test-only" in
the report rather than "zero-reference".
Produce the Dead Code Report using the format in
[references/patterns.md](references/patterns.md).
## Caveats
The following cases produce zero LSP references even though the symbol IS
used at runtime. Do not delete any zero-reference candidate without
manual review:
1. **Incomplete indexing.** `get_references` only searches files open or
indexed by the language server. If the workspace is partially indexed,
results may be incomplete. The Step 0 warm-up check catches this.
2. **Registration patterns.** Symbols passed as values to registration
functions (e.g. `server.AddTool(HandleFoo)`, `http.HandleFunc("/", handler)`)
appear as zero LSP references from the *definition site* because gopls
tracks the call to the registrar, not the handler name. Always grep
wiring files for zero-reference handlers before classifying as dead.
3. **Reflection and dynamic dispatch.** Symbols used via reflection
(`reflect.TypeOf` in Go, `Class.forName` in Java) or dynamic dispatch
have no static call sites visible to the LSP.
4. **`//go:linkname` and assembly.** Go symbols linked via `//go:linkname`
or referenced from assembly files will show zero LSP references.
5. **Library public API.** Exported symbols called from external packages
not present in the workspace will show zero references even if
consumers exist.
6. **Declaration excluded from count.** The definition site is not counted
(`include_declaration: false`). A count of 0 means no callers found,
not that the symbol never appears in the source tree.
7. **Always review before deleting.** Zero LSP references is a signal to
investigate, not a guarantee the symbol is unused.
## Step 4 — Next steps
After generating the report:
- **For each zero-reference symbol (confirmed by grep):** Run `lsp-impact`
on the symbol to confirm. If `lsp-impact` also finds zero references, it
is safe to consider for removal. Still check the Caveats section above.
- **For symbols with only test-file references:** Mark as "test-only" in
the report. These may be candidates for removal if the tests themselves
are redundant, but should not be deleted without reviewing whether the
tests serve a documentation or contract purpose.
- **For symbols with 1–2 references in production code:** These are likely
active but lightly used. Do not remove without checking whether they are
part of a committed public API.Three-tier documentation lookup for any symbol — hover → offline toolchain doc → source definition. Use when hover text is absent, insufficient, or the symbol is in an unindexed dependency.
> Requires the agent-lsp MCP server.
# lsp-docs
Three-tier documentation lookup for any symbol. Works when the language server
is unavailable, when hover returns empty results, or when the symbol lives in a
transitive dependency that gopls or pyright does not index.
Read-only — does not modify any files.
**Invocation:** User provides `symbol_name` in fully-qualified form (e.g.
`"fmt.Println"`, `"std::vec::Vec::new"`, `"os.path.join"`). Optionally provide
`file_path` for any file in the same module, which improves Go package resolution.
---
## Decision table
| Situation | Recommended tier |
|-----------|-----------------|
| Symbol in current workspace | Tier 1 (hover) |
| Symbol in direct dependency | Tier 2 (toolchain doc) |
| Symbol in transitive dep (not indexed by LSP) | Tier 2 |
| No LSP server available | Tier 2 → Tier 3 |
| No toolchain installed (e.g., Rust without cargo) | Tier 3 |
---
## Tier 1 — LSP hover (fast, live, position-based)
Call `get_info_on_location` with the file path and cursor position (1-based).
```
mcp__lsp__get_info_on_location({
"file_path": "/abs/path/to/file.go",
"line": 42,
"column": 8
})
```
If the result contains a non-empty `contents` field with useful type and doc
information, **stop here and return it**. Hover is the fastest path and should
always be tried first.
If hover returns empty `contents`, or the language server is not initialized,
proceed to Tier 2.
---
## Tier 2 — Offline toolchain documentation (authoritative, name-based)
Call `get_symbol_documentation` with the fully-qualified symbol name and
`language_id`. This fetches documentation from the local toolchain (go doc,
pydoc, cargo doc) without requiring an LSP session. Works for transitive
dependencies that the language server does not index.
```
mcp__lsp__get_symbol_documentation({
"symbol": "fmt.Println",
"language_id": "go",
"file_path": "/abs/path/to/any/file/in/the/module.go", // optional, improves Go pkg resolution
"format": "markdown" // optional: wraps signature in code fence
})
```
**Interpreting the result:**
- If `source == "toolchain"`: return the `doc` and `signature` fields to the
user. These are authoritative — sourced directly from the installed toolchain,
ANSI-stripped, and ready for display.
- If `source == "error"`: note the `error` field (toolchain failure reason) and
proceed to Tier 3.
---
## Tier 3 — Source definition (last resort)
Call `go_to_definition` to navigate to the symbol definition, then call
`get_symbol_source` to extract the source text. This always works when the
symbol exists in the workspace or module cache, even without a language server.
```
mcp__lsp__go_to_definition({
"file_path": "/abs/path/to/caller.go",
"line": 42,
"column": 8
})
// → returns definition location
mcp__lsp__get_symbol_source({
"file_path": "<definition file from above>",
"line": <definition line from above>
})
// → returns full function/type source text
```
Present the source text to the user with a note that it is raw source, not
rendered documentation.
---
## lsp-impact integration note
Before running `lsp-impact` on an unfamiliar symbol, call
`get_symbol_documentation` to understand its signature and semantics. This
prevents misinterpreting the impact report due to incorrect assumptions about
what the symbol does.
---
## Example
```
Goal: look up documentation for http.ListenAndServe in a Go project
Tier 1 — get_info_on_location: cursor on "ListenAndServe" in main.go:14:6
→ contents: "" (empty — server not initialized)
Proceed to Tier 2.
Tier 2 — get_symbol_documentation:
symbol: "net/http.ListenAndServe"
language_id: "go"
file_path: "/Users/you/code/myapp/main.go"
format: "markdown"
Result:
{
"symbol": "net/http.ListenAndServe",
"language": "go",
"source": "toolchain",
"doc": "func ListenAndServe(addr string, handler http.Handler) error\n\nListenAndServe listens on the TCP network address addr and then calls Serve...",
"signature": "func ListenAndServe(addr string, handler http.Handler) error",
"error": ""
}
source == "toolchain" → return doc and signature to user. Done.
Tier 3 — skipped (Tier 2 succeeded)
```Safe workflow for editing exported symbols or public APIs. Use when changing a function signature, modifying a public type, or altering any symbol used outside its own package — finds all callers first so nothing breaks silently.
> Requires the agent-lsp MCP server.
# lsp-edit-export
Safe workflow for editing exported symbols. Always discovers all callers before
touching any code, then verifies the change is clean.
## When to Use
Use this skill whenever you intend to change the signature, name, or behavior of
an **exported symbol**: a symbol visible outside its defining package or module.
**Language-specific definitions:**
| Language | Exported means... |
|------------|------------------------------------------------------------------------|
| Go | Identifier starts with an uppercase letter (e.g. `MyFunc`, `MyType`) |
| TypeScript | Has `export` keyword; or is a public class member (no `private`) |
| Python | Not prefixed with `_`; or explicitly listed in `__all__` |
| Java/C# | Has `public` or `protected` visibility modifier |
| Rust | Has `pub` keyword |
If you are unsure whether a symbol is exported, treat it as exported and run
this workflow anyway. The cost is a few extra tool calls; the benefit is never
breaking a hidden caller.
**Do NOT skip this workflow** even when you believe there are zero callers.
The confirmation gate in step 3 exists precisely for that case.
## Workflow
**If LSP is not yet initialized**, call `mcp__lsp__start_lsp` with the
workspace root first. (agent-lsp supports auto-inference from file paths, so
explicit start is only required when switching workspaces or on a cold session.)
### Step 1 — Locate the symbol
Use `go_to_symbol` to find the symbol's definition by name, without needing to
know its file path or line number in advance:
```
mcp__lsp__go_to_symbol({
"symbol_path": "PackageName.ExportedFunction",
"workspace_root": "/abs/path/to/repo" // optional, narrows scope
})
```
`symbol_path` uses dot notation. For a top-level function `Encode` in package
`codec`, use `"codec.Encode"`. For a method `Reset` on type `Buffer`, use
`"Buffer.Reset"`. The last component is the leaf name; any prefix is used to
disambiguate when multiple symbols share the same leaf.
The tool returns a `FormattedLocation` with the definition file and 1-indexed
line/column. Record this position — you will need it in step 2.
### Step 2 — Discover all callers
Call `get_references` using the `position_pattern` field to express the cursor
position as a readable text pattern rather than raw coordinates. The `@@` marker
indicates exactly where the cursor sits (the character immediately after `@@`):
```
mcp__lsp__get_references({
"file_path": "<definition file from step 1>",
"position_pattern": "func @@ExportedFunction(",
"include_declaration": false
})
```
The `@@` must immediately precede the first character of the symbol name.
Examples:
- `"func @@Encode("` — Go function declaration
- `"type @@Buffer struct"` — Go type declaration
- `"export function @@parse("` — TypeScript function
- `"class @@Parser:"` — Python class
- `"pub fn @@process("` — Rust function
If `position_pattern` is unavailable on your MCP client, fall back to the
`line` and `column` fields from the location returned in step 1.
The tool returns a list of reference locations across the codebase.
### Step 3 — Confirmation gate (REQUIRED — never skip)
Before making any change, present the impact summary to the user and ask for
explicit confirmation. This gate is mandatory even when the caller count is zero.
Format the gate as follows:
```
## Impact Check: <SymbolName>
- Definition: <file>:<line>
- Callers found: N reference(s) in M file(s)
Files with callers:
- <file1>
- <file2>
...
Proceed with the edit? [y/n]
```
If the user answers **n**, stop. Do not make any edits.
If the user answers **y**, proceed to step 4.
**Why this gate exists even for 0 callers:** the LSP index may be incomplete
(e.g. files not yet saved, workspace not fully loaded). Zero callers is a data
point, not a guarantee.
### Step 4 — Make the edit
Apply your intended change using `Edit` or `Write`. Follow the standard edit
workflow for the language. If renaming, update all call sites identified in
step 2 as well — do not leave broken callers.
Collect diagnostics **before** the edit so you have a baseline for comparison
in step 5:
```
mcp__lsp__get_diagnostics({
"file_path": "<definition file>"
})
```
Then apply the edit and collect diagnostics again after.
### Step 5 — Check diagnostics
Compare before and after diagnostic snapshots using the format in
[references/patterns.md](references/patterns.md).
If new errors appear, fix them before proceeding. Do not run the build with
known diagnostic errors outstanding.
### Step 6 — Run the build
```
mcp__lsp__run_build({
"workspace_root": "/abs/path/to/repo"
})
```
A clean build confirms no compilation errors across all affected packages.
If the build fails, diagnose using the error output and diagnostic data from
step 5. Fix and re-run until the build passes.
### Step 7 — Report
Emit the final output block:
```
## Edit Summary
- Symbol: <name> (<kind>)
- Callers found: N in M files
- Diagnostics: net +N/-N
- Build: PASSED / FAILED
```
If build is FAILED, include the first 3–5 error lines and a brief diagnosis.
## Example
```
Goal: rename exported function `ParseConfig` → `LoadConfig` in pkg/config
Step 1 — go_to_symbol: symbol_path="config.ParseConfig"
→ pkg/config/parser.go:42:6
Step 2 — get_references: position_pattern="func @@ParseConfig("
→ 7 references in 4 files
Step 3 — gate:
## Impact Check: ParseConfig
- Definition: pkg/config/parser.go:42
- Callers found: 7 in 4 files
Files: cmd/main.go, internal/app.go, internal/loader.go, pkg/config/parser_test.go
Proceed? [y/n] → y
Step 4 — edit: rename declaration + all 7 call sites
Step 5 — diagnostics: net 0 (no new errors)
Step 6 — build: PASSED
Step 7 — report:
## Edit Summary
- Symbol: LoadConfig (function)
- Callers found: 7 in 4 files
- Diagnostics: net 0
- Build: PASSED
```
## Note on position_pattern
`position_pattern` with `@@` is a agent-lsp extension. If your MCP client
or server does not support it, fall back to explicit `line` and `column`
parameters from the location returned by `go_to_symbol` in step 1.Edit a named symbol without knowing its file or position. Use when you want to change a function, type, or variable by name and don't have exact coordinates. Resolves the symbol to its definition, retrieves its full range, and applies the edit.
# lsp-edit-symbol
Edit a named symbol (function, type, variable) without needing its exact file path
or line/column. Composes `go_to_symbol` → `get_document_symbols` → `apply_edit`.
## Workflow
### Step 1 — Locate the symbol
```json
{ "tool": "get_workspace_symbols", "query": "MyFunc" }
```
Returns a list of matching symbols with file URI and position. Pick the definition
(not a test file, not a stub). If multiple matches, use the container name or file
path to disambiguate.
### Step 2 — Get the full range
```json
{
"tool": "get_document_symbols",
"file_path": "/path/to/file.go",
"language_id": "go"
}
```
Find `MyFunc` in the returned tree. The `range` field covers the entire symbol
including its body; `selectionRange` covers only the name. Use `range` when
replacing the full definition, `selectionRange` when renaming only.
### Step 3 — Apply the edit
**Option A — text-match (recommended when you have the old text):**
```json
{
"tool": "apply_edit",
"file_path": "/path/to/file.go",
"old_text": "func MyFunc() {",
"new_text": "func MyFunc() error {"
}
```
No position needed. Tolerates indentation differences.
**Option B — positional (when you have the exact range from Step 2):**
```json
{
"tool": "apply_edit",
"workspace_edit": {
"changes": {
"file:///path/to/file.go": [{
"range": { "start": {"line": 12, "character": 0}, "end": {"line": 18, "character": 1} },
"newText": "func MyFunc() error {\n\treturn nil\n}"
}]
}
}
}
```
## Decision guide
| Situation | Approach |
|-----------|----------|
| Changing signature only | Step 1 → Step 3A with one-line old_text |
| Replacing full body | Step 1 → Step 2 → Step 3B with full range |
| Symbol name ambiguous | Use `get_workspace_symbols` query + container name filter |
| After edit | Run `get_diagnostics` to verify no errors introduced |
## Notes
- `get_workspace_symbols` returns declaration sites, not all references. The
first non-test result is usually the definition.
- Positions in `get_document_symbols` are **1-based** (shifted from LSP convention).
`apply_edit` `workspace_edit` expects **0-based** — subtract 1 when using positional
mode (Option B). Text-match mode (Option A) requires no position math.
- For renames (not edits), use `/lsp-rename` instead — it updates all call sites.Tell me about this symbol": hover + implementations + call hierarchy + references in one pass — for navigating unfamiliar code.
> Requires the agent-lsp MCP server.
# lsp-explore
"Tell me about this symbol" — hover, implementations, call hierarchy, and
references in a single pass. Use when navigating unfamiliar code: you get
type info, doc comments, who calls it, what implements it, and every
reference site without issuing four separate commands.
Read-only — does not modify any files.
**Invocation:** User provides a symbol name in dot notation (e.g.
`"codec.Encode"`, `"Buffer.Reset"`). Optionally provide `workspace_root`
to scope the search.
---
## Prerequisites
If LSP is not yet initialized, call `mcp__lsp__start_lsp` with the workspace
root first. Auto-inference applies when file paths are provided.
---
## Phase 1 — Locate the symbol
Call `mcp__lsp__go_to_symbol` with `symbol_path` set to the user-provided name:
```
mcp__lsp__go_to_symbol({
"symbol_path": "Package.SymbolName", // dot notation; e.g. "codec.Encode"
"workspace_root": "<root>" // optional
})
→ returns: file, line, column (1-indexed)
```
Record the returned `file`, `line`, and `column`. If `go_to_symbol` returns
nothing, report:
> Symbol not found: `<name>`
> Check the dot-notation path (e.g. "Package.Symbol") and ensure the workspace
> root covers the file.
Stop immediately — do not proceed to Phase 2.
Then open the file so the language server has it in view:
```
mcp__lsp__open_document({
"file_path": "<file from go_to_symbol>"
})
```
---
## Phase 2 — Hover (always available)
Call `mcp__lsp__get_info_on_location` at the definition location:
```
mcp__lsp__get_info_on_location({
"file_path": "<file from Phase 1>",
"line": <line from Phase 1>,
"column": <column from Phase 1>
})
```
Store the result as `hover_text`. If the call fails or returns nothing, set
`hover_text` to an empty string. Do not stop.
---
## Phase 3 — Implementations (capability-gated)
Call `mcp__lsp__get_server_capabilities` to see what the server supports:
```
mcp__lsp__get_server_capabilities()
→ returns: supported_tools list
```
If `go_to_implementation` appears in `supported_tools`, call it:
```
mcp__lsp__go_to_implementation({
"file_path": "<file from Phase 1>",
"line": <line from Phase 1>,
"column": <column from Phase 1>
})
→ returns: list of implementation locations (file, line)
```
Record locations as `implementations`. If `go_to_implementation` is **not**
in `supported_tools`, record `"not supported by this server"` — do not stop.
---
## Phase 4 — Call hierarchy and references (run in parallel)
Issue both calls in the same message — they are independent:
### 4a — Incoming callers
Only if `call_hierarchy` appears in `supported_tools`:
```
mcp__lsp__call_hierarchy({
"file_path": "<file from Phase 1>",
"line": <line from Phase 1>,
"column": <column from Phase 1>,
"direction": "incoming"
})
→ returns: list of caller functions with file and line
```
If `call_hierarchy` is **not** in `supported_tools`, note
`"not supported by this server"` — do not stop.
### 4b — All reference sites
```
mcp__lsp__get_references({
"file_path": "<file from Phase 1>",
"line": <line from Phase 1>,
"column": <column from Phase 1>,
"include_declaration": false
})
→ returns: list of reference locations (file, line)
```
Collect all reference locations. Group by file and count distinct files.
---
## Output format — Explore Report
Produce the report in this format:
```
## Explore Report: <SymbolName>
### Definition
- File: <file>:<line>
- Hover: <hover_text or "unavailable">
### Implementations (<N> found, or "not supported")
[list of file:line entries, or "none found", or "not supported by this server"]
### Callers (incoming call hierarchy)
[list of caller function names with file:line, or "none", or "not supported"]
### References (<N> total across <M> files)
[list of file:line entries grouped by file, or "none found"]
### Summary
- Symbol kind: <inferred from hover or "unknown">
- Reference count: <N>
- Files with refs: <M distinct files>
- Callers: <K>
- Implementations: <P or "not supported">
```
Keep the report concise. The goal is "understand this symbol in one pass."
---
## Example
```
Goal: understand the exported function `ParseConfig` in pkg/config
Phase 1 — go_to_symbol: symbol_path="config.ParseConfig"
→ pkg/config/parser.go:42:6
open_document: pkg/config/parser.go
Phase 2 — get_info_on_location: line=42, column=6
→ hover_text: "func ParseConfig(path string) (*Config, error) — reads and
validates a config file from path"
Phase 3 — get_server_capabilities
→ go_to_implementation: in supported_tools
go_to_implementation: line=42, column=6
→ 0 implementations (ParseConfig is a concrete function, not an interface method)
Phase 4 (parallel):
call_hierarchy direction=incoming
→ 3 callers: cmd.main (cmd/main.go:14), app.Start (internal/app.go:31),
loader.Load (internal/loader.go:55)
get_references include_declaration=false
→ 7 references in 4 files
## Explore Report: ParseConfig
### Definition
- File: pkg/config/parser.go:42
- Hover: func ParseConfig(path string) (*Config, error) — reads and validates a
config file from path
### Implementations (0 found)
none found
### Callers (incoming call hierarchy)
- cmd.main — cmd/main.go:14
- app.Start — internal/app.go:31
- loader.Load — internal/loader.go:55
### References (7 total across 4 files)
cmd/main.go: line 14
internal/app.go: lines 31, 87
internal/loader.go: line 55
pkg/config/parser_test.go: lines 12, 34, 56, 78
### Summary
- Symbol kind: function
- Reference count: 7
- Files with refs: 4
- Callers: 3
- Implementations: 0
```Extract a selected code block into a named function. Primary path uses the language server's extract-function code action; falls back to manual extraction when no code action is available. Validates captured variables, scope shadowing, and compilation after extraction.
> Requires the agent-lsp MCP server.
# lsp-extract-function: Extract Code Block into a Named Function
**This skill RESTRUCTURES existing code** — it takes code that already exists
and moves it into a new function. This is distinct from `/lsp-generate`, which
creates NEW code that does not yet exist (stubs, mocks, interface implementations).
Use this skill when the code is already written; use `/lsp-generate` when you
need to generate code from scratch.
**Invocation:** User provides `file_path` (absolute path), `start_line` and
`end_line` (1-indexed range), and `new_function_name` (desired name for the
extracted function).
---
## Prerequisites
If LSP is not yet initialized, call `mcp__lsp__start_lsp` with the workspace
root first. Auto-inference applies when file paths are provided, but an explicit
start is required when switching workspaces.
---
## Step 1 — Get context (document symbols)
Call `mcp__lsp__open_document` to open the file, then call
`mcp__lsp__get_document_symbols` to understand the containing function and scope:
```
mcp__lsp__open_document({ "file_path": "<file_path>" })
mcp__lsp__get_document_symbols({ "file_path": "<file_path>" })
```
This establishes:
- Which function contains the selection
- Whether `new_function_name` already exists in the file (name collision check)
**Mandatory name collision check:** If `new_function_name` already exists as a
symbol in the document symbols list, report the conflict and stop immediately:
> Cannot extract: function `new_function_name` already exists in this file.
> Choose a different name and retry.
---
## Step 2 — Check server capabilities
Call `mcp__lsp__get_server_capabilities` to understand what the language server
supports:
```
mcp__lsp__get_server_capabilities({})
```
Check for `codeActionProvider` in the response. Note whether `execute_command`
is listed in `executeCommandProvider.commands`. This determines whether the
primary path (Step 3) is available.
---
## Step 3 — Primary path: LSP code action
Call `mcp__lsp__get_code_actions` with the selection range:
```
mcp__lsp__get_code_actions({
"file_path": "<file_path>",
"start_line": N,
"start_column": 1,
"end_line": M,
"end_column": 999
})
```
Filter the returned actions for extract-function actions: include any action
whose `kind` contains `"refactor.extract"` OR whose `title` contains both
"Extract" and "function" (case-insensitive).
**If an extract-function action is found:**
- Display the action title to the user
- If the action proposes a different name than `new_function_name`, ask for
confirmation before proceeding
- Execute via `mcp__lsp__execute_command` if the action has a `command` field:
```
mcp__lsp__execute_command({
"command": "<action.command.command>",
"arguments": <action.command.arguments>
})
```
- OR apply directly via `mcp__lsp__apply_edit` if the action has an `edit` field:
```
mcp__lsp__apply_edit({ "workspace_edit": <action.edit> })
```
- Skip to Step 5 after applying.
**If no extract-function action is found:** fall through to Step 4 (manual fallback).
---
## Step 4 — Manual fallback
When no code action is available, perform manual extraction:
### a) Analyze the selection
Read the selected lines (`start_line` through `end_line`) and identify:
- **Parameters:** Variables used inside the selection that are declared outside
(captured from outer scope — must become function parameters)
- **Return values:** Variables declared inside the selection that are used outside
(must be returned from the extracted function)
- **Early returns:** Return statements inside the selection (the extracted function
must wrap these)
### b) Construct and confirm the proposed signature
Build the extracted function signature based on the captured variables analysis.
Display the proposed signature to the user before writing:
> Proposed extraction:
> ```
> func new_function_name(param1 Type1, param2 Type2) (ReturnType, error) {
> // selected lines
> }
> ```
> Proceed with this signature? [y/n]
Wait for user confirmation before applying any edit.
### c) Apply the extraction (order matters)
Apply edits sequentially — do NOT batch edits from different line regions into a
single `apply_edit` call:
1. **First:** Replace the selected lines with a call to the new function:
```
mcp__lsp__apply_edit({
"workspace_edit": {
"changes": {
"<file_path>": [{
"range": { "start": { "line": start_line-1, "character": 0 },
"end": { "line": end_line, "character": 0 } },
"newText": " result := new_function_name(args...)\n"
}]
}
}
})
```
2. **Second:** Insert the new function definition after the containing function's
closing brace:
```
mcp__lsp__apply_edit({
"workspace_edit": {
"changes": {
"<file_path>": [{
"range": { "start": { "line": insert_line, "character": 0 },
"end": { "line": insert_line, "character": 0 } },
"newText": "\nfunc new_function_name(params) ReturnType {\n ...\n}\n"
}]
}
}
})
```
Apply call-site replacement first, then insert the new function. This order
preserves line numbers during editing: replacing call site does not shift the
insertion point for the new function definition.
---
## Step 5 — Validate
After extraction via either path:
### 1. Check diagnostics
```
mcp__lsp__get_diagnostics({ "file_path": "<file_path>" })
```
If errors are reported, display them with the table of common causes below.
### 2. Common post-extraction errors
| Error type | Likely cause | Fix |
|------------|--------------|-----|
| Undefined variable | Captured var not passed as parameter | Add parameter |
| Type mismatch | Return type inferred incorrectly | Adjust return type in signature |
| Name shadows outer | New function name matches outer scope | Choose different name |
| Unused variable | Return value not captured at call site | Add variable at call site |
### 3. Format the document
```
mcp__lsp__format_document({ "file_path": "<file_path>" })
```
This cleans up indentation introduced by the extraction.
---
## Output Format
After completing extraction, display:
```
## Extraction Summary
- File: path/to/file.go
- Extracted: lines N–M
- New function: new_function_name
- Path used: LSP code action / Manual fallback
- Post-extraction errors: 0
```
Follow with the Diagnostic Summary if any errors changed (format in
[references/patterns.md](references/patterns.md)).
---
## Language-Specific Notes
- **Go:** gopls may offer "Extract function" in code actions for selection ranges.
Check code actions first; gopls support varies by version.
- **TypeScript/JavaScript:** tsserver may offer "Extract to function in global scope"
or "Extract to inner function" — filter for these titles in Step 3.
- **Python:** pylsp and pyright-langserver typically do NOT offer extract-function
code actions. Manual fallback (Step 4) is required for Python files.Apply available quick-fix code actions for all current diagnostics in a file, one at a time with re-collection between each fix. Use to bulk-resolve errors and warnings the language server can fix automatically.
> Requires the agent-lsp MCP server.
# lsp-fix-all
Apply available quick-fix code actions for all current diagnostics in a file,
one at a time, re-collecting diagnostics between each fix because line numbers
shift after each application.
**Important distinction from `/lsp-safe-edit`:** This skill fixes **pre-existing**
diagnostics in a file — errors and warnings that already exist before any edit
session begins. `/lsp-safe-edit` has a code-action step (Step 7) for fixing errors
**introduced by a specific edit you just made**. Use this skill for systematic
bulk-fixing of existing issues, independent of any edit session.
## When to use / not use
**Use this skill when:**
- A file has accumulated errors or warnings you want to resolve automatically
- You want to clean up a file before starting new work
- You want to apply all available language-server quick-fixes in bulk
**Do NOT use this skill when:**
- You just made an edit and want to fix newly introduced errors — use `/lsp-safe-edit`
- You want to apply structural refactors — this skill applies quick-fixes only (see filtering below)
- The file has zero diagnostics (the skill will report clean and stop)
## Input
- **file_path:** Absolute path to the file to fix.
---
## Workflow
### Step 1 — Open and collect initial diagnostics
Call `mcp__lsp__open_document` with the target file path to ensure it is loaded
in the language server. Then call `mcp__lsp__get_diagnostics` to retrieve all
current diagnostics.
If zero diagnostics are returned: report "No diagnostics found — file is clean."
and stop. No further steps are needed.
Record the initial count of errors and warnings for the summary output.
### Step 2 — Classify and filter code actions
For EACH diagnostic (process one at a time, not in batch):
1. Call `mcp__lsp__get_code_actions` at the diagnostic's position/range.
2. Filter the returned actions to quick-fix kind only.
3. Skip any diagnostic for which no applicable quick-fix exists — note it in the summary.
**Decision gate — which code actions to apply:**
| Action kind | Apply? |
|---|---|
| `quickfix` | YES |
| `quickfix.*` | YES |
| `refactor` | NO — structural change |
| `refactor.extract` | NO — structural change |
| `refactor.inline` | NO — structural change |
| `source.organizeImports` | YES — safe formatting |
| `source.*` (others) | NO — skip unless organizeImports |
| (no kind / empty) | NO — unknown, skip |
A code action qualifies if: `kind == "quickfix"`, OR `kind` starts with `"quickfix."`,
OR `kind == "source.organizeImports"`.
Reject actions whose kind is `"refactor"`, starts with `"refactor."`, or has no
kind field at all.
### Step 3 — Apply one fix and re-collect (the core loop)
This is the critical correctness constraint: **never apply more than one fix per
iteration.** After each `apply_edit` call, line numbers in the file shift. Always
re-call `get_diagnostics` before processing the next diagnostic.
**Loop:**
```
iteration = 0
max_iterations = 50
while iteration < max_iterations:
diagnostics = mcp__lsp__get_diagnostics(file_path)
if diagnostics is empty: break
for each diagnostic in diagnostics:
actions = mcp__lsp__get_code_actions(diagnostic.range)
applicable = filter to quickfix / source.organizeImports kinds (see Step 2)
if applicable is not empty:
apply the first applicable action via mcp__lsp__apply_edit
record: (line, message, action title) in "Fixed" list
iteration += 1
break # restart the outer loop — line numbers have shifted
if no diagnostic in this pass had an applicable quick-fix:
break # no progress possible — exit loop
```
Exit the loop when:
- The diagnostics list is empty, OR
- No remaining diagnostic has an applicable quick-fix action, OR
- The iteration counter reaches 50 (safety guard against edge cases where a
fix introduces a new fixable diagnostic, preventing infinite loops)
If `apply_edit` returns an error: stop the loop immediately and report the
failure in the summary. Do not attempt further fixes.
### Step 4 — Verify and format
After the loop exits:
1. Call `mcp__lsp__get_diagnostics` one final time to capture the post-fix state.
2. For any remaining diagnostics that had no applicable quick-fix, list them in
the "Skipped" section with explanation.
3. Call `mcp__lsp__format_document` to clean up any indentation drift introduced
by the applied edits.
### Output format
```
## lsp-fix-all Summary
File: /path/to/file.go
Initial diagnostics: N errors, M warnings
Fixes applied: K
Remaining (no auto-fix available): J
### Fixed
- line X: <message> → applied: <action title>
### Skipped (no quick-fix available)
- line Y: <message>
```
If `apply_edit` failed mid-loop, append:
```
### Loop stopped
- apply_edit returned error on line Z: <error message>
- Fixes applied before failure: K
```
---
## Safety rules
- Never apply more than one code action per loop iteration
- Always re-collect diagnostics after each `apply_edit` before the next fix
- Never apply refactor or structural code actions — quick-fix and source.organizeImports only
- If `apply_edit` returns an error, stop the loop and report the failure; do not continue
- Maximum iterations: 50 (safety guard against infinite loops in edge cases where a fix introduces a new fixable diagnostic)
- Do not use `execute_command` — `apply_edit` is sufficient for all quick-fixes
---
## Prerequisites
LSP must be running for the target workspace. If not yet initialized, call
`mcp__lsp__start_lsp` with the workspace root before proceeding.
Auto-init note: agent-lsp supports workspace auto-inference from file paths.
Explicit `start_lsp` is only needed when switching workspace roots.Format a file or selection using the language server's formatter. Use before committing to apply consistent style, or after generating code to clean up indentation and spacing. Supports full-file and range-based formatting.
> Requires the agent-lsp MCP server.
# lsp-format-code
Format a file or selection using the language server's formatter — the same
formatting engine your IDE uses. Applies language-specific rules (gofmt, prettier,
rustfmt, black) without requiring those tools to be on PATH separately.
## When to use
- Before committing: ensure consistent style across edited files
- After generating code: clean up AI-generated indentation and spacing
- After a refactor that shifted indentation levels
- When a linter flags style violations fixable by the formatter
Use `/lsp-safe-edit` instead when you are making a logic change and want
before/after diagnostic comparison alongside the edit.
---
## Workflow
### Step 1 — Check formatting is supported (optional)
If unsure whether the language server supports formatting for this file, check
capabilities first:
```
mcp__lsp__get_server_capabilities({ "file_path": "<file>" })
```
Look for `documentFormattingProvider` (full-file) and
`documentRangeFormattingProvider` (range). If neither is present, the server
does not support formatting — stop and report.
Skip this step if you know the language supports formatting (Go, TypeScript,
Rust, Python all do via their standard servers).
### Step 2 — Open the file
```
mcp__lsp__open_document({ "file_path": "/abs/path/to/file.go", "language_id": "go" })
```
### Step 3 — Request formatting edits
**Full file:**
```
mcp__lsp__format_document({ "file_path": "/abs/path/to/file.go" })
```
**Selection only:**
```
mcp__lsp__format_range({
"file_path": "/abs/path/to/file.go",
"start_line": <N>,
"end_line": <M>
})
```
Both return `TextEdit[]` — a list of replacements to apply. They do **not**
write to disk. If the list is empty, the file is already correctly formatted.
### Step 4 — Apply the edits
Pass the `TextEdit[]` from Step 3 to `apply_edit`:
```
mcp__lsp__apply_edit({ "workspace_edit": <TextEdit[] from Step 3> })
```
This writes the formatting changes to disk.
### Step 5 — Verify (optional but recommended)
Call `get_diagnostics` to confirm formatting did not introduce any errors:
```
mcp__lsp__get_diagnostics({ "file_path": "/abs/path/to/file.go" })
```
Formatting should never introduce errors — if it does, report immediately
without committing.
---
## Output format
```
## Format result: <filename>
Changes applied: N edits
Lines affected: <range or "whole file">
Formatter: <gopls | tsserver | rust-analyzer | ...>
Status: FORMATTED ✓
```
If no edits were returned:
```
Status: ALREADY FORMATTED — no changes needed
```
If formatting is not supported:
```
Status: NOT SUPPORTED — <server> does not expose documentFormattingProvider
Fallback: run the formatter directly (gofmt, prettier, rustfmt, etc.)
```
---
## Multi-file formatting
For formatting multiple files (e.g. all files changed in a PR):
1. Call `format_document` for each file — these can run in parallel.
2. Collect all `TextEdit[]` responses.
3. Apply each file's edits via `apply_edit` sequentially.
4. Report total edits across all files.
Do not apply edits from multiple files in a single `apply_edit` call —
apply per-file to keep changes scoped and reversible.
---
## Decision guide
| Situation | Action |
|-----------|--------|
| Formatting a whole file before commit | `format_document` → `apply_edit` |
| Formatting only generated code in a function | `format_range` with the function's line range |
| Empty `TextEdit[]` returned | File is already formatted — no action needed |
| Server doesn't support formatting | Report and suggest running CLI formatter directly |
| Formatting introduces diagnostics | Do not commit — report immediately |
| Formatting a Go file in a workspace repo | Ensure `GOWORK=off` is set if running via shell fallback |
---
## Language notes
| Language | Formatter | Server |
|----------|-----------|--------|
| Go | `gofmt` (via gopls) | `gopls` |
| TypeScript / JavaScript | `prettier` or built-in (via tsserver) | `typescript-language-server` |
| Rust | `rustfmt` (via rust-analyzer) | `rust-analyzer` |
| Python | `black` or `autopep8` (via pyright/pylsp) | `pyright-langserver` or `pylsp` |
| C / C++ | `clang-format` (via clangd) | `clangd` |
The language server delegates to the language's standard formatter — results
match what your IDE would produce.Trigger language server code generation — implement interface stubs, generate test skeletons, add missing methods, generate mock types. Uses get_code_actions to surface generator options and execute_command to run them.
> Requires the agent-lsp MCP server.
# lsp-generate
**lsp-generate creates NEW code that does not yet exist in the file** — stubs,
mocks, implementations of interfaces, test functions. It is distinct from
`lsp-extract-function`, which restructures code that already exists. Use
`lsp-generate` when you want the language server to write something new; use
`lsp-extract-function` when you want to reorganize existing code.
## Input
- **`file_path`**: absolute path to the target file
- **`line`, `column`** (or **`position_pattern`**): position in the file where
generation is triggered (e.g., the line with the unimplemented interface,
the missing method error, the type declaration)
- **`intent`**: description of what to generate (e.g., "implement io.Reader",
"generate test skeleton", "add missing methods", "generate mock for Handler")
## Prerequisites
LSP must be running for the target workspace. If not yet initialized, call
`mcp__lsp__start_lsp` with the workspace root before proceeding.
Auto-init note: agent-lsp supports workspace auto-inference from file paths.
Explicit `start_lsp` is only needed when switching workspace roots.
---
## Workflow
### Step 1 — Open document and locate position
Call `mcp__lsp__open_document` for the target file:
```
mcp__lsp__open_document(file_path: "/abs/path/to/file.go", language_id: "go")
```
If using `position_pattern`, use the @@ marker convention from
`references/patterns.md` to identify the exact cursor position. For example:
```
"position_pattern": "var _ io.Reade@@r = (*MyType)(nil)"
```
### Step 2 — Get code actions at target position
```
mcp__lsp__get_code_actions({
"file_path": "...",
"start_line": N,
"start_column": C,
"end_line": N,
"end_column": C
})
```
Filter for generator actions:
- Kind `"quickfix"` with titles matching the intent (e.g., "Implement
interface", "Generate", "Add stub", "Create test")
- Kind `"source"` for source-level generation
If no matching action is found, report "No generator action available at this
position for the given intent" and proceed to the Fallback section below.
### Step 3 — Select and confirm action
Display available generator actions to the user. If multiple actions match the
intent, list all of them and ask which to apply. Confirm the selected action
before executing — do NOT auto-select when multiple candidates exist.
### Step 4 — Execute generator
Execute one generator at a time. Do NOT batch multiple `execute_command` calls.
- If the action has a `command` field: run via `mcp__lsp__execute_command`
- If the action has an `edit` field: apply via `mcp__lsp__apply_edit`
- If the action has both: apply the edit first, then run the command
### Step 5 — Format and verify
```
mcp__lsp__format_document({ "file_path": "..." })
mcp__lsp__get_diagnostics({ "file_path": "..." })
```
Report remaining diagnostics. Stub methods typically leave TODO comments or
`panic("not implemented")` bodies — this is expected behavior from the language
server. Surface any unexpected errors.
---
## Per-Language Generator Patterns
| Language | Generator | Trigger location | Code action kind |
|----------|-----------|-----------------|-----------------|
| Go (gopls) | Implement interface | Line with `var _ MyInterface = (*MyType)(nil)` or type declaration | `quickfix` — "Implement interface" |
| Go (gopls) | Generate test file | Any .go file without _test.go counterpart | `source` — "Generate unit tests" |
| Go (gopls) | Add missing method | Line with `undefined: method` error | `quickfix` |
| TypeScript (tsserver) | Implement interface | Class declaration | `quickfix` — "Implement interface members" |
| TypeScript (tsserver) | Add missing method | Method call with no definition | `quickfix` — "Add missing function declaration" |
| Python (pyright) | Add import | Name not defined | `quickfix` — "Add import" |
| Rust (rust-analyzer) | Implement trait | `impl Trait for Type {}` | `quickfix` — "Add missing impl members" |
---
## Fallback When No Code Action Is Available
If `get_code_actions` returns no generator actions, the language server at this
workspace may not support server-side generation for this intent. Explain this
to the user and suggest a manual approach specific to the intent:
- **Interface implementation:** Look up the interface definition first using
`mcp__lsp__go_to_symbol` to discover all required methods, then implement
them manually.
- **Test skeleton:** Check `mcp__lsp__get_server_capabilities` to confirm
whether the server advertises code action support; if not, generate the test
skeleton manually using standard testing package conventions.
- **Missing methods:** Use `mcp__lsp__get_diagnostics` to enumerate the missing
symbols by name, then implement them one at a time.
---
## Constraints
- Do NOT batch `execute_command` calls — run one generator at a time
- Do NOT skip user confirmation when multiple generator actions are availableBlast-radius analysis for a symbol or file — shows all callers, type supertypes/subtypes, and reference count before you change it. Use when refactoring, deleting, or changing the signature of any function, type, or method. Also accepts a file path to surface all exported-symbol impact in one shot.
> Requires the agent-lsp MCP server.
# lsp-impact
Blast-radius analysis for any symbol or file. Discovers all direct references,
callers (via call hierarchy), and type relationships before you touch anything.
Read-only — does not modify any files.
Run this skill **before** lsp-edit-export: impact tells you what exists and how
widespread the change is; lsp-edit-export tells you how to execute the change safely.
**Invocation:**
- **File path** (e.g. `"internal/lsp/client.go"`) → use the File-level entry (Step 0) to surface all exported-symbol impact at once.
- **Symbol name** in dot notation (e.g. `"codec.Encode"`, `"Buffer.Reset"`) → skip Step 0; start at Prerequisites, then Step 1.
---
## Step 0 — File-level entry (when user provides a file path)
Use this shortcut when the user is changing or auditing an entire file rather
than a single symbol. `get_change_impact` enumerates all exported symbols in
the file, resolves their references, and returns test callers (with enclosing
test function names) and non-test callers in a single call.
```
mcp__lsp__get_change_impact({
"changed_files": ["/abs/path/to/file.go"],
"include_transitive": false // set true to surface second-order callers
})
```
Returns:
- `affected_symbols` — each exported symbol with its reference count
- `test_callers` — test files + enclosing test function names
- `non_test_callers` — production call sites
**Decision after Step 0:**
| Result | Action |
|--------|--------|
| 0 non-test callers | Low blast radius. Proceed with change. |
| Few callers, known files | Medium risk. Update each call site. |
| Many callers across packages | High risk. Consider staged rollout. |
| Want symbol-level detail | Continue to Steps 1–5 for any specific symbol. |
Skip Steps 1–5 if the file-level summary is sufficient.
---
## Prerequisites (for symbol-level Steps 1–5)
If LSP is not yet initialized, call `mcp__lsp__start_lsp` with the workspace
root first.
Check what the server supports before proceeding — `call_hierarchy` and
`type_hierarchy` are optional LSP features not implemented by all servers:
```
mcp__lsp__get_server_capabilities()
```
Note which tools appear in `supported_tools`. Steps 3 and 4 below depend on
this result.
---
## Step 1 — Locate the symbol
Use `go_to_symbol` with the symbol name provided by the user:
```
mcp__lsp__go_to_symbol({
"symbol_path": "Package.SymbolName",
"workspace_root": "/abs/path" // optional, narrows scope
})
→ returns: file, line, column (1-indexed)
```
`symbol_path` uses dot notation. For a top-level function `Encode` in package
`codec`, use `"codec.Encode"`. For a method `Reset` on type `Buffer`, use
`"Buffer.Reset"`.
Record the returned `file`, `line`, and `column` — you will pass them to
every subsequent step.
---
## Step 2 — Enumerate all direct references (always available)
Call `get_references` with `include_declaration: false` to find every usage
site across the workspace:
```
mcp__lsp__get_references({
"file_path": "<file from Step 1>",
"position_pattern": "func @@SymbolName(", // adjust prefix for symbol kind
"include_declaration": false
})
```
Collect all reference locations. Group results by file. Record the total count
and list of files — these feed the Impact Report.
See [references/patterns.md](references/patterns.md) for `position_pattern`
examples by language and symbol kind.
---
## Step 3 — Call hierarchy (callers and callees)
Only if `call_hierarchy` appears in `supported_tools` from Step 0.
```
mcp__lsp__call_hierarchy({
"file_path": "<file from Step 1>",
"line": <line from Step 1>,
"column": <column from Step 1>,
"direction": "incoming" // use "both" if callees are also needed
})
```
If `call_hierarchy` is **not** in `supported_tools`, skip this step entirely.
Note `"call hierarchy not supported by this server"` in the Impact Report.
---
## Step 4 — Type hierarchy (supertypes and subtypes)
Only applicable when the symbol is a **type, interface, or class** (not a
plain function or method). Only if `type_hierarchy` appears in `supported_tools`.
```
mcp__lsp__type_hierarchy({
"file_path": "<file from Step 1>",
"line": <line from Step 1>,
"column": <column from Step 1>,
"direction": "both"
})
```
If the symbol is a **function or method**: skip this step; note
`"not applicable (function)"` in the report.
If `type_hierarchy` is **not** in `supported_tools`: skip this step; note
`"not supported by this server"` in the report.
---
## Step 5 — Report impact surface
Produce the Impact Report using the format defined in
[references/patterns.md](references/patterns.md).
Include:
- Symbol name, kind, and definition location
- Reference count and list of files containing references
- Callers from `call_hierarchy` incoming (or skip note)
- Supertypes and subtypes from `type_hierarchy` (or skip note)
- Blast radius: count of distinct files affected
Then apply the decision guide:
| Blast radius | Recommendation |
|---|---|
| 0 references | Likely dead code. Confirm with lsp-dead-code before deleting. |
| 1–5 files | Low risk. Proceed. Update all callers. |
| 6–20 files | Medium risk. Plan changes carefully. Stage in waves. |
| > 20 files | High risk. Consider a deprecation path or feature flag. |
---
## Example
```
Goal: assess blast radius of exported function `ParseConfig` in pkg/config
Prerequisites — get_server_capabilities:
→ supported_tools: [go_to_symbol, get_references, call_hierarchy, ...]
→ type_hierarchy: not in supported_tools
Step 1 — go_to_symbol: symbol_path="config.ParseConfig"
→ pkg/config/parser.go:42:6
Step 2 — get_references: position_pattern="func @@ParseConfig("
→ 7 references in 4 files
→ cmd/main.go, internal/app.go, internal/loader.go, pkg/config/parser_test.go
Step 3 — call_hierarchy: direction="incoming"
→ callers: cmd.main (cmd/main.go:14), app.Start (internal/app.go:31), ...
Step 4 — type_hierarchy: skipped (function), also not supported by server
Step 5 — Impact Report:
## Impact Report: ParseConfig
- Kind: function
- Definition: pkg/config/parser.go:42:6
- References: 7 across 4 files
...
- Risk level: low
```
## Note on position_pattern
`position_pattern` with `@@` is a agent-lsp extension. If your MCP client
does not support it, fall back to explicit `line` and `column` parameters from
the location returned by `go_to_symbol` in Step 1.Find all concrete implementations of an interface or abstract type. Use when you need to know what types satisfy an interface, or what subtypes exist before changing a base type.
> Requires the agent-lsp MCP server.
# lsp-implement
Find every concrete type that implements an interface, or every subtype of an
abstract type. Read-only — does not modify any files.
Use this skill **before** changing an interface signature, adding a method to
an interface, or removing a base-type method. It tells you every type that
must be updated.
**Invocation:** User provides `type_name` (e.g. `"Handler"`, `"io.Reader"`).
Optionally provide `workspace_root`.
---
## Prerequisites
Check server capabilities — `go_to_implementation` and `type_hierarchy` are
optional features not implemented by all language servers:
```
mcp__lsp__get_server_capabilities()
```
Note which of `go_to_implementation` and `type_hierarchy` appear in
`supported_tools`. The steps below depend on this result.
If neither is supported, report `"Server does not support implementation
lookup"` and stop.
---
## Step 1 — Locate the interface or type
```
mcp__lsp__go_to_symbol({
"symbol_path": "<TypeName>",
"workspace_root": "/abs/path" // optional
})
→ returns: file, line, column (1-indexed)
```
Open the file so the language server tracks it:
```
mcp__lsp__open_document({
"file_path": "<file from go_to_symbol>"
})
```
Record `file`, `line`, `column` for subsequent steps.
---
## Step 2 — Find all implementations
Only if `go_to_implementation` appears in `supported_tools`.
```
mcp__lsp__go_to_implementation({
"file_path": "<file>",
"line": <line>,
"column": <column>
})
```
Returns a list of locations — each is a concrete type that satisfies the
interface. Group by file. Record type names and locations.
If `go_to_implementation` is **not** supported: skip; note in report.
---
## Step 3 — Type hierarchy (subtypes and supertypes)
Only if `type_hierarchy` appears in `supported_tools`.
```
mcp__lsp__type_hierarchy({
"file_path": "<file>",
"line": <line>,
"column": <column>,
"direction": "subtypes" // use "both" to also see what this type extends
})
```
`subtypes` returns concrete types that extend or embed this type.
`supertypes` returns what this type itself implements.
Cross-reference with Step 2 results — the union gives the complete
implementation surface.
If `type_hierarchy` is **not** supported: skip; note in report.
---
## Step 4 — Report
```
## Implementation Report: <TypeName>
### Definition
- File: <file>:<line>
- Kind: interface / abstract type / base struct
### Concrete Implementations (<N> found)
- TypeA — <file>:<line>
- TypeB — <file>:<line>
...
### Type Hierarchy
Supertypes: [list or "none"]
Subtypes: [list or "same as implementations above" or "not supported"]
### Risk Assessment
| N implementations | Recommendation |
|---|---|
| 0 | Interface unused or no external implementors found. May be internal-only. |
| 1–3 | Low risk. All implementors can be updated together. |
| 4–10 | Medium risk. Plan updates package by package. |
| > 10 | High risk. Changing the interface is a breaking API change. |
```
---
## Common use cases
**Before adding a method to an interface:**
Run lsp-implement to find all types that will need the new method. Each
implementation site must be updated — this is your required change list.
**Before removing a method:**
Find all types that implement it. Check whether any external (outside this
repo) packages may be affected.
**Understanding polymorphism in an unfamiliar codebase:**
Run lsp-implement on the primary interface to see the full type hierarchy
before making any changes.
---
## Language notes
| Language | `go_to_implementation` finds... |
|---|---|
| Go | All types with matching method sets |
| TypeScript | All classes implementing the interface |
| Java/C# | All classes/structs implementing the interface |
| Rust | All structs with `impl Trait for ...` |
For Go: `go_to_implementation` on an interface finds all types that satisfy
it, even without an explicit `implements` declaration.Fast file-scoped symbol analysis — find all usages of a symbol within the current file, list all symbols defined in the file, and get type info at a position. Use when you need local-scope analysis without a workspace-wide search.
> Requires the agent-lsp MCP server.
# lsp-local-symbols
File-scoped symbol analysis using the language server index. Faster than
workspace-wide search for questions about a single file: what symbols are
defined here, where is this symbol used within the file, and what type does it
have.
Read-only — does not modify any files.
## When to use
- "Where is `x` used in this file?" — use `get_document_highlights`
- "What functions and types are defined in this file?" — use `get_document_symbols`
- "What type does this symbol have?" — use `get_info_on_location`
- Reviewing a file before editing — get the full symbol map first
- Local refactor scoping — confirm a symbol is only used in one place before inlining it
Use `/lsp-impact` instead when you need workspace-wide callers and cross-file
references. Use `/lsp-dead-code` when auditing exported symbols for zero callers.
## When NOT to use
`get_document_highlights` is file-scoped by design — it only finds usages within
the open file. If a symbol is used across multiple files, this skill will not
find those. Use `get_references` (via `/lsp-impact`) for cross-file analysis.
---
## Workflow
### Step 1 — Open the file
Open the file so the language server tracks it:
```
mcp__lsp__open_document
file_path: "/abs/path/to/file.go"
language_id: "go" # go, typescript, python, rust, etc.
```
### Step 2 — List all symbols in the file
Get the full symbol tree for the file:
```
mcp__lsp__get_document_symbols
file_path: "/abs/path/to/file.go"
```
This returns all functions, types, variables, constants, and methods defined in
the file — including nested symbols (methods on types, fields in structs).
Use this to:
- Understand the file's structure before editing
- Find the exact position of a named symbol
- See what a file exposes before reading it in full
**Reading the output:** Each symbol has a `range` (full body including braces)
and a `selectionRange` (just the name). Coordinates are 1-based. Use
`selectionRange.start.line` and `selectionRange.start.character` as inputs to
`get_document_highlights` and `get_info_on_location`.
### Step 3 — Find all usages within the file
Call `get_document_highlights` at the symbol's position:
```
mcp__lsp__get_document_highlights
file_path: "/abs/path/to/file.go"
line: <selectionRange.start.line from Step 2>
column: <selectionRange.start.character from Step 2>
```
Returns every occurrence of the symbol within the file, classified as:
- `read` — the symbol is read here
- `write` — the symbol is assigned/mutated here
- `text` — a text match (fallback when semantic classification isn't available)
**Speed note:** `get_document_highlights` is significantly faster than
`get_references` for file-local queries — it does not scan the entire workspace
index. Use it first; escalate to `get_references` only if you need cross-file
results.
### Step 4 — Get type information (optional)
For any position of interest, get the type signature and docs:
```
mcp__lsp__get_info_on_location
file_path: "/abs/path/to/file.go"
line: <line>
column: <column>
```
Returns the hover text: type signature, documentation, and inferred types.
Useful for confirming what a symbol is before deciding to rename or inline it.
---
## Output format
Report results in three sections (omit any section with no content):
```
## Symbols in <filename>
### Functions / Methods
- `FuncName` — line N–M
- `(Type) MethodName` — line N–M
### Types
- `TypeName` (struct/interface/alias) — line N
### Variables / Constants
- `ConstName` = value — line N
---
## Usages of `<symbol>` in <filename>
N occurrences across M lines:
- line 12 [write] — assignment
- line 34 [read] — passed as argument
- line 67 [read] — returned
---
## Type info
`<symbol>`: <type signature from get_info_on_location>
```
---
## Decision guide
| Question | Tool |
|----------|------|
| What's in this file? | `get_document_symbols` |
| Where is X used in this file? | `get_document_highlights` |
| What type is X? | `get_info_on_location` |
| Is X safe to inline (used once)? | `get_document_highlights` — count occurrences |
| Is X used outside this file? | Use `/lsp-impact` instead |
| Is X dead code (no callers anywhere)? | Use `/lsp-dead-code` instead |
---
## Example
```
# "Where is the `config` variable used in server.go?"
open_document(file_path="/repo/server.go", language_id="go")
get_document_symbols(file_path="/repo/server.go")
→ finds `config` at selectionRange line 42, col 2
get_document_highlights(file_path="/repo/server.go", line=42, column=2)
→ returns 7 occurrences: 1 write (line 42), 6 reads
get_info_on_location(file_path="/repo/server.go", line=42, column=2)
→ "config *Config — the parsed server configuration"
```End-to-end safe refactor workflow — blast-radius analysis, speculative preview, apply to disk, verify build, run affected tests. Inlines lsp-impact + lsp-safe-edit + lsp-verify + lsp-test-correlation into one coordinated sequence.
> Requires the agent-lsp MCP server.
# lsp-refactor
End-to-end safe refactor workflow. Sequences blast-radius analysis, speculative
preview, disk apply, build verification, and targeted test execution in one
coordinated pass.
**This skill does NOT replace lsp-safe-edit or lsp-impact.**
- `lsp-safe-edit` wraps a single edit with before/after diagnostic comparison —
use it when you need to make one targeted change with careful error diffing.
- `lsp-impact` is read-only blast-radius analysis — use it when you want to
understand scope before deciding whether to proceed.
- `lsp-refactor` sequences ALL four workflows (lsp-impact → lsp-safe-edit →
lsp-verify → lsp-test-correlation) in order. Use it when you know your target
and intent up front and want the complete workflow without switching skills.
---
## Input
- **target**: symbol name in dot notation (e.g. `"codec.Encode"`, `"Buffer.Reset"`)
OR file path (e.g. `"internal/lsp/client.go"`)
- **intent**: description of the change to make (e.g. "rename to ParseConfigV2",
"add a second parameter `timeout time.Duration`")
- **workspace_root**: absolute path to the workspace root
---
## Phase 1 — Blast-Radius Analysis (inlined from lsp-impact)
**This phase is mandatory. Do not skip it, even for "small" refactors.**
Call `mcp__lsp__get_change_impact` with `changed_files` set to the file
containing the target symbol. If the user provided a file path directly, use it.
If the user provided a symbol name, resolve the file first (e.g. via
`mcp__lsp__go_to_symbol`).
```
mcp__lsp__get_change_impact({
"changed_files": ["/abs/path/to/file"],
"include_transitive": false
})
```
Returns:
- `affected_symbols` — exported symbols with reference counts
- `test_callers` — test files and enclosing test function names
- `non_test_callers` — production call sites
**Display:**
- Affected symbol count
- Test callers (each with enclosing test function name)
- Non-test callers (each with file:line)
- Total reference count
**High blast-radius gate:** If the total reference count exceeds 20, STOP and
ask the user to confirm before continuing:
```
High blast radius: N callers found. Proceed with refactor? [y/n]
```
If the user answers "n", abort. Do not proceed to Phase 2.
---
## Phase 2 — Speculative Preview (inlined from lsp-safe-edit)
Only reached if Phase 1 blast radius is acceptable (≤ 20 callers, or user confirmed).
### 2a — Open file and capture baseline diagnostics
```
mcp__lsp__open_document({ "file_path": "/abs/path/to/file", "language_id": "go" })
mcp__lsp__get_diagnostics({ "file_path": "/abs/path/to/file" })
```
Store baseline diagnostics as BEFORE.
### 2b — Speculative simulation
For a **single-file change**: use `simulate_edit_atomic`:
```
mcp__lsp__simulate_edit_atomic({
"file_path": "/abs/path/to/file",
"start_line": <N>,
"start_column": <col>,
"end_line": <N>,
"end_column": <col>,
"new_text": "<replacement text>"
})
```
For a **multi-file change** (e.g. rename + call site updates): use `simulate_chain`:
```
mcp__lsp__simulate_chain({
"workspace_root": "/abs/path/to/workspace",
"language": "go",
"edits": [
{
"file_path": "/abs/path/to/file.go",
"start_line": <N>, "start_column": <col>,
"end_line": <N>, "end_column": <col>,
"new_text": "<replacement>"
}
// additional dependent edits ...
]
})
```
### 2c — Evaluate simulation result
Display the speculative result using the Diagnostic Diff Output Format from
[references/patterns.md](references/patterns.md).
**Decision:**
| `net_delta` | Action |
|-------------|--------|
| ≤ 0 | Safe. Proceed to Phase 3. |
| > 0 | **Abort.** Report introduced errors. Do NOT apply to disk. |
If `net_delta > 0`, stop and show the full list of errors the simulation
introduced. Do not proceed to Phase 3.
---
## Phase 3 — Apply to Disk
Only reached if Phase 2 `net_delta <= 0`.
Apply the change using the Edit or Write tool. For edits computed by simulation,
`mcp__lsp__apply_edit` may be used directly if the simulation returned an edit
object:
```
Edit(file_path: "/abs/path/to/file", old_string: "...", new_string: "...")
```
For multi-file changes, apply each file's edits before moving to Phase 4.
If any individual apply fails, stop and report before applying remaining files.
After applying, format the changed file(s):
```
mcp__lsp__format_document({ "file_path": "/abs/path/to/file" })
```
Apply the returned `TextEdit[]` via `mcp__lsp__apply_edit` if non-empty.
---
## Phase 4 — Build Verification (inlined from lsp-verify)
Run in this order — LSP diagnostics first, then the compiler build:
```
mcp__lsp__get_diagnostics({ "file_path": "/abs/path/to/file" })
mcp__lsp__run_build({ "workspace_root": "/abs/path/to/workspace" })
```
**Decision:**
| Result | Action |
|--------|--------|
| Diagnostics clean, build passes | Proceed to Phase 5. |
| Diagnostics show new errors | Display errors and stop. Do not proceed to Phase 5. |
| Build fails | Display build output and stop. Do not proceed to Phase 5. |
If build fails, report the full build error output and stop. Test execution
is skipped until build passes.
---
## Phase 5 — Run Affected Tests (inlined from lsp-test-correlation)
For each file changed in Phase 3, find correlated test files:
```
mcp__lsp__get_tests_for_file({ "file_path": "/abs/path/to/changed/file" })
```
Deduplicate the resulting test files if multiple source files were changed.
Run only the correlated test files:
```
mcp__lsp__run_tests({ "workspace_root": "/abs/path/to/workspace", "test_files": [...] })
```
**If no correlated test files are found:** note "No test correlation found —
run full suite manually to confirm." Do not attempt to run `./...` automatically.
---
## Abort Conditions
The following conditions abort the workflow immediately. Each abort displays the
relevant output before stopping.
1. **Phase 1:** blast radius > 20 callers AND user does not confirm → abort
2. **Phase 2:** `net_delta > 0` (simulation introduced errors) → abort, show errors
3. **Phase 4:** build fails → abort, show build output
4. **Any phase:** LSP tool returns an unexpected error → abort, report tool output verbatim
---
## Output Format
After completing all phases, produce this structured report:
```
## lsp-refactor Complete
### Phase 1 — Blast Radius
Affected symbols: N
Test callers: M (list each with enclosing test function)
Non-test callers: K
### Phase 2 — Speculative Preview
[Diagnostic Diff Output Format from patterns.md]
net_delta: 0 → safe to apply
### Phase 3 — Applied
Files changed: [list]
### Phase 4 — Build Verification
Diagnostics: N errors (0 new)
Build: PASS
### Phase 5 — Test Results
Test files run: [list]
Result: PASS / FAIL
```
If the workflow aborted at a phase, report only the phases completed and the
abort reason:
```
## lsp-refactor Aborted at Phase 2
### Phase 1 — Blast Radius
...
### Phase 2 — Speculative Preview
ABORTED: net_delta: +2 (errors introduced)
Errors:
- file.go:34 — undefined: NewType
- file.go:51 — cannot use int as string
```
---
## Example
```
Goal: rename exported function ParseConfig → ParseConfigV2 in pkg/config
Phase 1 — Blast Radius
get_change_impact(changed_files=["pkg/config/parser.go"])
→ affected_symbols: 1 (ParseConfig)
→ non_test_callers: 3 (cmd/main.go, internal/app.go, internal/loader.go)
→ test_callers: 1 (pkg/config/parser_test.go — TestParseConfig)
→ total references: 4 — within threshold, proceeding
Phase 2 — Speculative Preview
open_document(file_path="pkg/config/parser.go")
get_diagnostics → BEFORE: 0 errors
simulate_chain(edits: [parser.go rename + 3 call-site updates])
→ cumulative_delta: 0 → safe to apply
Phase 3 — Applied
Edit parser.go: func ParseConfig → func ParseConfigV2
Edit cmd/main.go, internal/app.go, internal/loader.go: update call sites
format_document(parser.go)
Phase 4 — Build Verification
get_diagnostics → 0 errors
run_build → success
Phase 5 — Test Results
get_tests_for_file(parser.go) → pkg/config/parser_test.go
run_tests(test_files=["pkg/config/parser_test.go"]) → PASS
## lsp-refactor Complete
...
```Two-phase safe rename across the entire workspace. Use when renaming any symbol, function, method, variable, type, or identifier — shows all affected sites before executing atomically via LSP. Never renames without confirmation.
> Requires the agent-lsp MCP server.
# lsp-rename: Safe Symbol Rename
Renames a symbol across the workspace in two phases: preview first, then execute
only after explicit confirmation. Never renames without showing impact.
**Invocation:** User provides `old_name` (the symbol to rename) and `new_name`
(the replacement). Optionally provide `workspace_root` to scope the search.
---
## Prerequisites
If LSP is not yet initialized, call `mcp__lsp__start_lsp` with the workspace
root first. Auto-inference applies when file paths are provided, but an explicit
start is required when switching workspaces.
---
## Phase 1: Preview
Find the symbol, enumerate all references, and produce a dry-run preview.
**Do not apply any edits in this phase.**
### Step 1 — Locate the symbol
Call `mcp__lsp__go_to_symbol` with `symbol_path` set to `old_name`:
```
mcp__lsp__go_to_symbol
symbol_path: "old_name" # or "Package.OldName" for qualified paths
workspace_root: "<root>" # optional; omit to search entire workspace
```
This returns the definition location (file, line, column). If not found, report
the error and stop.
### Step 2 — Validate rename is possible
Call `mcp__lsp__prepare_rename` at the definition location from Step 1:
```
mcp__lsp__prepare_rename
file_path: "<file from Step 1>"
line: <line from Step 1>
column: <column from Step 1>
```
`prepare_rename` asks the language server whether a rename is valid at this
position. If it returns an error (e.g. the symbol is a keyword, a built-in, or
in a location the server cannot rename), **stop immediately** and report:
> Cannot rename `OldName`: <server error message>
> Common causes: built-in or keyword, imported external package, or position is
> not on the symbol name. Try locating the declaration site directly.
Only proceed to Step 3 if `prepare_rename` succeeds.
### Step 3 — Enumerate references
Call `mcp__lsp__get_references` at the definition location from Step 1:
```
mcp__lsp__get_references
file_path: "<file from Step 1>"
position_pattern: "<symbol>@@" # @@ immediately after the symbol name
# fallback: use line/column from Step 1 if position_pattern is unavailable
```
Collect all returned locations. Note the total count and the distinct files.
### Step 4 — Dry-run preview
Call `mcp__lsp__rename_symbol` with `dry_run=true`. **Do not call `apply_edit`.**
```
mcp__lsp__rename_symbol
file_path: "<file from Step 1>"
line: <line from Step 1>
column: <column from Step 1>
new_name: "<new_name>"
dry_run: true
```
The response includes a `workspace_edit` with all proposed changes and a
`preview.note` describing the scope.
### Step 5 — Report impact and hard stop
Display the preview summary to the user:
```
Rename preview: OldName -> NewName
Locations to update: N (from get_references count)
Files affected: M (distinct files in workspace_edit)
Language server: <gopls | tsserver | rust-analyzer | ...>
Changes:
path/to/file1.go lines 12, 45, 78
path/to/file2.go line 3
...
```
**REQUIRED hard stop — do not proceed without explicit user confirmation:**
> Proceed with rename? [y/n]
Wait for user input. Do not apply any edit until the user answers "y" or "yes".
---
## Edge Case: 0 References
If `get_references` returns an empty list (the symbol exists but has no external
usages), warn the user before stopping:
> Warning: no references found for `OldName`. The symbol may be unexported,
> dead code, or the LSP index may be stale. Renaming will update only the
> declaration site.
> Proceed anyway? [y/n]
If user answers "n", stop. If "y", continue to Phase 2.
---
## Phase 2: Execute
Only enter this phase after the user answers "y" or "yes" to the confirmation
prompt in Phase 1.
### Step 1 — Capture pre-rename diagnostics
Before applying changes, capture the current diagnostic state:
```
mcp__lsp__get_diagnostics
file_path: "<one or more files in the workspace_edit>"
```
Store the result as `before_diagnostics`.
### Step 2 — Execute rename
Call `mcp__lsp__rename_symbol` without `dry_run` (or with `dry_run=false`):
```
mcp__lsp__rename_symbol
file_path: "<file from Phase 1 Step 1>"
line: <line from Phase 1 Step 1>
column: <column from Phase 1 Step 1>
new_name: "<new_name>"
```
This returns a `workspace_edit` with the full set of changes.
### Step 3 — Apply the edit
Call `mcp__lsp__apply_edit` with the `workspace_edit` from Step 2:
```
mcp__lsp__apply_edit
workspace_edit: <workspace_edit from rename_symbol>
```
### Step 4 — Check diagnostics
Call `mcp__lsp__get_diagnostics` on the affected files and compare against
`before_diagnostics`:
```
mcp__lsp__get_diagnostics
file_path: "<affected files>"
```
Compute introduced vs. resolved errors and display the Diagnostic Summary (see
[references/patterns.md](references/patterns.md)).
---
## Output Format
After Phase 2 completes, display:
```
## Rename Summary
- Old name: OldName
- New name: NewName
- Files changed: M
- Locations updated: N
- Post-rename errors: 0
```
Follow with the Diagnostic Summary if any errors changed (format in
[references/patterns.md](references/patterns.md)).
Only show Diagnostic Summary sections where N > 0. A net change of 0 means the
rename is safe.
---
## Language Support
The following language servers support `rename_symbol`:
- **Go** — `gopls`
- **TypeScript / JavaScript** — `tsserver`
- **Rust** — `rust-analyzer`
Other LSP-compliant servers that implement `textDocument/rename` also work.
Check your server's capability list via `mcp__lsp__get_server_capabilities` if
you are unsure.Wrap any code edit with before/after diagnostic comparison. Speculatively previews the change first (simulate_edit_atomic), then applies to disk only if the error delta is acceptable. If post-edit errors appear, surfaces code actions for quick fixes. Handles single and multi-file edits.
> Requires the agent-lsp MCP server.
# lsp-safe-edit
Wrap any code edit with a before/after diagnostic comparison. Speculatively
previews the change in-memory before touching disk, then diffs errors introduced
vs. resolved after applying. If errors appear, surfaces code actions to fix them.
## Prerequisites
LSP must be running for the target workspace. If not yet initialized, call
`mcp__lsp__start_lsp` with the workspace root before proceeding.
Auto-init note: agent-lsp supports workspace auto-inference from file paths.
Explicit `start_lsp` is only needed when switching workspace roots.
## Input
- **target file(s):** One or more files to be edited (absolute paths).
- **description of change:** What you intend to edit and why.
---
## Workflow
### Step 1 — Open target file(s)
Call `mcp__lsp__open_document` for each file that will be edited:
```
mcp__lsp__open_document(file_path: "/abs/path/to/file.go", language_id: "go")
```
### Step 2 — Capture baseline diagnostics (BEFORE)
Call `mcp__lsp__get_diagnostics` for each target file. Store as BEFORE.
For multi-file edits, collect diagnostics for all files involved.
```
BEFORE = mcp__lsp__get_diagnostics(file_path: "/abs/path/to/file.go")
```
If the server returns an empty list immediately after open, wait briefly and
retry — LSP analysis is async.
### Step 3 — Speculative preview (simulate_edit_atomic)
Before touching disk, call `mcp__lsp__simulate_edit_atomic` to preview the
error delta of the intended change:
```
mcp__lsp__simulate_edit_atomic(
file_path: "/abs/path/to/file.go",
start_line: <N>,
start_column: <col>,
end_line: <N>,
end_column: <col>,
new_text: "<replacement text>"
)
```
Returns `net_delta` (new errors introduced minus errors resolved) without
writing to disk.
**Decision:**
| `net_delta` | Action |
|-------------|--------|
| ≤ 0 | Proceed — edit improves or does not worsen error state |
| > 0 | **Pause.** Report introduced errors to user and ask: "Proceed anyway? [y/n]" |
If `net_delta > 0` and user says "n", stop. Do not apply the edit.
**Multi-file edits:** `simulate_edit_atomic` covers one file at a time. For
edits spanning multiple files, run it for each file independently and sum the
deltas. If any file shows `net_delta > 0`, pause before continuing.
**When to skip Step 3:** If the intended change is a new file (Write), there is
no existing file to simulate against. Skip to Step 4.
### Step 3b — Refactor preview with simulate_chain (renames and signature changes)
Use this step **instead of or after** Step 3 when the change is a rename,
signature change, or any edit with dependent follow-on edits (e.g., updating
all call sites after adding a parameter).
`simulate_chain` applies a sequence of speculative edits in-memory and reports
whether the cumulative change is safe — without touching disk:
```
mcp__lsp__simulate_chain({
"workspace_root": "/abs/path/to/workspace",
"language": "go",
"edits": [
{
"file_path": "/abs/path/to/file.go",
"start_line": <N>, "start_column": <col>,
"end_line": <N>, "end_column": <col>,
"new_text": "<replacement>"
},
// additional dependent edits (e.g. call site updates) ...
]
})
```
Returns:
- `cumulative_delta` — net error change across all steps
- `safe_to_apply_through_step` — how many steps are safe to apply in sequence
**Decision:**
| `cumulative_delta` | `safe_to_apply_through_step` | Action |
|--------------------|------------------------------|--------|
| ≤ 0 | = total steps | All steps safe. Proceed to Step 4. |
| ≤ 0 | < total steps | Safe up to that step. Review remaining steps. |
| > 0 | any | Net regression. Report to user before proceeding. |
**When to use Step 3b:**
- Renaming an exported symbol and updating its call sites
- Adding/removing a parameter and updating all callers
- Any multi-file refactor where edits are order-dependent
**When to skip Step 3b:**
- Simple in-place edits with no dependent follow-on edits (Step 3 is sufficient)
- New file creation (no existing text to simulate against)
See `docs/refactor-preview.md` for worked examples.
### Step 4 — Apply the edit to disk
Apply the change using the Edit or Write tool:
- Use `Edit` for targeted replacements in an existing file.
- Use `Write` only when creating a new file or doing a full rewrite.
```
Edit(file_path: "/abs/path/to/file.go", old_string: "...", new_string: "...")
```
For multi-file edits, apply each file's changes before collecting post-edit
diagnostics (Step 5). If any individual edit fails, stop and report before
applying remaining files.
### Step 5 — Capture post-edit diagnostics (AFTER)
Call `mcp__lsp__get_diagnostics` again for each edited file. Store as AFTER.
```
AFTER = mcp__lsp__get_diagnostics(file_path: "/abs/path/to/file.go")
```
For multi-file edits, collect diagnostics for all files and merge the results.
### Step 6 — Compute the diagnostic diff
Compare BEFORE and AFTER:
- **Introduced** = diagnostics in AFTER not in BEFORE (new problems).
- **Resolved** = diagnostics in BEFORE not in AFTER (fixed problems).
Match by `(file, line, message)` tuple to handle line-number shifts. Treat
`error` and `warning` severity separately.
### Step 7 — Surface code actions if errors were introduced
If any new `error`-severity diagnostics appear, call `mcp__lsp__get_code_actions`
at each error location to surface quick fixes:
```
mcp__lsp__get_code_actions(
file_path: "<file>",
start_line: <error line>,
start_column: 1,
end_line: <error line>,
end_column: 999
)
```
Report available code actions to the user:
```
Errors introduced (2):
file.go:34 — undefined: MyType
→ Code action: Import "mypackage" (quickfix)
file.go:51 — cannot use int as string
→ No code actions available
Apply code actions? [y/n/select]
```
If the user accepts, apply the code action's `WorkspaceEdit` via
`mcp__lsp__apply_edit`, then re-collect diagnostics and re-diff.
### Step 8 — Format (optional)
If the diagnostic diff is clean (net change ≤ 0), offer to format the edited
file via the language server:
```
mcp__lsp__format_document({ "file_path": "/abs/path/to/file" })
```
Returns `TextEdit[]`. If non-empty, apply immediately:
```
mcp__lsp__apply_edit({ "workspace_edit": <TextEdit[]> })
```
Skip if the user did not ask for formatting, or if there are unresolved errors
(fix errors before formatting).
### Step 9 — Report using DiagnosticDiffFormat
Output the final summary:
```
## Edit Summary
Files changed: N
Errors introduced: A → Errors resolved: B (net: A-B)
Warnings introduced: C → Warnings resolved: D
### Introduced errors
- file.go:34 — undefined: MyType
### Resolved errors
- file.go:12 — unused variable: x
```
---
## Decision Guide
| Net change | Action |
|------------|--------|
| 0 | Safe. No new errors. |
| Negative | Net improvement — errors resolved. Safe. |
| Positive (after code actions) | **Do NOT commit.** Offer to revert. |
When net change > 0 after code actions:
1. Show the full list of remaining introduced errors.
2. Offer to revert using the original `old_string` in a follow-up `Edit` call.
3. Wait for user decision before proceeding.
Do not commit or stage files when net change > 0.
---
## Multi-file workflow
For edits spanning multiple files (e.g., changing a function signature and all
its call sites):
1. **Open all files** in Step 1.
2. **Collect BEFORE diagnostics** for all files.
3. **Simulate each file** independently in Step 3 — sum `net_delta` values.
4. **Apply edits file by file** in Step 4 — stop on first failure.
5. **Collect AFTER diagnostics** for all files and merge.
6. **Check code actions** on any file showing new errors.
Report the combined diagnostic diff across all files in the final summary.Speculative code editing session — simulate changes in memory before touching disk. Use when planning edits that might break things, exploring refactors across multiple files, or verifying an edit is safe before applying.
> Requires the agent-lsp MCP server.
# lsp-simulate
Simulate code edits in memory before writing to disk. The LSP server applies
your changes to an in-memory overlay, runs diagnostics, and reports whether
the edit is safe — without touching any files.
## Prerequisites
LSP must be running for the target workspace. If not yet initialized, call
`start_lsp` before any simulation tool.
```
mcp__lsp__start_lsp(root_dir: "/your/workspace")
```
Auto-init note: agent-lsp supports workspace auto-inference from file paths.
Explicit `start_lsp` is only needed when switching workspace roots.
## Quick Start (single edit)
For a single what-if check, use `simulate_edit_atomic` — it creates a session,
applies the edit, evaluates, and destroys the session in one call:
```
mcp__lsp__simulate_edit_atomic(
workspace_root: "/your/workspace",
language: "go",
file_path: "/abs/path/to/file.go",
start_line: 42, start_column: 1,
end_line: 42, end_column: 20,
new_text: "replacement text"
)
```
Result:
```
{ net_delta: 0 } -- safe to apply
{ net_delta: 2 } -- 2 new errors introduced; do NOT apply
```
`net_delta: 0` means no new errors were introduced. Positive values mean
errors were introduced — inspect `errors_introduced` before deciding.
## Full Session Workflow (multiple edits)
Use a full session when applying several edits that build on each other, or
when you want to inspect the patch before deciding whether to write to disk.
**Step 1 — Create a simulation session**
```
mcp__lsp__create_simulation_session(
workspace_root: "/your/workspace",
language: "go"
)
→ { session_id: "abc123" }
```
**Step 2 — Apply edits in-memory**
Call `simulate_edit` one or more times. All edits are in-memory only.
Positions are 1-indexed (matching editor line numbers and `cat -n` output).
```
mcp__lsp__simulate_edit(
session_id: "abc123",
file_path: "/abs/path/to/file.go",
start_line: 10, start_column: 1,
end_line: 10, end_column: 30,
new_text: "func NewClient(cfg Config) *Client {"
)
→ { session_id: "abc123", edit_applied: true, version_after: 1 }
```
Repeat for additional edits as needed.
**Step 3 — Evaluate the session**
```
mcp__lsp__evaluate_session(
session_id: "abc123",
scope: "file"
)
→ {
net_delta: 0,
confidence: "high",
errors_introduced: [],
errors_resolved: [],
edit_risk_score: 0.0,
affected_symbols: []
}
```
`scope: "file"` (default) is faster and returns `confidence: "high"`.
`scope: "workspace"` catches cross-file type errors but returns
`confidence: "eventual"` (results may not be fully settled).
**Step 4 — Decision gate**
If `net_delta == 0`, proceed to commit. Otherwise, discard:
```
mcp__lsp__discard_session(session_id: "abc123")
```
**Step 5 — Commit the session**
```
-- Preview patch only (no disk write):
mcp__lsp__commit_session(session_id: "abc123", apply: false)
-- Write to disk:
mcp__lsp__commit_session(session_id: "abc123", apply: true)
```
**Step 6 — Destroy the session (always)**
```
mcp__lsp__destroy_session(session_id: "abc123")
```
Always call `destroy_session` after commit or discard to release server
resources. See [Cleanup Rule](#cleanup-rule) below.
## Chained Mutations (simulate_chain)
Use `simulate_chain` when you have a sequence of edits and want to find
how far through the sequence is safe to apply. Unlike multiple `simulate_edit`
calls, `simulate_chain` evaluates diagnostics after each step.
```
mcp__lsp__simulate_chain(
session_id: "abc123",
edits: [
{ file_path: "/abs/file.go", start_line: 5, start_column: 1,
end_line: 5, end_column: 40, new_text: "type Foo struct { Bar int }" },
{ file_path: "/abs/file.go", start_line: 20, start_column: 1,
end_line: 20, end_column: 10, new_text: "f.Bar" },
{ file_path: "/abs/other.go", start_line: 8, start_column: 1,
end_line: 8, end_column: 10, new_text: "x.Bar" }
]
)
→ {
steps: [
{ step: 1, net_delta: 0, errors_introduced: [] },
{ step: 2, net_delta: 0, errors_introduced: [] },
{ step: 3, net_delta: 1, errors_introduced: [...] }
],
safe_to_apply_through_step: 2,
cumulative_delta: 1
}
```
`safe_to_apply_through_step: 2` means steps 1 and 2 are safe; step 3
introduced errors. Commit the session after reviewing to apply steps 1–2,
or discard to cancel everything.
## Decision Guide
| net_delta | confidence | Action |
|-----------|-------------|-------------------------------------------------------------------|
| 0 | high | Safe. Commit or apply. |
| 0 | eventual | Likely safe. Workspace scope — re-evaluate if risk matters. |
| > 0 | any | Do NOT apply. Inspect `errors_introduced`. Discard session. |
| > 0 | partial | Timeout. Results incomplete. Discard and retry with smaller scope.|
## Session States
| State | Meaning | Next step |
|-----------|---------------------------------------------------------------|------------------------|
| created | Session initialized, no edits yet | simulate_edit |
| mutated | One or more edits applied in-memory | evaluate_session |
| evaluated | Diagnostics collected | commit or discard |
| committed | Patch returned (and optionally written to disk) | destroy_session |
| discarded | In-memory edits reverted, no disk write | destroy_session |
| dirty | Revert failed or version mismatch; session is inconsistent | destroy_session only |
A session in `dirty` state cannot be recovered — call `destroy_session` immediately.
## Cleanup Rule
Always call `destroy_session` after finishing a session, even on error paths:
```
-- After commit:
mcp__lsp__commit_session(session_id: "abc123", apply: true)
mcp__lsp__destroy_session(session_id: "abc123")
-- After discard:
mcp__lsp__discard_session(session_id: "abc123")
mcp__lsp__destroy_session(session_id: "abc123")
```
**MCP server restart:** Sessions are ephemeral — they live in server memory
only. If the MCP server restarts, all session IDs become invalid. To preserve
work across a restart, call `commit_session(apply: false)` first to get a
portable patch, then re-apply it after the server restarts.
See [references/patterns.md](references/patterns.md) for detailed field
descriptions and confidence interpretation.Find and run the tests that cover a source file. Use after editing a file to discover exactly which test files and test functions need to run — without running the entire test suite.
> Requires the agent-lsp MCP server.
# lsp-test-correlation
Discover which tests cover a source file, then run only those tests. Faster
than running the full suite when you've changed one or two files and want
targeted feedback.
## When to use
- After editing a source file: "Which tests do I need to run for this change?"
- Before committing: run only the tests that cover what you touched
- Debugging a failure: find which test file corresponds to a broken source file
- Code review: understand what test coverage exists for a file before merging
Use `/lsp-verify` instead when you want to run the full suite and check all
three layers (diagnostics + build + tests). Use this skill when you want fast,
scoped test execution.
---
## Workflow
### Step 1 — Find correlated test files
Call `get_tests_for_file` for each edited source file:
```
mcp__lsp__get_tests_for_file({ "file_path": "/abs/path/to/source.go" })
```
Returns the test files that correspond to the source file. For multiple edited
files, call once per file.
**If no test files are returned:** the source file may have no dedicated test
file, or the mapping is not resolvable (e.g. integration tests in a separate
directory). See Step 2 for fallback.
### Step 2 — Enumerate test functions (fallback or enrichment)
If `get_tests_for_file` returns test files, use `get_workspace_symbols` to list
the test functions defined in those files:
```
mcp__lsp__get_workspace_symbols({ "query": "Test" })
```
Filter results to the correlated test files from Step 1. This gives you the
specific test function names to run rather than the whole test file.
**Fallback (no test files found):** query `get_workspace_symbols` for test
functions that contain the changed symbol's name:
```
mcp__lsp__get_workspace_symbols({ "query": "Test<ChangedFunctionName>" })
```
This catches cases where `get_tests_for_file` misses indirect coverage.
### Step 3 — Report the correlation map
Before running, report what was found:
```
## Test correlation for <file>
Source file: internal/tools/analysis.go
Test files:
→ internal/tools/analysis_test.go
Tests: TestHandleGetCodeActions, TestHandleGetCompletions, TestHandleGetDocumentSymbols
No correlated test files found for: internal/lsp/normalize.go
→ Fallback: TestNormalizeCompletion, TestNormalizeDocumentSymbols (from workspace symbol search)
```
If the user provided `run=true` or asks to run, proceed to Step 4. Otherwise
stop here and let the user decide.
### Step 4 — Run correlated tests
Run only the correlated test files or functions. Scope as tightly as possible:
**Go — run specific package:**
```
mcp__lsp__run_tests({ "workspace_dir": "<root>", "test_filter": "TestHandleGetCodeActions|TestHandleGetCompletions" })
```
If `run_tests` does not support `test_filter`, pass the package path instead of
the workspace root to narrow scope. The test output will be smaller and faster
than running `./...`.
**Output handling:** If test output is large, do not read it in full. Search
for failures:
```
grep -E "^(FAIL|--- FAIL)" <output_file>
```
### Step 5 — Report results
```
## Test Results
Ran 3 tests in internal/tools/analysis_test.go
PASSED (2):
TestHandleGetCodeActions
TestHandleGetCompletions
FAILED (1):
TestHandleGetDocumentSymbols — expected 3 symbols, got 2 (analysis_test.go:87)
Recommendation: Fix TestHandleGetDocumentSymbols before committing.
```
---
## Multi-file workflow
For changes spanning multiple source files:
1. Call `get_tests_for_file` for each changed file in parallel.
2. Deduplicate the resulting test files (the same test file may cover multiple
source files).
3. Report the full correlation map before running.
4. Run the deduplicated test set once.
```
## Test correlation for 3 changed files
internal/tools/analysis.go → internal/tools/analysis_test.go
internal/lsp/client.go → internal/lsp/client_test.go, internal/lsp/client_completion_test.go
internal/resources/resources.go → (no dedicated test file)
Deduplicated test files to run: 3
```
---
## Decision guide
| Situation | Action |
|-----------|--------|
| `get_tests_for_file` returns test files | Use those; enumerate functions via `get_workspace_symbols` |
| No test files returned | Fallback to `get_workspace_symbols` with changed symbol names |
| Test files found but no matching test functions | Report gap — this source file may lack unit test coverage |
| More than 10 test files returned | Don't run all; use `/lsp-verify` for full suite instead |
| Test fails | Run `/lsp-verify` for full diagnostic picture |
---
## Example
```
# "I edited internal/tools/symbol_source.go — which tests should I run?"
get_tests_for_file(file_path="/repo/internal/tools/symbol_source.go")
→ internal/tools/symbol_source_test.go
get_workspace_symbols(query="TestGetSymbolSource")
→ TestGetSymbolSource_ContainsPosition (line 12)
→ TestGetSymbolSource_FindInnermost (line 34)
→ TestGetSymbolSource_PositionPattern (line 67)
# Report correlation, then run:
run_tests(workspace_dir="/repo", test_filter="TestGetSymbolSource")
→ 3 passed in 0.4s
```Deep-dive exploration of unfamiliar code — given a symbol or file, builds a complete Code Map showing type info, implementations, call hierarchy (2-level depth limit), all references, and source. Broader than lsp-explore: accepts files, synthesizes multi-symbol relationships, and produces a navigable dependency map.
> Requires the agent-lsp MCP server.
# lsp-understand
Deep-dive exploration of unfamiliar code — given a symbol or file, synthesizes
hover info, implementations, call hierarchy (bounded to 2 levels), all
references, and source into a structured Code Map.
Read-only — does not modify any files.
---
## Differentiation from lsp-explore
`/lsp-explore` is a single-symbol pass: given one symbol name, it runs hover +
implementations + call hierarchy + references and produces a per-symbol report.
Use lsp-explore for quick "what is this one thing" questions.
`/lsp-understand` is broader in three ways:
1. **Accepts a file path as input** — explores all exported symbols in that file
as a group (Mode B), rather than requiring a single symbol name.
2. **Synthesizes cross-symbol relationships** — produces a dependency map showing
how entry points call each other, share callers, or implement the same
interface, rather than isolated per-symbol reports.
3. **Enforces a 2-level call hierarchy depth limit** — prevents infinite recursion
in deeply connected code.
Use lsp-understand for "how does this module work as a whole."
---
## Input — Two Modes
**Mode A (symbol):** User provides a symbol name in dot notation
(e.g., `"codec.Encode"`, `"Handler.ServeHTTP"`).
**Mode B (file):** User provides an absolute file path. All exported symbols in
the file become the entry points.
---
## Prerequisites
Call `mcp__lsp__get_server_capabilities` before Step 2 to determine which
capabilities are available. Skip steps that require missing capabilities:
- `go_to_implementation`: skip Step 2b if `implementationProvider: false`
- `call_hierarchy`: skip Steps 2c and 2d if `callHierarchyProvider: false`; note
in the Code Map output that call hierarchy was unavailable
---
## Step 1 — Entry Point Resolution
### Mode A: Single Symbol
Call `mcp__lsp__go_to_symbol` to locate the symbol definition:
```
mcp__lsp__go_to_symbol({
"symbol_path": "<dot-notation name>", // e.g. "codec.Encode"
"workspace_root": "<root>" // optional
})
→ returns: file_path, line, column (1-indexed)
```
Record `file_path`, `line`, and `column`. If `go_to_symbol` returns nothing,
report:
> Symbol not found: `<name>`
> Check the dot-notation path (e.g. "Package.Symbol") and ensure the workspace
> root covers the file.
Stop immediately — do not proceed to Step 2.
The single symbol becomes the sole entry point.
### Mode B: File Path
Call `mcp__lsp__open_document` then `mcp__lsp__get_document_symbols`:
```
mcp__lsp__open_document({ "file_path": "<absolute path>" })
mcp__lsp__get_document_symbols({ "file_path": "<absolute path>" })
→ returns: list of symbols with kind, line, column
```
Filter to exported symbols:
- **Go:** uppercase first letter
- **TypeScript/JavaScript:** `export` keyword
- **Rust:** `pub` visibility
Cap at **10 exported symbols maximum**. If more than 10 are found, prioritize
top-level functions and types; skip constants and variables.
Each filtered symbol becomes an entry point with its `file_path`, `line`, and
`column`.
---
## Step 2 — Per-Symbol Analysis
For each entry point, run the following sub-steps. Where possible, parallelize
calls within each step.
### 2a — Type Info and Docs
Call `mcp__lsp__get_info_on_location` using `position_pattern` with the `@@`
marker (see references/patterns.md):
```
mcp__lsp__get_info_on_location({
"file_path": "<file>",
"position_pattern": "<symbol@@name>",
"line_scope_start": <line - 5>,
"line_scope_end": <line + 5>
})
→ returns: hover text with type signature and doc comment
```
Store result as `hover_text`. If the call fails or returns nothing, set
`hover_text` to an empty string. Do not stop.
### 2b — Implementations (capability-gated)
If `implementationProvider` is available in server capabilities:
```
mcp__lsp__go_to_implementation({
"file_path": "<file>",
"line": <line>,
"column": <column>
})
→ returns: list of concrete implementation locations
```
Skip if capability is absent. Record `"not supported by this server"` rather
than stopping.
### 2c — Incoming Call Hierarchy (bounded to 2 levels)
If `callHierarchyProvider` is available:
**Level 1 — Direct callers:**
```
mcp__lsp__call_hierarchy({
"file_path": "<file>",
"line": <line>,
"column": <column>,
"direction": "incoming"
})
→ returns: list of direct caller functions with file and line
```
**Level 2 — Callers of callers:**
For each Level 1 caller, call `mcp__lsp__call_hierarchy` once more:
```
mcp__lsp__call_hierarchy({
"file_path": "<caller file>",
"line": <caller line>,
"column": <caller column>,
"direction": "incoming"
})
→ returns: Level 2 callers
```
**STOP at Level 2 — do not recurse further under any circumstances.**
If Level 2 callers > 10: summarize by count and file, do not list individually.
### 2d — Outgoing Calls (Level 1 only)
If `callHierarchyProvider` is available:
```
mcp__lsp__call_hierarchy({
"file_path": "<file>",
"line": <line>,
"column": <column>,
"direction": "outgoing"
})
→ returns: list of functions this symbol calls
```
**Level 1 only — no recursion.**
### 2e — All References
```
mcp__lsp__get_references({
"file_path": "<file>",
"line": <line>,
"column": <column>,
"include_declaration": false
})
→ returns: every usage site across the workspace
```
Group by file and count distinct files.
### 2f — Source
```
mcp__lsp__get_symbol_source({
"file_path": "<file>",
"line": <line>,
"column": <column>
})
→ returns: implementation body
```
---
## Step 3 — Synthesize Relationships
After analyzing all entry points, identify cross-symbol relationships:
- **Internal calls:** Which entry points call each other? (from outgoing calls
in Step 2d)
- **Shared callers:** Which entry points are called by the same Level 1 callers?
- **Shared interface:** Which entry points implement the same interface? (from
Step 2b)
This synthesis step is what distinguishes `/lsp-understand` from running
multiple `/lsp-explore` calls. The output is a dependency map, not isolated
per-symbol reports.
---
## Step 4 — Output: Code Map
Produce a structured Code Map with these sections:
```
## Code Map: <target>
### Summary
<2-3 sentence description of what this code does, synthesized from
hover docs and source reading>
### Symbols (<N> analyzed)
#### <SymbolName>
- **Type:** <type signature from hover>
- **Source:** <file:line>
- **Incoming callers (L1):** <list; count only if > 5>
- **Incoming callers (L2):** <summarized; e.g., "called by 3 HTTP handlers">
- **Outgoing calls:** <what this symbol calls>
- **Implements:** <interface name, if applicable>
- **References:** N sites across M files
### Dependency Relationships
<symbols that call each other, as a simple text diagram or list>
e.g.:
HandlerA → Parse → Validate
HandlerB → Parse
### Entry Points to This Code
<top-level callers that are NOT in this file — where does outside code
call in?>
### Depth-limit Note
Call hierarchy stopped at 2 levels. <N> additional callers exist beyond
Level 2 — use /lsp-explore on specific symbols to drill deeper.
```
---
## Depth Control Rules
These limits are hard constraints — never exceed them:
- Incoming call hierarchy recursion **stops at Level 2**
- Outgoing calls: **Level 1 only**, no recursion
- If Level 2 callers > 10: **summarize by count and file**, do not list individually
- Do NOT follow call chains beyond these limits under any circumstances
---
## Example
```
Goal: understand how the file pkg/codec/encoder.go works as a whole
Step 1 — Mode B (file path)
open_document: pkg/codec/encoder.go
get_document_symbols: pkg/codec/encoder.go
→ exported symbols: Encoder (type), Encode (func), Reset (func), NewEncoder (func)
→ 4 exported symbols (under 10 cap)
get_server_capabilities
→ go_to_implementation: supported
→ call_hierarchy: supported
Step 2 — Per-symbol analysis (run in parallel across symbols)
Symbol: NewEncoder (pkg/codec/encoder.go:12)
get_info_on_location → "func NewEncoder(w io.Writer) *Encoder"
go_to_implementation → 0 (concrete function)
call_hierarchy incoming L1 → 5 callers
call_hierarchy incoming L2 → 3 callers of those callers
call_hierarchy outgoing → calls: bufio.NewWriter
get_references → 5 sites in 3 files
get_symbol_source → implementation body
Symbol: Encode (pkg/codec/encoder.go:28)
get_info_on_location → "func (e *Encoder) Encode(v any) error"
go_to_implementation → implements codec.Encoder interface
call_hierarchy incoming L1 → 8 callers (listed)
call_hierarchy incoming L2 → > 10: "12 additional callers across 5 files"
call_hierarchy outgoing → calls: NewEncoder, e.w.Flush
get_references → 8 sites in 5 files
get_symbol_source → implementation body
(similar for Encoder type and Reset func...)
Step 3 — Synthesize relationships
- Encode calls NewEncoder (internal dependency)
- NewEncoder and Encode share callers in cmd/main.go
- Encode implements codec.Encoder interface
## Code Map: pkg/codec/encoder.go
### Summary
This file implements a streaming JSON encoder backed by a buffered writer.
NewEncoder constructs an Encoder wrapping any io.Writer; Encode serializes
values and flushes. Reset allows reuse without allocation.
### Symbols (4 analyzed)
#### NewEncoder
- **Type:** func NewEncoder(w io.Writer) *Encoder
- **Source:** pkg/codec/encoder.go:12
- **Incoming callers (L1):** cmd.main, app.Start, loader.Load, test.Setup, bench.Run
- **Incoming callers (L2):** 3 callers across 2 files
- **Outgoing calls:** bufio.NewWriter
- **Implements:** n/a
- **References:** 5 sites across 3 files
#### Encode
- **Type:** func (e *Encoder) Encode(v any) error
- **Source:** pkg/codec/encoder.go:28
- **Incoming callers (L1):** 8 callers (cmd/main.go, internal/app.go, ...)
- **Incoming callers (L2):** 12 additional callers across 5 files (depth limit reached)
- **Outgoing calls:** NewEncoder, e.w.Flush
- **Implements:** codec.Encoder
- **References:** 8 sites across 5 files
...
### Dependency Relationships
cmd.main → NewEncoder → bufio.NewWriter
cmd.main → Encode → NewEncoder
Encode → Reset
### Entry Points to This Code
- cmd.main (cmd/main.go:14)
- app.Start (internal/app.go:31)
- loader.Load (internal/loader.go:55)
### Depth-limit Note
Call hierarchy stopped at 2 levels. 12 additional callers exist beyond
Level 2 for Encode — use /lsp-explore on specific symbols to drill deeper.
```Full three-layer verification after any change — LSP diagnostics + compiler build + test suite, ranked by severity. Use after completing any edit, refactor, or feature to confirm nothing is broken before committing.
> Requires the agent-lsp MCP server.
# lsp-verify: Three-Layer Verification
## When to Use
Run this skill after any significant change to verify correctness at every level:
- After editing source files (logic changes, refactors, new functions)
- After merging or rebasing branches
- After dependency updates or configuration changes
- Before committing or pushing code
## Input
- `workspace_dir` (required): absolute path to the workspace root (e.g. `/Users/you/code/myproject`)
- `changed_files` (optional): list of files you edited — used for targeted diagnostics
## Execution
### Pre-step: Test correlation (when `changed_files` is provided)
Before running the three layers, call `get_tests_for_file` for each changed
source file to build a source → test file map:
```
mcp__lsp__get_tests_for_file({ "file_path": "<changed/source/file>" })
```
Returns the test files that correspond to each source file. Store this map —
it is used in Layer 3 to focus failure analysis. If `changed_files` is unknown,
skip this step.
**Run all three layers in parallel** — they are independent and do not need to
be sequenced. Issue all three calls in the same message to minimize wall time.
### Layer 1: LSP Diagnostics
Call `mcp__lsp__get_diagnostics` with `file_path` set to each changed file.
`get_diagnostics` takes a file path, not a workspace directory.
Note: requires LSP to be initialized. If not yet running, call `start_lsp`
with the workspace root first.
```
mcp__lsp__get_diagnostics({ "file_path": "<path/to/changed/file>" })
```
Call once per changed file. If you don't know which files changed, call it on
the primary files touched in this session. Rank results by severity: errors
first, then warnings.
### Layer 2: Build
```
mcp__lsp__run_build({ "workspace_dir": "<workspace_dir>" })
```
Returns `{ "success": bool, "errors": [...] }`. A failed build means the code
does not compile. Build errors are blocking — must be resolved before shipping.
### Layer 3: Tests
```
mcp__lsp__run_tests({ "workspace_dir": "<workspace_dir>" })
```
Does NOT require `start_lsp`. Returns `{ "passed": bool, "failures": [...] }`.
**Large output warning:** `run_tests` on large repos can return hundreds of
thousands of characters and exceed the context window. If the result is saved
to a file rather than returned inline, do NOT attempt to read the whole file.
Instead, search it for failures:
```bash
grep -E "^(FAIL|--- FAIL)" <output_file>
```
Or scope tests to the correlated test files from the pre-step to avoid the
size issue entirely:
```bash
GOWORK=off go test -count=1 -short ./internal/mypackage/... 2>&1 | grep -E "FAIL|ok"
```
**Using test correlation:** If the pre-step produced a source → test file map,
cross-reference failing test names against that map. For each failure, note
whether it is in a correlated test file (directly covers the changed code) or
an unrelated test file (collateral failure from a shared dependency). This
distinction guides where to investigate first.
Test failures are blocking — they indicate regressions or unmet contracts.
## Output Format
After running all three layers, produce a structured report:
```
## Verification Report
### Layer 1: LSP Diagnostics
[CLEAN / N errors, M warnings]
<details if N > 0 or M > 0>
Errors:
- file:line - message
Warnings:
- file:line - message
</details>
### Layer 2: Build
[PASSED / FAILED - N errors]
<details if FAILED>
- error message (file:line)
</details>
### Layer 3: Tests
[PASSED / FAILED - N failures]
<details if FAILED>
- test name: message (file:line) [correlated / unrelated]
</details>
<if test correlation map exists>
Test files covering changed source:
changed/source/file.go → test/source_file_test.go
</if>
### Summary
Overall: CLEAN / NEEDS ATTENTION
Blocking issues: [errors that must be fixed before shipping]
```
- **CLEAN**: no errors in any layer (warnings are advisory only)
- **NEEDS ATTENTION**: one or more blocking issues found
## Blocking vs Advisory
| Layer | Errors | Warnings |
|-------|--------|----------|
| LSP Diagnostics | Blocking | Advisory |
| Build | All blocking | N/A |
| Tests | All blocking | N/A |
Build errors and test failures block shipping. LSP warnings and style
suggestions are advisory — document them but do not treat as blockers unless
they indicate logical errors.
## When Verification Passes: Optional Format
If all three layers are CLEAN and `changed_files` is known, offer to format
the changed files before committing:
```
mcp__lsp__format_document({ "file_path": "<changed-file>" })
```
Apply the returned `TextEdit[]` via `apply_edit` if non-empty. Run once per
changed file. Skip if the user did not request formatting.
---
## When Errors Are Found: Applying Code Actions
If Layer 1 returns errors, the LSP may offer quick fixes. For each error
location, call `get_code_actions` to surface available fixes:
```
mcp__lsp__get_code_actions({
"file_path": "<file>",
"line": <error line>,
"column": <error column>
})
```
Returns a list of available actions (e.g. "Add missing import", "Implement
interface methods", "Remove unused variable"). Pick the most appropriate one
and apply it:
```
mcp__lsp__apply_edit({
"file_path": "<file>",
"old_text": "<text to replace>",
"new_text": "<replacement>"
})
```
Or if the code action returns a `workspace_edit`, pass it directly to
`apply_edit` via the `workspace_edit` parameter.
After applying, **re-run Layer 1** on the affected file to confirm the error
is resolved before moving on. Do not apply multiple code actions in bulk
without verifying each one — they may interact.
**When to use:** Compile errors from missing imports, unimplemented interface
methods, or type mismatches often have one-click fixes available. Manual
reasoning is still required for logic errors.