Skip to content

Instantly share code, notes, and snippets.

@realgenekim
Last active March 10, 2026 09:48
Show Gist options
  • Select an option

  • Save realgenekim/7314172e7efd0fe4b98f3fe3a96d7a5c to your computer and use it in GitHub Desktop.

Select an option

Save realgenekim/7314172e7efd0fe4b98f3fe3a96d7a5c to your computer and use it in GitHub Desktop.
Setting up clojure-lsp (cclsp) and clojure-mcp for Claude Code — Clojure developer tooling

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

Setting up clojure-lsp for Claude Code via cclsp

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.

What clojure-lsp adds (beyond REPL and Grep)

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

Architecture

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.

Setup Steps

1. Install clojure-lsp native binary

brew install clojure-lsp/brew/clojure-lsp-native
# or upgrade if already installed:
brew upgrade clojure-lsp/brew/clojure-lsp-native

On Linux, download the native binary from clojure-lsp releases.

2. Install cclsp

npm install -g cclsp

3. Create cclsp.json in your project root

This 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"
    }
  ]
}

4. Create .mcp.json in your project root

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.

5. Restart Claude Code

# exit current session, then:
claude

On 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).

6. Add to .gitignore

Both config files contain hardcoded absolute paths, so they're machine-specific:

.mcp.json
cclsp.json
.lsp/

Alternative: cclsp setup wizard

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

Available Tools (12 total)

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

.mcp.json vs claude mcp add

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

LSP vs Grep vs REPL: When to Use What

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)

LSP Staleness: When You Need restart_server

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.

Real-World Validation

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."

Memory / Performance

  • 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

Resources

Setting up clojure-mcp (Clojure REPL) for Claude Code

clojure-mcp by Bruce Hauman is a full MCP server that gives Claude Code direct access to a Clojure nREPL. This means Claude can evaluate Clojure expressions, structurally edit s-expressions, search inside dependency jars, and repair paren mismatches — all without leaving the conversation.

What it provides

  • REPL evaluation — Claude runs Clojure code against your live nREPL, tests expressions, inspects runtime state
  • Structural editing — Edit Clojure forms by name (defn, def, ns) with syntax validation, avoiding the "unbalanced parens" problem
  • Dependency inspection — Search and read source code inside dependency jars on your classpath
  • Paren repair — Fix delimiter mismatches in Clojure files using parinfer

Available Tools

Tool What it does
clojure_eval Evaluate Clojure code in the nREPL
clojure_edit Replace/insert top-level forms (defn, def, ns, defmethod) with syntax validation
deps_grep Search for patterns inside dependency jar files
deps_read Read a file from inside a dependency jar
deps_list List all dependencies on the classpath with Maven coordinates
paren_repair Fix unbalanced delimiters in Clojure files using parinfer
list_nrepl_ports Discover running nREPL servers

Setup

Prerequisites

  • A Clojure project with deps.edn
  • An nREPL server running (or let clojure-mcp auto-start one)

1. Add the :mcp alias to your global deps.edn

Add to ~/.clojure/deps.edn:

:mcp {:extra-deps {io.github.bhauman/clojure-mcp {:git/tag "v0.1.4" :git/sha "xxxxxxx"}}
      :exec-fn clojure-mcp.main/start
      :exec-args {:config-profile :cli-assist}}

Check the clojure-mcp releases for the latest tag and sha.

2. Register as an MCP server with Claude Code

claude mcp add clojure-mcp -- \
  /bin/sh -c 'cd /path/to/your-project && clojure -Tmcp start :config-profile :cli-assist'

This registers clojure-mcp globally in ~/.claude/settings.json so it's available in all Claude Code sessions.

The :cli-assist profile disables tools that would be redundant with Claude Code's built-in capabilities (file editing, grep, etc.) and provides only the Clojure-specific tools listed above.

3. Start nREPL

clojure-mcp connects to a running nREPL. Start one in your project:

clojure -M:nrepl

Or let clojure-mcp auto-start nREPL by adding :start-nrepl-cmd:

claude mcp add clojure-mcp -- \
  /bin/sh -c 'cd /path/to/your-project && clojure -Tmcp start \
    :start-nrepl-cmd '\''["clojure" "-M:nrepl"]'\'' \
    :config-profile :cli-assist'

With :start-nrepl-cmd, no manual nREPL startup is needed — clojure-mcp launches it as a subprocess and auto-discovers the port.

4. Restart Claude Code

# exit current session, then:
claude

Claude Code will spawn clojure-mcp on startup, which connects to (or starts) your nREPL.

