After watching the Clojure/conj 2025 talk on ECA, I was inspired to try using the clojure-lsp plugin. I stumbled upon the cclsp MCP server, which seemed like the ideal way to use this with Claude Code.
Enclosed below are all the instructions necessary to get them both up and running.
My high-level impressions:
- Claude Code seems to love having cclsp around when doing large refactorings -- having done several, some with and some without, those with cclsp and clojure-lsp seem to go more smoothly
I'd love to hear your thoughts!
All of these docs were generated by Claude Code.
Cheers, Gene
cclsp is an MCP server that wraps any LSP server and exposes its capabilities as tools to Claude Code. For Clojure developers, this means you get semantic code navigation, cross-project refactoring, and call hierarchy analysis — all available to Claude as first-class tools.
Claude Code already has text search (Grep/Glob) and REPL tools (via clojure-mcp). clojure-lsp fills the semantic gap:
| Capability | Impact | Notes |
|---|---|---|
| Find references | HIGH | "Who calls this fn?" — precise, not regex guessing |
| Rename symbol | HIGH | Across all files, semantically correct |
| Go to definition | MEDIUM | Jump to where a symbol is defined |
| Call hierarchy | MEDIUM | Trace call chains up/down |
| Diagnostics | MEDIUM | Catch errors without eval, linting |
Claude Code starts
└─ spawns cclsp (MCP server, stays running)
└─ spawns clojure-lsp (LSP server, stays running)
└─ analyzes your classpath, builds index
All three processes run for the duration of your Claude Code session. cclsp routes tool calls to the correct LSP server by file path.
brew install clojure-lsp/brew/clojure-lsp-native
# or upgrade if already installed:
brew upgrade clojure-lsp/brew/clojure-lsp-nativeOn Linux, download the native binary from clojure-lsp releases.
npm install -g cclspThis tells cclsp which LSP servers to run and for which projects:
{
"servers": [
{
"extensions": ["clj", "cljs", "cljc", "edn"],
"command": ["clojure-lsp"],
"rootDir": "/path/to/your-project"
}
]
}For multiple projects, add more entries — each gets its own clojure-lsp process:
{
"servers": [
{
"extensions": ["clj", "cljs", "cljc", "edn"],
"command": ["clojure-lsp"],
"rootDir": "/path/to/project-a"
},
{
"extensions": ["clj", "cljs", "cljc", "edn"],
"command": ["clojure-lsp"],
"rootDir": "/path/to/project-b"
}
]
}This tells Claude Code to spawn cclsp as an MCP server:
{
"mcpServers": {
"cclsp": {
"command": "cclsp",
"env": {
"CCLSP_CONFIG_PATH": "/path/to/your-project/cclsp.json"
}
}
}
}Scope: .mcp.json is project-scoped (only active when Claude Code runs in this directory). Alternative: add to ~/.claude/settings.json for global scope.
# exit current session, then:
claudeOn restart, Claude Code picks up .mcp.json, spawns cclsp, which spawns clojure-lsp. First run takes ~10-30s for clojure-lsp to analyze the classpath (cached in .lsp/.cache/ after that — subsequent starts are ~2-5s).
Both config files contain hardcoded absolute paths, so they're machine-specific:
.mcp.json
cclsp.json
.lsp/
cclsp has a setup wizard that auto-detects languages and generates config:
npx cclsp@latest setup # project-level: .claude/cclsp.json
npx cclsp@latest setup --user # user-level: ~/.config/claude/cclsp.json| Tool | What it does |
|---|---|
find_definition |
Jump to where a symbol is defined |
find_references |
Find all usages across workspace |
find_workspace_symbols |
Search symbols by name pattern |
get_hover |
Type info + docstrings at a position |
get_diagnostics |
Errors/warnings for a file |
find_implementation |
Find interface implementations |
prepare_call_hierarchy |
Set up call hierarchy queries |
get_incoming_calls |
Who calls this function? |
get_outgoing_calls |
What does this function call? |
rename_symbol |
Rename across entire workspace |
rename_symbol_strict |
Rename at exact position (when multiple matches) |
restart_server |
Restart LSP if it gets stuck |
Both register an MCP server with Claude Code. The difference is scope:
claude mcp add |
.mcp.json |
|
|---|---|---|
| Where stored | ~/.claude/settings.json |
./mcp.json in project root |
| Scope | All projects, all directories | This project directory only |
| Git-trackable | No (user home dir) | Yes (but has absolute paths) |
| Add/remove | claude mcp add/remove |
Edit/delete the file |
| Scenario | Best tool | Why |
|---|---|---|
| "Where is X defined?" | LSP find_definition |
One call, exact location |
| "Who uses X?" | LSP find_references |
Precise refs, not text matches |
| "What type/docs does X have?" | LSP get_hover |
No namespace loading needed |
| "What calls what?" | LSP get_outgoing_calls |
Static call graph |
| "Find pattern in any file" | Grep | Works on all file types, regex |
| "Search comments/strings" | Grep | LSP ignores non-code text |
| "Test a function" | REPL | Can evaluate, not just read |
| "Runtime value of X?" | REPL | Only tool that executes code |
| "Rename across project" | LSP rename_symbol |
Safe multi-file rename |
| "Find text in .md, .json, .js" | Grep | LSP only indexes Clojure |
Key insight: LSP, Grep, and REPL are complementary:
- LSP = understands code semantically (definitions, references, types, call graphs)
- Grep = finds text anywhere (comments, strings, non-Clojure files, regex)
- REPL = runs code (test expressions, runtime state, live values)
| Change type | LSP auto-detects? | Need restart? |
|---|---|---|
| Edit file contents (save) | Yes | No |
| Add new file | Yes | No |
git mv / rename files |
No | Yes |
| Delete files | Partial (may show ghosts) | Yes to be safe |
Change deps.edn (new deps) |
No | Yes (new classpath) |
Rule: After any git mv or file rename, always run restart_server. Re-indexing takes ~3 seconds.
Tested on a db.clj refactoring (729 lines split into 5 domain files):
- An earlier attempt using Grep to find all callers missed 5 out of 12 callers (42% miss rate)
- With LSP
find_references, zero missed references across 4 extractions
| Grep | LSP find_references |
|
|---|---|---|
| Accuracy | Missed 5/12 callers | 0 missed across 4 extractions |
| False positives | Matches in comments, strings | Only real code references |
| Speed | Fast per query, but needs iteration | One call, complete answer |
| Setup cost | Zero | 2 minutes (if tools installed) |
Bottom line: For safe refactoring, LSP find_references is essential. Grep is fine for exploration but not reliable enough for "move this function and update all callers."
- 1 project: ~300-500MB RAM total (cclsp + clojure-lsp)
- 3 projects: ~1-1.5GB
- GraalVM native binary helps — JVM version would be ~2x
- All processes stay resident for instant responses
- Exit Claude Code → kills cclsp → kills all clojure-lsp children
- cclsp — MCP wrapper for LSP servers
- clojure-lsp — Language Server Protocol for Clojure
- Claude Code MCP docs
From @dazld, who updated the
cclsppackage above — woo!!! 🙏 ❤️The new repo: https://github.com/dazld/cclsp
===
Giving Claude Code clojure-lsp superpowers (via cclsp)
cclsp is an MCP server that bridges clojure-lsp to Claude Code — giving it semantic code intelligence: find references, go to definition, rename symbol, diagnostics, call hierarchy, and AST-aware refactoring via code actions.
Why code actions matter for LLMs
Claude Code edits files via string replacement. For simple changes (updating a value, adding a map key) that works fine. But for structural edits in Clojure — extracting functions, threading expressions, restructuring
letblocks — string manipulation in deeply nested s-expressions is error-prone. One misplaced paren and the whole file breaks.Code actions solve this. The LSP server understands the AST, validates cursor positions, and produces correct edits. Claude Code discovers what's available via
get_code_actions, then applies by title viaapply_code_action. No manual paren-counting required.But Claude Code won't use tools it doesn't know about. Adding cclsp to your project gives Claude the tools — adding guidance to your
CLAUDE.mdtells it when to reach for them.Setup
1. Install dependencies
2. Create
cclsp.jsonin your project root{ "servers": [ { "extensions": ["clj", "cljs", "cljc", "edn"], "command": ["clojure-lsp"], "rootDir": "/absolute/path/to/your-project", "requestTimeout": 600000 } ] }requestTimeoutis in milliseconds. Default is 30000 (30s). Set higher for large projects where clojure-lsp needs more time to initialize.3. Create
.mcp.jsonin your project root{ "mcpServers": { "cclsp": { "command": "npx", "args": ["@dazld/cclsp"], "env": { "CCLSP_CONFIG_PATH": "/absolute/path/to/your-project/cclsp.json" } } } }4. Add to
.gitignore5. Restart Claude Code
First run takes ~10-30s for clojure-lsp to index (cached after that).
Add guidance to your CLAUDE.md
This is the critical step most people skip. Without it, Claude Code has the tools but doesn't know when to use them. Add something like this to your project's
CLAUDE.md:Performance tuning for large projects
If clojure-lsp takes too long to start (many dependencies), create
.lsp/config.ednwith a source-only classpath to skip JAR analysis:{:copy-kondo-configs? false :java {:jdk-source-uri nil :decompile-jar-as-project? false} :stubs {:generation {:namespaces #{}}} :source-paths-ignore-regex ["target" "node_modules" "resources"] :project-specs [{:project-path "deps.edn" :classpath-cmd ["echo" "src/clj:src/cljs:src/cljc"]}]}This trades go-to-definition into library source for fast startup. All refactoring, diagnostics, and navigation within project code still work.
Available MCP tools
find_definitionfind_referencesfind_workspace_symbolsget_hoverget_diagnosticsfind_implementationprepare_call_hierarchyget_incoming_callsget_outgoing_callsrename_symbolrename_symbol_strictexecute_commandget_code_actionsapply_code_actionrestart_serverCode actions: the recommended workflow
Step 1: Discover
Returns a numbered list of what's available at that position:
Step 2: Apply
The tool fetches actions, finds the match, and applies the edits.
Range selection
Some actions (especially
extract-function) need a range. Useend_lineandend_character:Important: The range must cover complete expressions at the same nesting level. Crossing nesting levels gives "Expressions must be at the same level" errors.
Title matching
apply_code_actionmatches flexibly:"Clean namespace""clean namespace""extract"(matches "Extract function")Why code actions over execute_command?
execute_commandsendsworkspace/executeCommanddirectly — you must construct file URIs and 0-indexed positions manually, and some commands crash withIndexOutOfBoundsExceptiondue to position sensitivity. Code actions have the server validate everything first.Refactoring command reference
All of these are available via code actions (discover with
get_code_actions, apply withapply_code_action). Theexecute_commandargs are listed for reference but you rarely need them.Namespace management
clean-nsadd-missing-libspecadd-missing-importLet block editing
Particularly useful — manipulates let bindings structurally, so parentheses can never break.
introduce-letmove-to-letexpand-letinline-symbolExtraction and function creation
extract-functionextract-to-defcreate-functionpromote-fn#()tofn, orfntodefndemote-fnfnto#()Threading
thread-first-all->thread-last-all->>unwind-allStructural editing (paredit)
forward-slurpforward-barfraise-sexpCollection manipulation
cycle-coll#{},{},[],()sort-clausesConditionals
if->cond-refactoriftocondcond->if-refactorcondtoifOther
cycle-privacydefnanddefn-destructure-keysrestructure-keysdrag-forwarddrag-backwardcreate-testTips
extract-functionvia code actions requires selecting a range covering complete expressions at the same nesting level.clean-nsworks from any position in the file — it reorganizes the entire namespace declaration.When to restart_server
After
git mv, file renames, ordeps.ednchanges (new dependencies).Resources