Skip to content

Conversation

@bhauman
Copy link
Owner

@bhauman bhauman commented Dec 5, 2025

Summary

  • Add lazy initialization of nREPL connections per-port in nrepl.clj
  • Create unified start entry point in main.clj that sets project-dir from CWD
  • Add graceful fallback for bash tool when no default port configured
  • Add graceful degradation for inspect-project when no REPL available
  • Improve error messages for nREPL connection failures

Key Changes

Lazy nREPL Initialization

  • nREPL connections are now initialized lazily on first eval-code call
  • Per-port state tracking: env-type, initialized?, project-dir
  • Thread-safe initialization with per-port locking

Graceful Degradation

  • bash tool falls back to local execution when no default port
  • inspect-project works without REPL (skips describe info)
  • clojure_eval shows clear error when connection fails

New Entry Point

  • clojure-mcp.main/start sets :project-dir to CWD by default
  • Allows MCP server to start without immediate REPL connection
  • Port is optional when project-dir is provided

Test plan

  • Test clojure_eval with valid port - lazy init works
  • Test clojure_eval with invalid port - clear error message
  • Test clojure_inspect_project without REPL - graceful degradation
  • Test clojure_inspect_project with REPL - shows Clojure/Java versions
  • Test bash tool without REPL - local execution fallback
  • Test bash tool with REPL - executes over nREPL when configured

Summary by CodeRabbit

  • Documentation

    • Updated build/test and linter command docs; removed single-test guidance and lint prohibition.
  • New Features

    • Added optional per-port parameter for evaluation and CLI tools.
    • New start entry points for main service and SSE that default project-dir to CWD.
  • Improvements

    • Lazy, per-port initialization and optional project-dir-based configuration loading.
    • Safer project inspection with prerequisite checks and conditional environment queries.
  • Tests

    • Test mocks updated to include a port entry.
  • Chores

    • Startup aliases simplified and exec entrypoints adjusted.

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

Bruce Hauman added 2 commits December 4, 2025 08:48
- Add lazy initialization of nREPL connections per-port in nrepl.clj
- Create unified start entry point in main.clj
- Add nrepl-available? check to bash tool for graceful fallback
- Add nrepl-available? check to inspect-project for graceful degradation
- Fix test and lint commands in CLAUDE.md
Catch ConnectException and SocketException in execute-tool and return
a clear error message that includes the port number and suggests
checking that an nREPL server is running.
@coderabbitai
Copy link

coderabbitai bot commented Dec 5, 2025

Walkthrough

Adds lazy, per-port nREPL initialization and per-port state/locks; moves dialect runtime execution into the nREPL layer (dialects now only provide expressions); introduces optional :project-dir startup path (direct config load) and small CLI/start wrappers; makes tools and tests nREPL-port-aware.

Changes

Cohort / File(s) Summary
Build & Dev Config
CLAUDE.md, deps.edn
Simplified test/lint commands; updated dev/sse aliases to call new start entrypoints and removed explicit exec port args; enabled logging in dev alias.
nREPL core & state
src/clojure_mcp/nrepl.clj
Added per-port state (make-port-entry), accessors (get/set port env/project-dir/initialized), init-locks and per-port initialization (initialize-port!, ensure-port-initialized!, with-port*, get-lock-for-port), detect/fetch/init/load helpers, read-nrepl-port-file, and refactored eval flow to ensure port initialization.
Core initialization
src/clojure_mcp/core.clj
create-and-start-nrepl-connection, create-additional-connection and server startup refactored to support explicit :project-dir (direct config load) or lazy REPL discovery; added ensure-port-if-needed and updated config-loading call sites to use nrepl helpers.
Dialects (declarative only)
src/clojure_mcp/dialects.clj
Removed runtime/REPL-executing helpers and side-effecting methods; retained only expression-producing multimethods (fetch-project-directory-exp, initialize-environment-exp, load-repl-helpers-exp) and defaults.
CLIs / Entrypoints
src/clojure_mcp/main.clj, src/clojure_mcp/sse_main.clj
Added lightweight start wrappers that default :project-dir to cwd (unless :not-cwd true) and delegate to existing start functions; updated docstrings to describe lazy REPL behavior.
Prompt CLI
src/clojure_mcp/prompt_cli.clj
Replaced calls from dialects/* to nrepl/* equivalents (env detection, project-dir fetch, init helpers).
Tools: bash & eval
src/clojure_mcp/tools/bash/tool.clj, src/clojure_mcp/tools/bash/core.clj, src/clojure_mcp/tools/eval/tool.clj
Bash tool now uses nREPL only when session-type and a resolved port exist; execute-bash-command-nrepl accepts optional port. Eval tool is port-aware: adds optional :port, resolves effective port (inputs, config, or .nrepl-port), uses per-port initialization (with-port-initialized) and handles connection errors.
Project inspection
src/clojure_mcp/tools/project/core.clj
Always read bb.edn; added precondition for working-dir/allowed-directories; resolve describe-info only when a port is available; formatting accepts nil describe data.
Tests: mock nREPL port
test/clojure_mcp/tools/bash/config_test.clj, test/clojure_mcp/tools/bash/session_test.clj
Added :port 7888 to mock nREPL client maps to reflect port-configured test scenarios.

Sequence Diagram(s)

sequenceDiagram
    participant User as User / Tool
    participant Core as core.clj
    participant nREPL as nrepl.clj
    participant Dialects as dialects.clj
    participant REPL as Remote nREPL

    rect rgb(220,240,220)
    Note over User,Core: Start with :project-dir provided
    User->>Core: start/build-and-start-mcp-server(opts with :project-dir)
    Core->>nREPL: create-and-start-nrepl-connection(:project-dir)
    Core->>Core: load config from project-dir (no REPL)
    end

    rect rgb(240,220,220)
    Note over User,REPL: Start without :project-dir (lazy)
    User->>Core: start/build-and-start-mcp-server(opts without project-dir)
    Core->>nREPL: create-and-start-nrepl-connection(no project-dir)
    Core->>nREPL: ensure-port-if-needed()
    Note over User,nREPL: Initialization deferred until first eval/use
    User->>nREPL: eval-code() or tool request (first use)
    nREPL->>nREPL: ensure-port-initialized! (acquire per-port lock)
    nREPL->>REPL: describe() — detect env type
    REPL-->>nREPL: env-type
    nREPL->>Dialects: fetch-project-directory-exp(env-type)
    Dialects-->>nREPL: expression template
    nREPL->>REPL: eval(expression) — get project-dir
    REPL-->>nREPL: project-dir
    nREPL->>nREPL: initialize-environment / load-repl-helpers (eval helpers)
    nREPL->>nREPL: mark port initialized (cache env-type, project-dir)
    nREPL->>User: perform requested eval/command (using initialized port)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

  • Review focus:
    • Thread-safety and lock semantics in src/clojure_mcp/nrepl.clj (init-locks, ensure-port-initialized!, with-port-initialized).
    • Correct handling of the two startup modes in src/clojure_mcp/core.clj (explicit :project-dir vs lazy REPL discovery) and ensure-port-if-needed usage.
    • Port resolution and error handling in src/clojure_mcp/tools/eval/tool.clj.
    • That src/clojure_mcp/dialects.clj expression-only API covers all env cases used by nrepl.

Possibly related PRs

Poem

🐰 I waited by the port with a thump and a twitch,

held a tiny lock and nudged the REPL to switch,
dialects handed scripts, I ran them with care,
lazy as a nap—ready when called, I’m there,
a hop, a small log, and the project-dir in my stitch.

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 accurately describes the main change: introducing lazy per-port nREPL initialization with graceful degradation support.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch lazy-nrepl-initialization

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.

- Add start function to sse_main.clj that sets project-dir from CWD
- Update :mcp-sse alias to use new start function (port now optional)
- Update :mcp and :dev-mcp aliases to use lazy initialization
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

🧹 Nitpick comments (3)
deps.edn (2)

40-40: Update the comment to reflect that port is now optional.

The comment states "it needs an nrepl port to talk to," but per the PR objectives, port is now optional when project-dir is provided, and initialization is lazy.

Consider updating to:

-   ;; it needs an nrepl port to talk to
+   ;; optional nrepl port for REPL connection (uses lazy initialization)

73-73: Update the comment to reflect that port is now optional.

The comment states "it needs an nrepl port to talk to," but this is outdated given the lazy initialization changes.

Consider updating to:

-   ;; it needs an nrepl port to talk to
+   ;; optional nrepl port for REPL connection (uses lazy initialization)
src/clojure_mcp/sse_main.clj (1)

37-41: Consider dissoc-ing :not-cwd before delegating.

The :not-cwd key is passed through to start-sse-mcp-server and eventually to lower layers where it's not needed. While harmless if ignored, removing internal control flags improves clarity.

Apply this diff to clean up the control flag:

 (defn start
   [opts]
   (let [not-cwd? (get opts :not-cwd false)
         opts' (if not-cwd?
                 opts
-                (assoc opts :project-dir (System/getProperty "user.dir")))]
-    (start-sse-mcp-server opts')))
+                (assoc opts :project-dir (System/getProperty "user.dir")))
+        opts'' (dissoc opts' :not-cwd)]
+    (start-sse-mcp-server opts'')))
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0bd4f92 and 3b0a709.

📒 Files selected for processing (2)
  • deps.edn (2 hunks)
  • src/clojure_mcp/sse_main.clj (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
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/sse_main.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/sse_main.clj
🧠 Learnings (4)
📓 Common learnings
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.
📚 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:

  • deps.edn
📚 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:

  • deps.edn
📚 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:

  • deps.edn
🔇 Additional comments (3)
deps.edn (2)

48-49: LGTM!

The function rename to clojure-mcp.sse-main/start aligns with the unified start entry point pattern, and using :mcp-sse-port clearly distinguishes the SSE server port from the nREPL port.


72-72: LGTM!

The function rename to clojure-mcp.main/start, commenting out the port, and adding :enable-logging? true all align with the PR objectives of lazy initialization and making port optional.

Also applies to: 74-75

src/clojure_mcp/sse_main.clj (1)

24-41: LGTM! Clear implementation of lazy initialization entry point.

The function correctly implements the lazy REPL initialization pattern documented in the PR objectives. The logic properly defaults :project-dir to the current working directory while allowing users to opt out via :not-cwd. The docstring clearly documents the behavior and supported options.

:aliases

{:mcp
{:exec-fn clojure-mcp.main/start-mcp-server
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Update the exec-fn to match the new unified start entry point.

The :dev-mcp alias (line 72) was updated to use clojure-mcp.main/start, but this :mcp alias still references clojure-mcp.main/start-mcp-server. Per the PR objectives introducing a unified start entry point, this should be updated to start for consistency.

Apply this diff:

-  {:exec-fn clojure-mcp.main/start-mcp-server
+  {:exec-fn clojure-mcp.main/start
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{:exec-fn clojure-mcp.main/start-mcp-server
{:exec-fn clojure-mcp.main/start
🤖 Prompt for AI Agents
In deps.edn around line 39 (alias :mcp), the :exec-fn is still set to
clojure-mcp.main/start-mcp-server; update that value to clojure-mcp.main/start
so the :mcp alias uses the new unified start entry point (mirror the change made
for :dev-mcp), ensuring the alias references clojure-mcp.main/start instead of
start-mcp-server.

When no port is provided or configured, the eval tool now checks for
a .nrepl-port file in the project directory. This allows automatic
connection to any nREPL server that writes its port to this standard
location.
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

🧹 Nitpick comments (3)
src/clojure_mcp/tools/eval/tool.clj (3)

11-22: Add port range validation to prevent invalid port numbers.

The function parses the port but doesn't validate that it's within the valid port range (1-65535). Invalid port numbers could cause connection failures later with less clear error messages.

Apply this diff to add port validation:

 (defn- read-nrepl-port-file
   "Reads the .nrepl-port file from the given directory.
    Returns the port number if found and valid, nil otherwise."
   [dir]
   (when dir
     (let [port-file (io/file dir ".nrepl-port")]
       (when (.exists port-file)
         (try
-          (-> (slurp port-file)
-              string/trim
-              Integer/parseInt)
+          (let [port (-> (slurp port-file)
+                         string/trim
+                         Integer/parseInt)]
+            (when (and (pos-int? port) (<= port 65535))
+              port))
           (catch Exception _ nil))))))

86-88: Validate port number is within valid range.

The validation checks that port is a positive integer but doesn't enforce the valid port range (1-65535). This is inconsistent with the validation added to read-nrepl-port-file (if that suggestion is applied).

Apply this diff to add upper bound validation:

-    (when (and port (not (pos-int? port)))
-      (throw (ex-info (str "Error parameter must be positive integer: port " (pr-str inputs))
+    (when (and port (not (and (pos-int? port) (<= port 65535))))
+      (throw (ex-info (str "Error parameter must be valid port number (1-65535): port " (pr-str inputs))
               {:inputs inputs})))

113-119: Clarify client selection logic with a comment.

The conditional logic for selecting whether to use with-port-initialized or ensure-port-initialized! is not immediately clear. The condition (or port (not (:port base-client))) could benefit from an explanatory comment.

Consider adding a clarifying comment:

     (try
-      (let [client (if (or port (not (:port base-client)))
-                     ;; Use specified port or discovered port
-                     (nrepl/with-port-initialized base-client effective-port)
-                     (do
-                       ;; For default port, ensure it's initialized (should already be, but safe)
-                       (nrepl/ensure-port-initialized! base-client)
-                       base-client))]
+      (let [client (if (or port (not (:port base-client)))
+                     ;; Non-default port case: user specified a port OR no default port configured.
+                     ;; Create/get a port-specific client via lazy initialization.
+                     (nrepl/with-port-initialized base-client effective-port)
+                     ;; Default port case: port matches configured default.
+                     ;; Ensure default port is initialized and return base client.
+                     (do
+                       (nrepl/ensure-port-initialized! base-client)
+                       base-client))]
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3b0a709 and 8963165.

📒 Files selected for processing (1)
  • src/clojure_mcp/tools/eval/tool.clj (3 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
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/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/tool.clj
🧠 Learnings (5)
📓 Common learnings
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.
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`)
📚 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/tools/eval/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 **/*.{clj,cljc} : Align namespaces with directory structure (e.g., `clojure-mcp.repl-tools`)

Applied to files:

  • src/clojure_mcp/tools/eval/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: Prefer REPL-driven development for rapid iteration and feedback

Applied to files:

  • src/clojure_mcp/tools/eval/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} : Use `:require` with ns aliases for imports (e.g., `[clojure.string :as string]`)

Applied to files:

  • src/clojure_mcp/tools/eval/tool.clj
🔇 Additional comments (1)
src/clojure_mcp/tools/eval/tool.clj (1)

124-131: Good error handling for connection failures.

The exception handling for ConnectException and SocketException provides clear, actionable error messages that include the port number and suggest troubleshooting steps. This aligns well with the graceful degradation objectives.

Comment on lines 90 to 94
(let [service @nrepl-client-atom
project-dir (config/get-nrepl-user-dir service)
effective-port (or port
(:port service)
(read-nrepl-port-file project-dir))]
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Extract duplicated effective-port determination logic.

The logic to determine the effective port (from provided port, configured port, or .nrepl-port file) is duplicated between validate-inputs (lines 90-94) and execute-tool (lines 109-111). This duplication could lead to inconsistency if one location is updated without the other.

Extract this logic into a helper function:

(defn- determine-effective-port
  "Determines the effective port from multiple sources in priority order:
   1. Explicitly provided port
   2. Configured port from service
   3. Port from .nrepl-port file in project directory"
  [service provided-port]
  (or provided-port
      (:port service)
      (read-nrepl-port-file (config/get-nrepl-user-dir service))))

Then use it in both locations:

 (defmethod tool-system/validate-inputs ::clojure-eval [{:keys [nrepl-client-atom]} inputs]
   (let [{:keys [code timeout_ms port]} inputs]
     ...
     ;; Check that a port is available
     (let [service @nrepl-client-atom
-          project-dir (config/get-nrepl-user-dir service)
-          effective-port (or port
-                             (:port service)
-                             (read-nrepl-port-file project-dir))]
+          effective-port (determine-effective-port service port)]
       (when-not effective-port
         (throw (ex-info "No nREPL port available..."
-                        {:inputs inputs
-                         :project-dir project-dir}))))
+                        {:inputs inputs
+                         :project-dir (config/get-nrepl-user-dir service)}))))
     inputs))

 (defmethod tool-system/execute-tool ::clojure-eval [{:keys [nrepl-client-atom timeout session-type]}
                                                     {:keys [timeout_ms port] :as inputs}]
   (let [base-client @nrepl-client-atom
-        project-dir (config/get-nrepl-user-dir base-client)
-        effective-port (or port
-                           (:port base-client)
-                           (read-nrepl-port-file project-dir))]
+        effective-port (determine-effective-port base-client port)]
     ...))

Also applies to: 109-111

🤖 Prompt for AI Agents
In src/clojure_mcp/tools/eval/tool.clj around lines 90-94 and 109-111, the
effective-port selection logic is duplicated; extract it into a private helper
(e.g., determine-effective-port) that accepts the service and provided-port and
returns (or provided-port, (:port service), (read-nrepl-port-file
(config/get-nrepl-user-dir service))); replace the duplicated expressions in
validate-inputs and execute-tool with calls to this helper so both sites share
the same logic.

Bruce Hauman added 2 commits December 5, 2025 08:46
Move port resolution logic (provided, configured, or .nrepl-port file)
to validate-inputs and pass the resolved port to execute-tool via the
inputs map. This removes duplicate port resolution code.
- Move read-nrepl-port-file to nrepl.clj as shared utility
- Update eval tool to discover port from .nrepl-port when not configured
- Update bash tool to use discovered port for nREPL execution
- Update inspect-project to fetch describe info using discovered port
- Update eval tool test to expect resolved port in validate-inputs
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

♻️ Duplicate comments (1)
src/clojure_mcp/tools/eval/tool.clj (1)

74-85: Extract duplicated port resolution logic.

The port resolution logic at lines 77-79 is duplicated in execute-tool. As noted in previous review comments, this duplication should be extracted into a helper function to ensure consistency if the logic needs to be updated.

Consider extracting into a helper function as suggested in the previous review:

(defn- determine-effective-port
  "Determines the effective port from multiple sources in priority order:
   1. Explicitly provided port
   2. Configured port from service
   3. Port from .nrepl-port file in project directory"
  [service provided-port]
  (or provided-port
      (:port service)
      (nrepl/read-nrepl-port-file (config/get-nrepl-user-dir service))))

Then use it in both locations to eliminate duplication.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8963165 and 43832b3.

📒 Files selected for processing (6)
  • src/clojure_mcp/nrepl.clj (5 hunks)
  • src/clojure_mcp/tools/bash/core.clj (2 hunks)
  • src/clojure_mcp/tools/bash/tool.clj (2 hunks)
  • src/clojure_mcp/tools/eval/tool.clj (3 hunks)
  • src/clojure_mcp/tools/project/core.clj (2 hunks)
  • test/clojure_mcp/tools/eval/tool_test.clj (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{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/eval/tool_test.clj
  • src/clojure_mcp/tools/bash/tool.clj
  • src/clojure_mcp/tools/project/core.clj
  • src/clojure_mcp/tools/eval/tool.clj
  • src/clojure_mcp/tools/bash/core.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
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/bash/tool.clj
  • src/clojure_mcp/tools/project/core.clj
  • src/clojure_mcp/tools/eval/tool.clj
  • src/clojure_mcp/tools/bash/core.clj
  • src/clojure_mcp/nrepl.clj
🧠 Learnings (9)
📓 Common learnings
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.
📚 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:

  • test/clojure_mcp/tools/eval/tool_test.clj
  • src/clojure_mcp/tools/bash/tool.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/tools/bash/tool.clj
  • src/clojure_mcp/tools/project/core.clj
  • src/clojure_mcp/tools/eval/tool.clj
  • src/clojure_mcp/tools/bash/core.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/tools/bash/tool.clj
  • src/clojure_mcp/tools/eval/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} : Return structured data with both result and error status in MCP tools

Applied to files:

  • src/clojure_mcp/tools/bash/tool.clj
  • src/clojure_mcp/tools/project/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/tools/bash/tool.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 src/**/*.{clj,cljc} : Include clear tool `:description` for LLM guidance

Applied to files:

  • src/clojure_mcp/tools/bash/tool.clj
  • src/clojure_mcp/tools/project/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/tools/eval/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} : Use `:require` with ns aliases for imports (e.g., `[clojure.string :as string]`)

Applied to files:

  • src/clojure_mcp/tools/eval/tool.clj
🔇 Additional comments (21)
test/clojure_mcp/tools/eval/tool_test.clj (1)

60-64: LGTM! Test correctly validates port resolution.

The test expectations properly verify that validate-inputs now returns both the code string and a resolved port as a positive integer, aligning with the per-port evaluation changes introduced in this PR.

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

6-6: LGTM! Import required for port-file reading.

The nREPL namespace import is necessary for the read-nrepl-port-file function used in the port resolution logic below.


118-130: LGTM! Graceful degradation for bash tool.

The execute-tool implementation correctly implements graceful degradation by:

  • Resolving the effective port from configured port or .nrepl-port file
  • Using nREPL execution only when both session-type and port are available
  • Falling back to local execution when no port is configured

This aligns with the PR's objectives for graceful degradation.

src/clojure_mcp/tools/project/core.clj (4)

186-187: LGTM! Enables Babashka detection without REPL.

Reading bb.edn unconditionally allows project inspection to detect Babashka projects via file presence, supporting graceful degradation when no REPL connection is available.


345-348: LGTM! Guard clause prevents invalid configuration.

The guard clause properly checks for required configuration before attempting project inspection, returning a clear error message when configuration is missing.


350-356: LGTM! Graceful degradation for project inspection.

The port resolution and conditional describe-info fetching correctly implement graceful degradation:

  • Only attempts to fetch version information when a port is available
  • Allows project inspection to proceed without a REPL connection
  • Passes potentially nil describe-info to format-project-info

The code properly supports the PR's graceful degradation objectives.


358-361: LGTM! format-project-info handles nil describe-info.

The call to format-project-info with potentially nil describe-info is safe, as the formatting function uses when guards (lines 215-223) to conditionally include version information only when available.

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

169-169: LGTM! Port parameter added for per-port evaluation.

The function signature correctly adds port to the destructured parameters, enabling per-port nREPL evaluation as required by the lazy initialization changes.


182-186: LGTM! Port override logic is clear and correct.

The conditional port override using cond-> is idiomatic and clearly implements the intended behavior: using the provided port when available, otherwise using the client's configured port.

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

5-7: LGTM! Required imports for port-aware evaluation.

The imports for config and nrepl namespaces are necessary for the port resolution and initialization logic introduced in this PR.


32-33: LGTM! Clear documentation for port parameter.

The PORT PARAMETER documentation clearly explains the use case for evaluating on different nREPL servers and mentions lazy initialization behavior, providing helpful guidance for tool users.


59-60: LGTM! Schema correctly defines port property.

The schema properly defines the port property as an optional integer with a clear description of its purpose and use case.


87-104: LGTM! Excellent error handling for connection failures.

The execute-tool implementation properly:

  • Uses lazy port initialization via with-port-initialized
  • Delegates to the core evaluation function with appropriate options
  • Catches connection exceptions and returns structured error responses with clear, actionable messages

The error messages help users understand connection failures and guide them toward resolution.

src/clojure_mcp/nrepl.clj (8)

3-9: LGTM! Necessary imports for per-port functionality.

The added imports support the new functionality for port file reading, dialect detection, and initialization logging introduced in this PR.


15-22: LGTM! Well-structured port entry model.

The make-port-entry function properly initializes per-port state with all necessary fields for tracking sessions, namespaces, environment type, initialization status, and project directory.


24-32: LGTM! Create function supports optional port initialization.

The updated create function properly supports lazy per-port initialization by conditionally creating an initial port entry when a port is provided, while remaining backward compatible when port is nil.


99-152: LGTM! Consistent per-port state accessors.

The per-port state accessor functions follow Clojure naming conventions and provide a clean API for managing port-specific state (env-type, initialized?, project-dir). The ensure-port-entry! function properly handles lazy port entry creation.


179-199: LGTM! Clean separation for lazy initialization.

The refactor properly separates:

  • eval-code-internal: for use during initialization (avoids circular calls)
  • eval-code: public API that triggers lazy initialization

The forward declaration is necessary and clearly documented. This design prevents initialization loops while ensuring ports are initialized before use.


225-293: LGTM! Dialect functions moved to break circular dependencies.

The dialect-aware functions are correctly moved to the nREPL namespace to avoid circular dependencies. The use of eval-code-internal in these functions properly avoids triggering full initialization during the initialization process itself, preventing infinite loops.


313-366: LGTM! Well-designed lazy initialization with proper locking.

The initialization functions properly implement lazy, thread-safe per-port initialization:

  • detect-and-store-env-type!: idempotent environment detection
  • initialize-port!: runs initialization steps only when needed
  • ensure-port-initialized!: provides thread safety via per-port locking
  • with-port and with-port-initialized: clean API for port lifecycle management

The separation between with-port (entry creation) and with-port-initialized (full init) provides flexibility while ensuring thread safety.


368-379: LGTM! Port file reading supports graceful degradation.

The read-nrepl-port-file function properly reads the .nrepl-port file with appropriate error handling, returning nil on failure to support graceful degradation when port discovery fails.

Comment on lines +302 to +311
(defn- get-lock-for-port
"Gets or creates a lock object for the given port."
[port]
(or (get @init-locks port)
(let [lock (Object.)]
(swap! init-locks (fn [locks]
(if (contains? locks port)
locks
(assoc locks port lock))))
(get @init-locks port))))
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

🧩 Analysis chain

🏁 Script executed:

cat -n src/clojure_mcp/nrepl.clj | sed -n '298,315p'

Repository: bhauman/clojure-mcp

Length of output: 843


🏁 Script executed:

rg -nP --type=clj -C3 '\b(future|thread|concurrent|parallel)\b' src/

Repository: bhauman/clojure-mcp

Length of output: 90


🏁 Script executed:

find . -name "*test*.clj" -o -name "*_test.clj" | head -20

Repository: bhauman/clojure-mcp

Length of output: 976


🏁 Script executed:

cat test/clojure_mcp/nrepl_launcher_test.clj | head -100

Repository: bhauman/clojure-mcp

Length of output: 4318


🏁 Script executed:

rg -n "get-lock-for-port" src/clojure_mcp/nrepl.clj

Repository: bhauman/clojure-mcp

Length of output: 136


🏁 Script executed:

rg -n "future\|pmap\|async" test/clojure_mcp/nrepl_launcher_test.clj

Repository: bhauman/clojure-mcp

Length of output: 45


🏁 Script executed:

cat -n src/clojure_mcp/nrepl.clj | sed -n '340,350p'

Repository: bhauman/clojure-mcp

Length of output: 444


🏁 Script executed:

rg -B5 -A5 "get-lock-for-port" src/clojure_mcp/nrepl.clj

Repository: bhauman/clojure-mcp

Length of output: 684


Simplify lock retrieval to eliminate unnecessary object creation.

The get-lock-for-port function may create redundant lock objects under concurrent access. When multiple threads call it simultaneously for the same port, each creates a lock object before any swap! occurs. While the swap's internal check ensures only one lock is stored per port, this still wastes the discarded objects.

Additionally, the final (get @init-locks port) on line 311 retrieves from the atom after swap! completes, which is safe but creates a small window of redundancy.

Simplify by returning the lock directly from the swap result:

(defn- get-lock-for-port
  "Gets or creates a lock object for the given port."
  [port]
  (let [lock (Object.)]
    (get (swap! init-locks
                (fn [locks]
                  (if (contains? locks port)
                    locks
                    (assoc locks port lock))))
         port)))

This ensures the returned lock is guaranteed to be in the atom and eliminates the redundant second dereference.

🤖 Prompt for AI Agents
In src/clojure_mcp/nrepl.clj around lines 302 to 311, avoid creating redundant
lock objects under concurrent access by allocating the lock before the swap! and
returning the lock from the swap! result instead of dereferencing the atom
again; specifically, create a lock object, call (swap! init-locks (fn [locks]
(if (contains? locks port) locks (assoc locks port lock)))) and then return (get
swapped-map port) (i.e., the value from the swap! return) so only one lock is
retained and no extra dereference is needed.

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