Usage Tips

  • Always use :reload when requiring namespaces in clojure_eval: (require 'my.ns :reload) — ensures you get the latest version of files
  • clojure_edit is the fallback for when Claude's built-in Edit tool fails on Clojure forms (matching exact whitespace in s-expressions is hard). It matches by form type + name instead of exact text.
  • Use deps_grep to explore libraries — e.g., search for usage patterns in Ring, Reitit, or any dependency on your classpath
  • paren_repair is your safety net — if an edit leaves a file with unbalanced delimiters, this fixes it automatically

Companion: clojure-mcp-light

clojure-mcp-light provides lightweight CLI tools that complement the full MCP server:

  • clj-nrepl-eval — CLI command to eval Clojure against a running nREPL via Bash
  • clj-paren-repair-claude-hook — Pre/PostToolUse hook that auto-fixes paren mismatches on every Write/Edit

Bruce Hauman recommends using both together: clojure-mcp for REPL eval and structural editing, clojure-mcp-light for paren repair hooks that catch errors at write time.

Resources

@realgenekim
Copy link
Author

realgenekim commented Mar 10, 2026

From @dazld, who updated the cclsp package 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 let blocks — 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 via apply_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.md tells it when to reach for them.

Setup

1. Install dependencies

brew install clojure-lsp/brew/clojure-lsp-native
npm install -g @dazld/cclsp

2. Create cclsp.json in your project root

{
  "servers": [
    {
      "extensions": ["clj", "cljs", "cljc", "edn"],
      "command": ["clojure-lsp"],
      "rootDir": "/absolute/path/to/your-project",
      "requestTimeout": 600000
    }
  ]
}

requestTimeout is in milliseconds. Default is 30000 (30s). Set higher for large projects where clojure-lsp needs more time to initialize.

3. Create .mcp.json in your project root

{
  "mcpServers": {
    "cclsp": {
      "command": "npx",
      "args": ["@dazld/cclsp"],
      "env": {
        "CCLSP_CONFIG_PATH": "/absolute/path/to/your-project/cclsp.json"
      }
    }
  }
}

4. Add to .gitignore

.mcp.json
cclsp.json
.lsp/

5. 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:

### Structural Code Edits (prefer LSP code actions)

When editing Clojure/ClojureScript code structurally — restructuring `let` blocks,
extracting functions, threading/unthreading, slurping/barfing, inlining symbols —
**prefer `apply_code_action` over manual Edit tool edits**. The LSP server understands
the AST and cannot produce unbalanced parentheses, while manual string edits in deeply
nested code are error-prone.

Use `get_code_actions` to discover what's available at a position, then
`apply_code_action` to apply by title. Key operations:

- **Let blocks**: `introduce-let`, `move-to-let`, `expand-let`, `inline-symbol`
- **Extraction**: `extract-function`, `extract-to-def`
- **Threading**: `thread-first-all`, `thread-last-all`, `unwind-all`
- **Paredit**: `forward-slurp`, `forward-barf`, `raise-sexp`
- **Namespace**: `clean-ns`, `add-missing-libspec`

Manual Edit is fine for simple changes (updating values, adding map keys, small additions).
Use LSP for anything that changes code structure.

Performance tuning for large projects

If clojure-lsp takes too long to start (many dependencies), create .lsp/config.edn with 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

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
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
execute_command Run clojure-lsp commands directly (low-level)
get_code_actions Discover available refactorings at a position
apply_code_action Apply a refactoring by title (preferred)
restart_server Restart LSP if it gets stuck

Code actions: the recommended workflow

Step 1: Discover

get_code_actions(file_path, line, character)

Returns a numbered list of what's available at that position:

1. Extract function       Kind: refactor.extract
2. Thread last all        Kind: refactor.rewrite
3. Clean namespace        Kind: source.organizeImports

Step 2: Apply

apply_code_action(file_path, line, character, title: "Extract function")

The tool fetches actions, finds the match, and applies the edits.

Range selection

Some actions (especially extract-function) need a range. Use end_line and end_character:

get_code_actions(
  file_path: "src/clj/my/ns.clj",
  line: 10, character: 5,
  end_line: 15, end_character: 30
)

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_action matches flexibly:

  1. Exact: "Clean namespace"
  2. Case-insensitive: "clean namespace"
  3. Partial substring: "extract" (matches "Extract function")

Why code actions over execute_command?

execute_command sends workspace/executeCommand directly — you must construct file URIs and 0-indexed positions manually, and some commands crash with IndexOutOfBoundsException due 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 with apply_code_action). The execute_command args are listed for reference but you rarely need them.

Namespace management

Command Description
clean-ns Sort requires, remove unused imports
add-missing-libspec Add missing require for unresolved symbol
add-missing-import Add import to namespace

Let block editing

Particularly useful — manipulates let bindings structurally, so parentheses can never break.

Command Description
introduce-let Wrap expression in a new let binding
move-to-let Move expression into an existing let binding
expand-let Expand let scope to wrap more code
inline-symbol Remove a let binding, inline everywhere

Extraction and function creation

Command Description
extract-function Extract expression to a new defn
extract-to-def Extract expression to a def
create-function Create function from usage example
promote-fn Promote #() to fn, or fn to defn
demote-fn Demote fn to #()

Threading

Command Description
thread-first-all Thread entire expression with ->
thread-last-all Thread entire expression with ->>
unwind-all Fully unwind a threaded expression

Structural editing (paredit)

Command Description
forward-slurp Slurp next expression into current form
forward-barf Barf last expression out of current form
raise-sexp Replace parent form with current expression

Collection manipulation

Command Description
cycle-coll Cycle between #{}, {}, [], ()
sort-clauses Sort map/vector/list/set/clauses

Conditionals

Command Description
if->cond-refactor Refactor if to cond
cond->if-refactor Refactor cond to if

Other

Command Description
cycle-privacy Toggle between defn and defn-
destructure-keys Convert map access to destructuring
restructure-keys Convert destructuring back to map access
drag-forward Move form forward in sequence
drag-backward Move form backward in sequence
create-test Create test for function at cursor

Tips

  • Use code actions for refactoring. They handle position validation and argument construction automatically.
  • Let block operations are safe structural edits — great for Claude Code when editing deeply nested code where parentheses are tricky.
  • Threading operations are similarly safe — they transform code structure without risking paren mistakes.
  • extract-function via code actions requires selecting a range covering complete expressions at the same nesting level.
  • clean-ns works from any position in the file — it reorganizes the entire namespace declaration.

When to restart_server

After git mv, file renames, or deps.edn changes (new dependencies).

Resources

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment