Skip to content

Conversation

@bhauman
Copy link
Owner

@bhauman bhauman commented Nov 20, 2025

Summary

This PR is a major refactor of the nREPL evaluation code to make it more robust.

Changes

  • Removed needless multiplexing: Previously, evaluations were unnecessarily multiplexed. This has been simplified so that individual tool evaluations now open a socket and send the eval directly.

  • Cleaner architecture: The refactored code is more straightforward and easier to reason about.

Motivation

This work lays the groundwork for the next step: making ClojureMCP optionally start without a port and allowing more dynamic REPL interactions by making the port an optional parameter in the eval tool.

Summary by CodeRabbit

  • New Features

    • Stronger config loading with pre-merge validation, clearer path reporting and debug logs
    • Dev tooling now enables logging for the development pipeline
  • Bug Fixes

    • Improved error, timeout and interrupt handling for remote code evaluation
    • More consistent evaluation outputs and error reporting
  • Refactor

    • Simplified evaluation/session model using symbolic session-type identifiers
    • Removed background polling during connection lifecycle
  • Tests

    • Updated tests to reflect polling removal and new evaluation semantics

✏️ Tip: You can customize this high-level summary in your review settings.

- Replaced long-lived nREPL connections and async polling with ephemeral, blocking connections per evaluation.
- `clojure-mcp.nrepl`:
    - Removed `start-polling`, `stop-polling`, and callback logic.
    - Added blocking `eval-code` taking a `:session-type` (e.g., `:default`, `:tools`, `:shadow`, `:figwheel`).
    - State is now a simple map tracking session IDs by port and type.
    - Added `current-ns` helper to track namespace by session type.
- Tools updates:
    - `eval`: Updated to use blocking `eval-code` and accept `:session-type`.
    - `bash`: Removed custom session creation; uses `:tools` session type.
    - `figwheel` & `shadow`: Updated to use `:session-type` for dedicated sessions.
- Core & CLI:
    - `clojure-mcp.core`: Removed polling initialization.
    - `prompt-cli`: Removed polling initialization.
- Tests:
    - Updated `eval` tool tests to reflect API changes.
@coderabbitai
Copy link

coderabbitai bot commented Nov 20, 2025

Walkthrough

Refactors nREPL interaction from polling/callbacks to synchronous, per-connection evals with symbolic session-types; enhances config.load-config to explicitly load → validate (home/project) → merge → process with canonical-path reporting and debug logs; updates tools/tests to propagate :session-type and removes polling lifecycle.

Changes

Cohort / File(s) Summary
Config System
src/clojure_mcp/config.clj
Split load-config into explicit stages: load home/project configs, validate each (when present/non-empty) using canonical paths for errors, merge (home defaults + project overrides), then process final config; add debug logging around existence/validation and processed-config.
nREPL Core
src/clojure_mcp/nrepl.clj
Replace callback/polling model with a per-port stateful model; change signatures/behavior of eval-code, interrupt, and current-ns; add get/set session helpers; evals execute synchronously and return response sequences.
Core & CLI
src/clojure_mcp/core.clj, src/clojure_mcp/prompt_cli.clj
Remove start/stop of nREPL polling from startup/shutdown and prompt runner; prompt CLI disables logging pre-connection via logging/configure-logging!.
Dialects & REPL Helpers
src/clojure_mcp/dialects.clj
Switch to nrepl-core eval flow (nrepl/eval-code), normalize results (strip quotes when needed), rename project-dirproject-dir-arg, and adjust helper calls to session-type semantics.
Eval Tooling
src/clojure_mcp/tools/eval/core.clj, src/clojure_mcp/tools/eval/tool.clj
Add private process-responses to collate outputs/errors; change evaluate-code to future-based sync evals with timeout/interrupt handling using connection/session helpers; replace :session keys with :session-type in tool config/inputs.
Bash Tooling
src/clojure_mcp/tools/bash/core.clj, src/clojure_mcp/tools/bash/tool.clj
Remove create-bash-over-nrepl-session; change execute-bash-command-nrepl to expect :session-type; tool maps and execute-tool now propagate :session-type instead of session objects.
Figwheel & Shadow Examples
src/clojure_mcp/tools/figwheel/tool.clj, src/clojure_mcp/main_examples/shadow_main.clj
Start functions now return symbolic session-types (:figwheel, :shadow) instead of session objects; evals call nrepl/eval-code with :session-type; start flows simplified and wrapped with error logging.
Tests & Fixtures
test/... (tools/eval/*, tools/bash/*, tools/project/*, tools/test_utils.clj, repl_tools/*, ...)
Remove polling start/stop in fixtures; call nrepl/eval-code without identity callbacks; update mocks and assertions to expect :session-type propagation; adjust teardown behavior in a few fixtures.
Dev Script
clojure-mcp-dev.sh
Enable logging for dev-mcp invocation by passing :enable-logging? true in the background pipeline command.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant MCP as MCP Service
    participant NRepl as nREPL

    rect rgb(245,240,255)
    Note over Client,MCP: Previous (Polling + Callbacks)
    Client->>MCP: eval-code(service, code, callback)
    activate MCP
    MCP->>NRepl: send request (long-lived/polled)
    activate NRepl
    NRepl-->>MCP: streaming responses (async)
    MCP->>MCP: accumulate via polling callbacks
    MCP-->>Client: invoke callback with results
    deactivate NRepl
    deactivate MCP
    end

    rect rgb(235,255,240)
    Note over Client,MCP: New (Synchronous, session-type)
    Client->>MCP: eval-code(service, code, {:session-type k})
    activate MCP
    MCP->>NRepl: open fresh connection + eval
    activate NRepl
    NRepl-->>MCP: collect responses (blocking)
    deactivate NRepl
    MCP->>MCP: process-responses & update per-port state
    MCP-->>Client: return response sequence / processed result
    deactivate MCP
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Focus reviews on:
    • src/clojure_mcp/nrepl.clj (new per-port state model, signature changes, concurrency/closure of connections).
    • src/clojure_mcp/tools/eval/core.clj (timeout, interrupt, exception handling, resource cleanup).
    • Consistency of :session-type propagation across tools and tests (bash, eval, figwheel, shadow, tests).

Possibly related PRs

Poem

🐰 I hopped through code with nimble paws,
Polling gone, now sync applause.
Session-types wear tiny crowns,
Ports keep state in tidy towns.
Logs and configs neat and spry — a rabbit's happy bounce, bye-bye!

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Refactor nREPL evaluation for robustness and simplicity' accurately summarizes the main change: refactoring the nREPL evaluation system to use a simpler, stateless connection model.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch nrepl-refactor

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae5f83b and 8a80562.

📒 Files selected for processing (8)
  • clojure-mcp-dev.sh (1 hunks)
  • src/clojure_mcp/dialects.clj (4 hunks)
  • src/clojure_mcp/main_examples/shadow_main.clj (2 hunks)
  • src/clojure_mcp/nrepl.clj (1 hunks)
  • src/clojure_mcp/tools/bash/tool.clj (3 hunks)
  • src/clojure_mcp/tools/eval/core.clj (3 hunks)
  • test/clojure_mcp/repl_tools/test_utils.clj (2 hunks)
  • test/clojure_mcp/tools/bash/session_test.clj (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
src/**/*.{clj,cljc}

📄 CodeRabbit inference engine (CLAUDE.md)

src/**/*.{clj,cljc}: Use :require with ns aliases for imports (e.g., [clojure.string :as string])
Include clear tool :description for LLM guidance
Validate inputs and provide helpful error messages in MCP tools
Return structured data with both result and error status in MCP tools
Maintain atom-based state for consistent service access in MCP tools

Files:

  • src/clojure_mcp/tools/eval/core.clj
  • src/clojure_mcp/main_examples/shadow_main.clj
  • src/clojure_mcp/dialects.clj
  • src/clojure_mcp/nrepl.clj
  • src/clojure_mcp/tools/bash/tool.clj
**/*.{clj,cljc}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{clj,cljc}: Use kebab-case for vars/functions; end predicates with ? (e.g., is-top-level-form?)
Use try/catch with specific exception handling; use atom for tracking errors
Use 2-space indentation and maintain whitespace in edited forms
Align namespaces with directory structure (e.g., clojure-mcp.repl-tools)

Files:

  • src/clojure_mcp/tools/eval/core.clj
  • src/clojure_mcp/main_examples/shadow_main.clj
  • test/clojure_mcp/tools/bash/session_test.clj
  • test/clojure_mcp/repl_tools/test_utils.clj
  • src/clojure_mcp/dialects.clj
  • src/clojure_mcp/nrepl.clj
  • src/clojure_mcp/tools/bash/tool.clj
test/**/*.{clj,cljc}

📄 CodeRabbit inference engine (CLAUDE.md)

Use deftest with descriptive names; testing for subsections; is for assertions in tests

Files:

  • test/clojure_mcp/tools/bash/session_test.clj
  • test/clojure_mcp/repl_tools/test_utils.clj
🧠 Learnings (9)
📓 Common learnings
Learnt from: CR
Repo: bhauman/clojure-mcp PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-02T20:23:28.499Z
Learning: Applies to src/**/*.{clj,cljc} : Maintain atom-based state for consistent service access in MCP tools
📚 Learning: 2025-08-02T20:23:28.499Z
Learnt from: CR
Repo: bhauman/clojure-mcp PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-02T20:23:28.499Z
Learning: Applies to src/**/*.{clj,cljc} : Maintain atom-based state for consistent service access in MCP tools

Applied to files:

  • clojure-mcp-dev.sh
  • src/clojure_mcp/main_examples/shadow_main.clj
  • test/clojure_mcp/repl_tools/test_utils.clj
  • src/clojure_mcp/dialects.clj
  • src/clojure_mcp/nrepl.clj
  • src/clojure_mcp/tools/bash/tool.clj
📚 Learning: 2025-08-02T20:23:28.499Z
Learnt from: CR
Repo: bhauman/clojure-mcp PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-02T20:23:28.499Z
Learning: Applies to src/**/*.{clj,cljc} : Validate inputs and provide helpful error messages in MCP tools

Applied to files:

  • clojure-mcp-dev.sh
  • src/clojure_mcp/dialects.clj
📚 Learning: 2025-08-18T00:39:24.837Z
Learnt from: hugoduncan
Repo: bhauman/clojure-mcp PR: 86
File: src/clojure_mcp/subprocess.clj:0-0
Timestamp: 2025-08-18T00:39:24.837Z
Learning: In the clojure-mcp project, the user prefers to only parse stdout for nREPL port discovery in the subprocess.clj module, and explicitly does not want to parse stderr for port information.

Applied to files:

  • clojure-mcp-dev.sh
  • src/clojure_mcp/dialects.clj
📚 Learning: 2025-08-02T20:23:28.499Z
Learnt from: CR
Repo: bhauman/clojure-mcp PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-02T20:23:28.499Z
Learning: Applies to src/**/*.{clj,cljc} : Return structured data with both result and error status in MCP tools

Applied to files:

  • clojure-mcp-dev.sh
  • src/clojure_mcp/tools/eval/core.clj
📚 Learning: 2025-08-02T20:23:28.499Z
Learnt from: CR
Repo: bhauman/clojure-mcp PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-02T20:23:28.499Z
Learning: Applies to src/**/*.{clj,cljc} : Include clear tool `:description` for LLM guidance

Applied to files:

  • clojure-mcp-dev.sh
📚 Learning: 2025-08-02T20:23:28.499Z
Learnt from: CR
Repo: bhauman/clojure-mcp PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-02T20:23:28.499Z
Learning: Applies to test/**/*.{clj,cljc} : Use `deftest` with descriptive names; `testing` for subsections; `is` for assertions in tests

Applied to files:

  • test/clojure_mcp/repl_tools/test_utils.clj
📚 Learning: 2025-08-02T20:23:28.499Z
Learnt from: CR
Repo: bhauman/clojure-mcp PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-02T20:23:28.499Z
Learning: Applies to **/*.{clj,cljc} : Align namespaces with directory structure (e.g., `clojure-mcp.repl-tools`)

Applied to files:

  • test/clojure_mcp/repl_tools/test_utils.clj
  • src/clojure_mcp/dialects.clj
📚 Learning: 2025-08-02T20:23:28.499Z
Learnt from: CR
Repo: bhauman/clojure-mcp PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-02T20:23:28.499Z
Learning: Applies to src/**/*.{clj,cljc} : Use `:require` with ns aliases for imports (e.g., `[clojure.string :as string]`)

Applied to files:

  • src/clojure_mcp/dialects.clj

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

- Updated  and  to reflect the removal of direct session creation and the use of .
- Updated  and  to remove obsolete polling calls and use the new  signature.
- Formatted modified test files with Usage: clj-paren-repair FILE [FILE ...]

Fix delimiter errors and format Clojure files.

Features enabled by default:
  - Delimiter error detection and repair
  - cljfmt formatting

Options:
  -h, --help    Show this help message.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/clojure_mcp/dialects.clj (1)

95-106: Missing clojure.string require and alias causes a runtime error

fetch-project-directory calls clojure.string/replace at Line 105, but clojure.string is not required in this namespace. This will fail at compile or runtime.

Also, project guidelines prefer :require with an alias (e.g. [clojure.string :as string]) rather than fully qualified calls.

Suggested fix:

-(ns clojure-mcp.dialects
-  "Handles environment-specific behavior for different nREPL dialects.
-   ...
-  (:require [clojure.edn :as edn]
-            [clojure.java.io :as io]
-            [taoensso.timbre :as log]
-            [nrepl.core :as nrepl-core]
-            [clojure-mcp.nrepl :as nrepl]
-            [clojure-mcp.utils.file :as file-utils]))
+(ns clojure-mcp.dialects
+  "Handles environment-specific behavior for different nREPL dialects.
+   ...
+  (:require [clojure.edn :as edn]
+            [clojure.java.io :as io]
+            [clojure.string :as string]
+            [taoensso.timbre :as log]
+            [nrepl.core :as nrepl-core]
+            [clojure-mcp.nrepl :as nrepl]
+            [clojure-mcp.utils.file :as file-utils]))
...
-      (if (and (vector? raw-result) (= 1 (count raw-result)) (string? (first raw-result)))
-        (clojure.string/replace (first raw-result) #"^\"|\"$" "")
+      (if (and (vector? raw-result) (= 1 (count raw-result)) (string? (first raw-result)))
+        (string/replace (first raw-result) #"^\"|\"$" "")
         raw-result))))
🧹 Nitpick comments (8)
src/clojure_mcp/prompt_cli.clj (1)

226-229: Global logging disable in run-prompt is appropriate for CLI, but note side effects

Disabling logging via logging/configure-logging! before connecting keeps stdout clean for CLI usage; if run-prompt is ever reused from a long-lived REPL or host app, consider an option or temporary override so logging isn’t permanently suppressed.

src/clojure_mcp/config.clj (1)

112-155: Config load/validate/merge pipeline is clearer; consider behavior for missing CLI config files

The refactored load-config (load → validate per-file → merge → process → debug log) is easier to reason about and plays well with the schema error handling in core.clj. One behavioral question: when cli-config-file is provided but the file doesn’t exist, it’s currently treated as {} and only logged as exists: false; if a user explicitly passes --config you may want to treat a missing file as an error instead of silently ignoring it.

src/clojure_mcp/tools/bash/tool.clj (1)

24-40: Session-type wiring for bash-over-nREPL is consistent with the new model

Deriving session-type as :tools when bash-over-nrepl is enabled and threading it via :nrepl-session-type keeps the tool config simple while preserving atom-based nREPL state. If you later add more bash session variants, consider making the session-type configurable instead of hard-coded.

src/clojure_mcp/tools/bash/core.clj (1)

168-187: nREPL bash executor correctly propagates :session-type

Destructuring session-type and conditionally adding :session-type into the evaluate-code options hooks bash-over-nREPL into the new session-type model without altering the existing EDN/timeout logic. As a tiny nit, the log key :nrelp-eval-output-map is misspelled if you ever touch that logging again.

src/clojure_mcp/tools/figwheel/tool.clj (2)

10-32: Figwheel startup now uses :session-type :figwheel; consider surfacing hard failures

Calling nrepl/eval-code with :session-type :figwheel and returning :figwheel from start-figwheel cleanly hooks figwheel into the new session-type model. Right now, even if startup throws, you just log and still return :figwheel; you might want to rethrow (or signal failure) so that a completely broken figwheel start is visible to the caller instead of only in logs.


70-79: execute-tool correctly uses session-type but can simplify input handling

Asserting non-nil session-type and passing it to both current-ns and evaluate-with-repair ensures all figwheel evals stay on the dedicated session. Given that inputs should already be normalized by validate-inputs, the assert (:code inputs) plus fallback to (get inputs "code") is slightly redundant; either rely solely on :code or relax the assertion if you truly expect string-keyed inputs here.

src/clojure_mcp/main_examples/shadow_main.clj (1)

33-48: Guard against missing :shadow-build to avoid opaque NPE

(name shadow-build) will throw if :shadow-build is absent or nil in config, giving an unhelpful NullPointerException. Given this is user‑facing config, it would be better to validate and fail with a clear message before building start-code.

Consider something like:

-(defn start-shadow-repl [nrepl-client-atom {:keys [shadow-build shadow-watch]}]
-  (let [start-code (format
+(defn start-shadow-repl [nrepl-client-atom {:keys [shadow-build shadow-watch] :as config}]
+  (when-not shadow-build
+    (throw (ex-info "Missing :shadow-build in Shadow CLJS config"
+                    {:config config})))
+  (let [start-code (format
     ...

This keeps failures explicit and aligned with the “validate inputs and provide helpful error messages in MCP tools” guideline.

src/clojure_mcp/nrepl.clj (1)

6-15: State initialization matches atom-based model but assumes callers always use create

create attaches an atom under ::state with a per-port :ports map, which fits the “atom-based state for consistent service access” guideline. Just be aware all public entry points (eval-code, interrupt, current-ns, describe) assume ::state is present—swap! on (get-state service) will blow up if someone bypasses create.

If you expect external callers to sometimes construct the service map themselves, consider a small helper that asserts ::state is present and throws an ex-info with a helpful message instead of an NPE.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5dae6ed and 62bfe87.

📒 Files selected for processing (13)
  • src/clojure_mcp/config.clj (1 hunks)
  • src/clojure_mcp/core.clj (1 hunks)
  • src/clojure_mcp/dialects.clj (4 hunks)
  • src/clojure_mcp/main_examples/shadow_main.clj (2 hunks)
  • src/clojure_mcp/nrepl.clj (1 hunks)
  • src/clojure_mcp/prompt_cli.clj (2 hunks)
  • src/clojure_mcp/tools/bash/core.clj (2 hunks)
  • src/clojure_mcp/tools/bash/tool.clj (3 hunks)
  • src/clojure_mcp/tools/eval/core.clj (3 hunks)
  • src/clojure_mcp/tools/eval/tool.clj (2 hunks)
  • src/clojure_mcp/tools/figwheel/tool.clj (2 hunks)
  • test/clojure_mcp/tools/eval/core_test.clj (1 hunks)
  • test/clojure_mcp/tools/eval/tool_test.clj (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
src/**/*.{clj,cljc}

📄 CodeRabbit inference engine (CLAUDE.md)

src/**/*.{clj,cljc}: Use :require with ns aliases for imports (e.g., [clojure.string :as string])
Include clear tool :description for LLM guidance
Validate inputs and provide helpful error messages in MCP tools
Return structured data with both result and error status in MCP tools
Maintain atom-based state for consistent service access in MCP tools

Files:

  • src/clojure_mcp/config.clj
  • src/clojure_mcp/prompt_cli.clj
  • src/clojure_mcp/dialects.clj
  • src/clojure_mcp/tools/bash/core.clj
  • src/clojure_mcp/tools/eval/tool.clj
  • src/clojure_mcp/core.clj
  • src/clojure_mcp/tools/eval/core.clj
  • src/clojure_mcp/tools/bash/tool.clj
  • src/clojure_mcp/tools/figwheel/tool.clj
  • src/clojure_mcp/main_examples/shadow_main.clj
  • src/clojure_mcp/nrepl.clj
**/*.{clj,cljc}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{clj,cljc}: Use kebab-case for vars/functions; end predicates with ? (e.g., is-top-level-form?)
Use try/catch with specific exception handling; use atom for tracking errors
Use 2-space indentation and maintain whitespace in edited forms
Align namespaces with directory structure (e.g., clojure-mcp.repl-tools)

Files:

  • src/clojure_mcp/config.clj
  • src/clojure_mcp/prompt_cli.clj
  • src/clojure_mcp/dialects.clj
  • src/clojure_mcp/tools/bash/core.clj
  • src/clojure_mcp/tools/eval/tool.clj
  • src/clojure_mcp/core.clj
  • src/clojure_mcp/tools/eval/core.clj
  • test/clojure_mcp/tools/eval/tool_test.clj
  • src/clojure_mcp/tools/bash/tool.clj
  • src/clojure_mcp/tools/figwheel/tool.clj
  • src/clojure_mcp/main_examples/shadow_main.clj
  • test/clojure_mcp/tools/eval/core_test.clj
  • src/clojure_mcp/nrepl.clj
test/**/*.{clj,cljc}

📄 CodeRabbit inference engine (CLAUDE.md)

Use deftest with descriptive names; testing for subsections; is for assertions in tests

Files:

  • test/clojure_mcp/tools/eval/tool_test.clj
  • test/clojure_mcp/tools/eval/core_test.clj
🧠 Learnings (8)
📓 Common learnings
Learnt from: CR
Repo: bhauman/clojure-mcp PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-02T20:23:28.499Z
Learning: Applies to src/**/*.{clj,cljc} : Maintain atom-based state for consistent service access in MCP tools
📚 Learning: 2025-08-02T20:23:28.499Z
Learnt from: CR
Repo: bhauman/clojure-mcp PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-02T20:23:28.499Z
Learning: Applies to src/**/*.{clj,cljc} : Validate inputs and provide helpful error messages in MCP tools

Applied to files:

  • src/clojure_mcp/config.clj
  • src/clojure_mcp/prompt_cli.clj
  • src/clojure_mcp/dialects.clj
  • src/clojure_mcp/tools/eval/core.clj
📚 Learning: 2025-08-18T00:39:24.837Z
Learnt from: hugoduncan
Repo: bhauman/clojure-mcp PR: 86
File: src/clojure_mcp/subprocess.clj:0-0
Timestamp: 2025-08-18T00:39:24.837Z
Learning: In the clojure-mcp project, the user prefers to only parse stdout for nREPL port discovery in the subprocess.clj module, and explicitly does not want to parse stderr for port information.

Applied to files:

  • src/clojure_mcp/prompt_cli.clj
  • src/clojure_mcp/dialects.clj
  • src/clojure_mcp/core.clj
📚 Learning: 2025-08-02T20:23:28.499Z
Learnt from: CR
Repo: bhauman/clojure-mcp PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-02T20:23:28.499Z
Learning: Applies to src/**/*.{clj,cljc} : Maintain atom-based state for consistent service access in MCP tools

Applied to files:

  • src/clojure_mcp/prompt_cli.clj
  • src/clojure_mcp/dialects.clj
  • src/clojure_mcp/core.clj
  • test/clojure_mcp/tools/eval/tool_test.clj
  • src/clojure_mcp/main_examples/shadow_main.clj
  • src/clojure_mcp/nrepl.clj
📚 Learning: 2025-08-02T20:23:28.499Z
Learnt from: CR
Repo: bhauman/clojure-mcp PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-02T20:23:28.499Z
Learning: Applies to **/*.{clj,cljc} : Align namespaces with directory structure (e.g., `clojure-mcp.repl-tools`)

Applied to files:

  • src/clojure_mcp/dialects.clj
📚 Learning: 2025-08-02T20:23:28.499Z
Learnt from: CR
Repo: bhauman/clojure-mcp PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-02T20:23:28.499Z
Learning: Applies to src/**/*.{clj,cljc} : Use `:require` with ns aliases for imports (e.g., `[clojure.string :as string]`)

Applied to files:

  • src/clojure_mcp/dialects.clj
📚 Learning: 2025-08-02T20:23:28.499Z
Learnt from: CR
Repo: bhauman/clojure-mcp PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-02T20:23:28.499Z
Learning: Applies to src/**/*.{clj,cljc} : Return structured data with both result and error status in MCP tools

Applied to files:

  • src/clojure_mcp/dialects.clj
  • src/clojure_mcp/tools/eval/core.clj
📚 Learning: 2025-08-02T20:23:28.499Z
Learnt from: CR
Repo: bhauman/clojure-mcp PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-02T20:23:28.499Z
Learning: Prefer REPL-driven development for rapid iteration and feedback

Applied to files:

  • src/clojure_mcp/core.clj
  • test/clojure_mcp/tools/eval/core_test.clj
🔇 Additional comments (17)
src/clojure_mcp/prompt_cli.clj (1)

18-19: Require aliases for file-utils and logging look correct

Aliases match usage (file-utils/slurp-utf8, logging/configure-logging!), keeping dependencies explicit and consistent.

src/clojure_mcp/config.clj (1)

59-85: Centralized validate-configs improves error reporting

Validating each non-empty config with its canonical path and tagging ::schema-error is a clean way to surface per-file schema issues and integrate with load-config-handling-validation-errors.

test/clojure_mcp/tools/eval/tool_test.clj (1)

14-25: Fixture update to blocking eval-code matches new nREPL API

Using a single blocking (eval-code client "(require 'clojure.repl)") setup without polling keeps the tests simple and consistent with the refactored client.

src/clojure_mcp/tools/bash/tool.clj (1)

119-125: execute-tool correctly chooses between local and nREPL-backed execution

Dispatching to execute-bash-command-nrepl with :session-type when nrepl-session-type is set, and otherwise using the local executor, matches the intended bash-over-nREPL semantics without changing the result shape.

src/clojure_mcp/core.clj (1)

283-297: Additional info log on nREPL client creation is useful and low-risk

Logging "nREPL client map created" right after nrepl/create gives a clear lifecycle marker during startup without changing behavior.

test/clojure_mcp/tools/eval/core_test.clj (1)

13-26: Test fixture correctly updated for blocking eval-code and removed polling

Initializing the test REPL with a single blocking (eval-code …) and dropping start-polling/stop-polling keeps the tests in sync with the refactored nREPL client while preserving behavior.

src/clojure_mcp/main_examples/shadow_main.clj (2)

51-59: Session-type wiring for secondary Shadow connection looks good

shadow-eval-tool-secondary-connection-tool correctly starts the Shadow REPL on the secondary connection and then builds the eval tool with {:session-type :shadow}, ensuring subsequent evals reuse the same Shadow session type. No issues from the refactor here.


62-66: Shared-connection Shadow eval path is consistent with new model

The shared-connection variant now delegates to start-shadow-repl and then creates the eval tool with {:session-type :shadow} against the primary nrepl-client-atom. This matches the new session-type model and avoids the older explicit session plumbing.

src/clojure_mcp/dialects.clj (3)

79-89: Use of nrepl/eval-code + nrepl-core/combine-responses is consistent with new eval model

fetch-project-directory-helper now evaluates the dialect-specific expression via clojure-mcp.nrepl/eval-code and then uses nrepl.core/combine-responses to extract :value. That’s aligned with the new synchronous, per-session-type evaluation flow and keeps this helper independent of the higher-level eval tooling.


110-118: Environment initialization now correctly uses eval-code

Switching initialize-environment to call nrepl/eval-code for each init-exp keeps dialect init aligned with the new nREPL client abstraction. Since these are short, one-off bootstrapping forms, the lack of explicit :session-type (falling back to :default) is fine.


120-126: Using :session-type :tools for REPL helpers is appropriate

Loading helper expressions via nrepl/eval-code with :session-type :tools cleanly separates “tools” state from default eval sessions, and matches how other tooling in this PR uses the tools session type.

src/clojure_mcp/tools/eval/tool.clj (2)

12-16: Factory now cleanly carries :session-type into tool config

The updated arity for create-eval-tool correctly captures an optional :session-type and stores it on the tool config. This is the right place to anchor session-type selection for consumers.


68-73: execute-tool correctly propagates :session-type and timeout into core eval

execute-tool now threads session-type into the inputs map and preserves the existing timeout-ms override behavior. That keeps the tool behavior aligned with the new session-type–based nREPL API without changing the external schema.

src/clojure_mcp/tools/eval/core.clj (2)

43-56: Response aggregation correctly tracks outputs and error state

process-responses captures :out, :err, and :value in order and marks :error when either an exception (:ex) appears or :status contains "error"/"eval-error". That’s a solid fit for nREPL’s response model and works well with the existing partitioning/formatting pipeline.


104-122: Repaired-code handling remains correct with new eval pipeline

evaluate-with-repair still repairs delimiter errors, then delegates to evaluate-code and adds :repaired based on whether the code changed. This composes cleanly with the new synchronous eval and process-responses output structure.

src/clojure_mcp/nrepl.clj (2)

59-82: Synchronous eval-code with per-session-type tracking looks solid

Using a fresh connection per eval-code call, with ensure-session! to reuse or create a per-session-type nREPL session and updating :current-ns from responses, is a clean design and aligns well with the new stateless-connection goal. The use of truncation-length for print quota also matches the higher-level formatting logic.


94-99: describe implementation is straightforward and matches new connection model

describe now opens a short-lived connection, creates a client, and returns nrepl/combine-responses for a simple {:op "describe"} message. That’s minimal and consistent with the rest of the synchronous API.

Comment on lines 84 to 92
(defn interrupt [service]
(let [state (get-state service)
id (:current-eval-id @state)]
(when id
(with-open [conn (connect service)]
(let [client (nrepl/client conn 1000)
;; Assuming default session for interrupt for now as per typical usage
session-id (get-stored-session service :default)]
(nrepl/message client {:op "interrupt" :session session-id :interrupt-id id}))))))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

interrupt only targets the default session, which can break timeouts for other session types

interrupt currently looks up the session ID via:

(let [state (get-state service)
      id (:current-eval-id @state)]
  ...
  (let [client (nrepl/client conn 1000)
        ;; Assuming default session for interrupt for now as per typical usage
        session-id (get-stored-session service :default)]
    (nrepl/message client {:op "interrupt" :session session-id :interrupt-id id})))

But eval-code accepts a :session-type (defaulting to :default) and stores only :current-eval-id in state, not the associated session-type. When evaluate-code in tools.eval.core runs with :session-type :tools or :shadow, a timeout will call nrepl/interrupt, which then tries to interrupt the default session. That likely won’t stop the long-running eval in the non-default session.

To make interrupts work reliably across session-types, consider also tracking the current eval’s session-type and using it here, e.g.:

-      (swap! (get-state service) assoc :current-eval-id id)
+      (swap! (get-state service) assoc
+             :current-eval-id id
+             :current-eval-session-type session-type)
...
-(defn interrupt [service]
-  (let [state (get-state service)
-        id (:current-eval-id @state)]
+(defn interrupt [service]
+  (let [state (get-state service)
+        {:keys [current-eval-id current-eval-session-type]} @state
+        session-type (or current-eval-session-type :default)]
-    (when id
+    (when current-eval-id
       (with-open [conn (connect service)]
         (let [client (nrepl/client conn 1000)
-              session-id (get-stored-session service :default)]
-          (nrepl/message client {:op "interrupt" :session session-id :interrupt-id id}))))))
+              session-id (get-stored-session service session-type)]
+          (nrepl/message client {:op "interrupt"
+                                 :session session-id
+                                 :interrupt-id current-eval-id}))))))

And clear :current-eval-session-type in the eval-code finally block alongside :current-eval-id.

This would make interrupt behave correctly for :tools, :shadow, and other session-types used throughout the PR.

🤖 Prompt for AI Agents
In src/clojure_mcp/nrepl.clj around lines 84 to 92, interrupt only looks up the
default session and thus fails to interrupt evals running in non-default
sessions; update the state to record the session-type for the current eval (e.g.
:current-eval-session-type) when eval-code starts, clear that key in the
eval-code finally block alongside :current-eval-id, and change interrupt to read
that session-type from state and pass it to get-stored-session (falling back to
:default if absent) before sending the nREPL interrupt message so the correct
session is targeted.

Comment on lines 69 to 102
(let [{:keys [code timeout_ms session-type]} opts
timeout-ms (or timeout_ms 20000)
outputs (atom [])
error-occurred (atom false)
form-str code
add-output! (fn [prefix value] (swap! outputs conj [prefix value]))
result-promise (promise)]
form-str code]

;; Evaluate the code
;; Push to eval history if available
(when-let [state (::nrepl/state nrepl-client)]
(swap! state update :clojure-mcp.repl-tools/eval-history conj form-str)

;; Evaluate the code, using the namespace parameter if provided
(try
(nrepl/eval-code-msg
nrepl-client form-str
(if session {:session session} {})
(->> identity
(nrepl/out-err
#(add-output! :out %)
#(add-output! :err %))
(nrepl/value #(add-output! :value %))
(nrepl/done (fn [_]
(deliver result-promise
{:outputs @outputs
:error @error-occurred})))
(nrepl/error (fn [{:keys [exception]}]
(reset! error-occurred true)
(add-output! :err exception)
(deliver result-promise
{:outputs @outputs
:error true})))))
(catch Exception e
;; prevent connection errors from confusing the LLM
(log/error e "Error when trying to eval on the nrepl connection")
(throw
(ex-info
(str "Internal Error: Unable to reach the nREPL "
"thus we are unable to execute the bash command.")
{:error-type :connection-error}
e))))
(swap! state update :clojure-mcp.repl-tools/eval-history conj form-str))

;; Wait for the result and return it
(let [tmb (Object.)
res (deref result-promise timeout-ms tmb)]
(if-not (= tmb res)
res
(try
(let [fut (future
(try
(let [responses (nrepl/eval-code nrepl-client form-str :session-type (or session-type :default))]
(process-responses responses))
(catch Exception e
(log/error e "Error during nREPL eval")
{:outputs [[:err (str "Internal Error: " (.getMessage e))]] :error true})))
res (deref fut timeout-ms :timeout)]
(if (= res :timeout)
(do
(future-cancel fut)
(nrepl/interrupt nrepl-client)
{:outputs [[:err (str "Eval timed out after " timeout-ms "ms.")]
[:err "Perhaps, you had an infinite loop or an eval that ran too long."]]
:error true}))))))
:error true})
res))
(catch Exception e
;; prevent connection errors from confusing the LLM
(log/error e "Error when trying to eval on the nrepl connection")
(throw
(ex-info
(str "Internal Error: Unable to reach the nREPL "
"thus we are unable to execute the bash command.")
{:error-type :connection-error}
e))))))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Outer connection-error message still refers to “bash command”

The outer catch in evaluate-code throws an ex-info with:

"thus we are unable to execute the bash command."

But this namespace is the generic eval core, not the bash tool, so that wording is confusing.

Consider updating the message to something eval-specific, e.g.:

- (str "Internal Error: Unable to reach the nREPL "
-      "thus we are unable to execute the bash command.")
+ (str "Internal Error: Unable to reach the nREPL, "
+      "so we are unable to evaluate your Clojure code.")

This keeps the semantics the same while making the error clearer to callers.

🤖 Prompt for AI Agents
In src/clojure_mcp/tools/eval/core.clj around lines 69 to 102, the outer catch
builds an ex-info whose message incorrectly mentions "bash command"; change the
message to refer to nREPL/eval (for example: "Internal Error: Unable to reach
the nREPL; unable to execute the requested eval.") while keeping the same
ex-info shape and metadata ({:error-type :connection-error}) and preserving the
original exception as the cause; update only the message text to be
eval-specific so callers see accurate context.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/clojure_mcp/tools/bash/config_test.clj (1)

29-79: Test inputs missing required :timeout-ms parameter that will cause assertion failures in real execution.

The test provides inputs {:command "echo test" :working-directory ...} but both execute-bash-command (core.clj:50) and execute-bash-command-nrepl (core.clj:172) assert that timeout-ms is required. The tool's validate-inputs method properly handles timeout defaults (tool.clj:78–79), but execute-tool :bash (tool.clj:119–125) passes inputs directly to execute functions without calling validation or merging the tool's default :timeout-ms.

The mocked execute functions in the test bypass these assertions, hiding the bug. In actual execution, calls would fail with "timeout-ms is required".

Fix: Either add :timeout-ms to test inputs (e.g., :timeout-ms 180000), or ensure execute-tool :bash merges the tool's default timeout into inputs before calling execute functions.

🧹 Nitpick comments (2)
test/clojure_mcp/tools/bash/session_test.clj (1)

10-11: Consider renaming to reflect session-type testing.

The function name test-bash-execution-uses-session references "session" but the test now verifies :session-type behavior. Consider renaming to test-bash-execution-uses-session-type for clarity.

-(deftest test-bash-execution-uses-session
+(deftest test-bash-execution-uses-session-type
   (testing "Bash command execution passes session-type to evaluate-code"
test/clojure_mcp/tools/project/tool_test.clj (1)

20-31: Fixture setup aligns with blocking eval; consider explicit session-type or basic assertion

The direct, blocking nrepl/eval-code calls for requiring clojure.repl and clojure.edn make sense with the new stateless client. If eval-code now keys behavior off :session-type, it may be worth explicitly passing the same :session-type that the project inspection tool uses (e.g. :tools or :default) and/or asserting that the require completes successfully, just to catch unexpected REPL startup issues early in the fixture.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 62bfe87 and ae5f83b.

📒 Files selected for processing (4)
  • test/clojure_mcp/tools/bash/config_test.clj (2 hunks)
  • test/clojure_mcp/tools/bash/session_test.clj (1 hunks)
  • test/clojure_mcp/tools/project/tool_test.clj (1 hunks)
  • test/clojure_mcp/tools/test_utils.clj (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{clj,cljc}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{clj,cljc}: Use kebab-case for vars/functions; end predicates with ? (e.g., is-top-level-form?)
Use try/catch with specific exception handling; use atom for tracking errors
Use 2-space indentation and maintain whitespace in edited forms
Align namespaces with directory structure (e.g., clojure-mcp.repl-tools)

Files:

  • test/clojure_mcp/tools/bash/config_test.clj
  • test/clojure_mcp/tools/test_utils.clj
  • test/clojure_mcp/tools/bash/session_test.clj
  • test/clojure_mcp/tools/project/tool_test.clj
test/**/*.{clj,cljc}

📄 CodeRabbit inference engine (CLAUDE.md)

Use deftest with descriptive names; testing for subsections; is for assertions in tests

Files:

  • test/clojure_mcp/tools/bash/config_test.clj
  • test/clojure_mcp/tools/test_utils.clj
  • test/clojure_mcp/tools/bash/session_test.clj
  • test/clojure_mcp/tools/project/tool_test.clj
🧠 Learnings (2)
📓 Common learnings
Learnt from: CR
Repo: bhauman/clojure-mcp PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-02T20:23:28.499Z
Learning: Prefer REPL-driven development for rapid iteration and feedback
📚 Learning: 2025-08-02T20:23:28.499Z
Learnt from: CR
Repo: bhauman/clojure-mcp PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-02T20:23:28.499Z
Learning: Applies to src/**/*.{clj,cljc} : Maintain atom-based state for consistent service access in MCP tools

Applied to files:

  • test/clojure_mcp/tools/project/tool_test.clj
🔇 Additional comments (5)
test/clojure_mcp/tools/bash/session_test.clj (1)

20-44: LGTM! Test correctly validates session-type propagation.

The test implementation properly verifies the refactored behavior:

  • Mocks evaluate-code to capture arguments
  • Asserts :session-type key is present with value :tools
  • Validates result formatting

This aligns well with the PR's objective to replace session objects with session-type identifiers.

test/clojure_mcp/tools/bash/config_test.clj (3)

9-14: LGTM!

The test correctly verifies that bash-over-nrepl defaults to false when not explicitly configured.


16-27: LGTM!

The test correctly verifies that bash-over-nrepl can be explicitly set to true or false via configuration.


81-100: LGTM!

The test correctly verifies that process-config handles the bash-over-nrepl parameter for true, false, and missing values, ensuring that missing values don't add the key to the processed config.

test/clojure_mcp/tools/project/tool_test.clj (1)

93-97: All call sites are consistent; code change is correct.

Verification confirms all core/inspect-project invocations pass the atom directly:

  • src/clojure_mcp/tools/project/tool.clj:52 passes nrepl-client-atom
  • src/clojure_mcp/resources.clj:130 passes nrepl-client-atom
  • src/clojure_mcp/agent/general_agent.clj:48 passes nrepl-client-atom
  • test/clojure_mcp/tools/project/tool_test.clj:96 passes *client-atom*

The change from @*client-atom* to *client-atom* aligns this test with the established pattern throughout the codebase.

@bhauman bhauman changed the title Refactor: Switch nREPL client to stateless connection model Refactor nREPL evaluation for robustness and simplicity Nov 24, 2025
@bhauman bhauman merged commit 78cf3ca into main Nov 24, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants