Skip to content

Commit 78cf3ca

Browse files
bhaumanBruce Hauman
andauthored
Refactor nREPL evaluation for robustness and simplicity (#132)
* Refactor: Switch nREPL client to stateless connection model - 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. * Test updates: Align tests with new nREPL session handling - 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. * nrepl-refactor tweaks * adding id to shadow repl code * tweask to dev script --------- Co-authored-by: Bruce Hauman <bhauman@gmail.com>
1 parent 5dae6ed commit 78cf3ca

File tree

19 files changed

+291
-500
lines changed

19 files changed

+291
-500
lines changed

clojure-mcp-dev.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ PORT=7888
2323
# PORT=58709 # conj-talk
2424

2525
# Start tee process to capture stdin in background
26-
tee "$STDIN_LOG" < "$PIPE" | clojure -X:dev-mcp :port $PORT 2>&1 | tee "$STDOUT_LOG" &
26+
tee "$STDIN_LOG" < "$PIPE" | clojure -X:dev-mcp :enable-logging? true :port $PORT 2>&1 | tee "$STDOUT_LOG" &
27+
# tee "$STDIN_LOG" < "$PIPE" | clojure -X:mcp-shadow :enable-logging? true :port $PORT 2>&1 | tee "$STDOUT_LOG" &
28+
# tee "$STDIN_LOG" < "$PIPE" | clojure -X:mcp-shadow-dual :enable-logging? true :port 7888 :shadow-port $PORT 2>&1 | tee "$STDOUT_LOG" &
2729

2830
# Get the PID of the background pipeline
2931
CLOJURE_PID=$!

src/clojure_mcp/config.clj

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,7 @@
110110
(assoc :nrepl-env-type (:nrepl-env-type config)))))
111111

112112
(defn load-config
113-
"Loads configuration from both user home (~/.clojure-mcp/config.edn) and project directory.
114-
User home config provides defaults, project config provides overrides.
115-
Validates both configs before merging."
113+
"Loads the configuration from user home and project directories."
116114
[cli-config-file user-dir]
117115
;; Load user home config first (provides defaults)
118116
(let [home-config (load-home-config)

src/clojure_mcp/core.clj

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,6 @@
273273
274274
This function handles the complete setup process including:
275275
- Creating the nREPL client connection
276-
- Starting the polling mechanism
277276
- Loading required namespaces and helpers (if Clojure environment)
278277
- Setting up the working directory
279278
- Loading configuration
@@ -285,10 +284,7 @@
285284
(try
286285
(let [nrepl-client-map (nrepl/create (dissoc initial-config :project-dir :nrepl-env-type))
287286
cli-env-type (:nrepl-env-type initial-config)
288-
_ (do
289-
(log/info "nREPL client map created")
290-
(nrepl/start-polling nrepl-client-map)
291-
(log/info "Started polling nREPL"))
287+
_ (log/info "nREPL client map created")
292288
;; Detect environment type early
293289
;; TODO this needs to be sorted out
294290
env-type (dialects/detect-nrepl-env-type nrepl-client-map)
@@ -314,7 +310,6 @@
314310
(log/info "Creating additional nREPL connection" initial-config)
315311
(try
316312
(let [nrepl-client-map (nrepl/create initial-config)]
317-
(nrepl/start-polling nrepl-client-map)
318313
;; copy config
319314
;; maybe we should create this just like the normal nrelp connection?
320315
;; we should introspect the project and get a working directory
@@ -331,15 +326,12 @@
331326
"Convenience higher-level API function to gracefully shut down MCP and nREPL servers.
332327
333328
This function handles the complete shutdown process including:
334-
- Stopping nREPL polling if a client exists in nrepl-client-atom
335329
- Gracefully closing the MCP server
336330
- Proper error handling and logging"
337331
[nrepl-client-atom]
338332
(log/info "Shutting down servers")
339333
(try
340334
(when-let [client @nrepl-client-atom]
341-
(log/info "Stopping nREPL polling")
342-
(nrepl/stop-polling client)
343335
;; Clean up auto-started nREPL process if present
344336
(when-let [nrepl-process (:nrepl-process client)]
345337
(log/info "Cleaning up auto-started nREPL process")

src/clojure_mcp/dialects.clj

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
44
Supports different Clojure-like environments by providing expressions
55
and initialization sequences specific to each dialect."
6-
(:require [clojure.edn :as edn]
7-
[clojure.java.io :as io]
6+
(:require [clojure.java.io :as io]
7+
[clojure.string :as str]
88
[taoensso.timbre :as log]
9+
[nrepl.core :as nrepl-core]
910
[clojure-mcp.nrepl :as nrepl]
1011
[clojure-mcp.utils.file :as file-utils]))
1112

@@ -79,8 +80,10 @@
7980
;; default to fetching from the nrepl
8081
(when-let [exp (fetch-project-directory-exp nrepl-env-type)]
8182
(try
82-
(edn/read-string
83-
(nrepl/tool-eval-code nrepl-client-map exp))
83+
(let [result-value (->> (nrepl/eval-code nrepl-client-map exp :session-type :tools)
84+
nrepl-core/combine-responses
85+
:value)]
86+
result-value)
8487
(catch Exception e
8588
(log/warn e "Failed to fetch project directory")
8689
nil))))
@@ -93,10 +96,14 @@
9396
"Fetches the project directory for the given nREPL client.
9497
If project-dir is provided in opts, returns it directly.
9598
Otherwise, evaluates environment-specific expression to get it."
96-
[nrepl-client-map nrepl-env-type project-dir]
97-
(if project-dir
98-
(.getCanonicalPath (io/file project-dir))
99-
(fetch-project-directory-helper nrepl-env-type nrepl-client-map)))
99+
[nrepl-client-map nrepl-env-type project-dir-arg]
100+
(if project-dir-arg
101+
(.getCanonicalPath (io/file project-dir-arg))
102+
(let [raw-result (fetch-project-directory-helper nrepl-env-type nrepl-client-map)]
103+
;; nrepl sometimes returns strings with extra quotes and in a vector
104+
(if (and (vector? raw-result) (= 1 (count raw-result)) (string? (first raw-result)))
105+
(str/replace (first raw-result) #"^\"|\"$" "")
106+
raw-result))))
100107

101108
;; High-level wrapper functions that execute the expressions
102109

@@ -107,15 +114,15 @@
107114
(log/debug "Initializing Clojure environment")
108115
(when-let [init-exps (not-empty (initialize-environment-exp nrepl-env-type))]
109116
(doseq [exp init-exps]
110-
(nrepl/eval-code nrepl-client-map exp identity)))
117+
(nrepl/eval-code nrepl-client-map exp)))
111118
nrepl-client-map)
112119

113120
(defn load-repl-helpers
114121
"Loads REPL helper functions appropriate for the environment."
115122
[nrepl-client-map nrepl-env-type]
116123
(when-let [helper-exps (not-empty (load-repl-helpers-exp nrepl-env-type))]
117124
(doseq [exp helper-exps]
118-
(nrepl/tool-eval-code nrepl-client-map exp)))
125+
(nrepl/eval-code nrepl-client-map exp :session-type :tools)))
119126
nrepl-client-map)
120127

121128
(defn detect-nrepl-env-type [nrepl-client-map]

src/clojure_mcp/main_examples/shadow_main.clj

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ JavaScript interop is fully supported including `js/console.log`, `js/setTimeout
3030
3131
**IMPORTANT**: This repl is intended for CLOJURESCRIPT CODE only.")
3232

33-
(defn start-shadow-repl [nrepl-client-atom cljs-session {:keys [shadow-build shadow-watch]}]
33+
(defn start-shadow-repl [nrepl-client-atom {:keys [shadow-build shadow-watch]}]
3434
(let [start-code (format
3535
;; TODO we need to check if its already running
3636
;; here and only initialize if it isn't
@@ -39,36 +39,33 @@ JavaScript interop is fully supported including `js/console.log`, `js/setTimeout
3939
"(do (shadow/repl %s) %s)")
4040
(pr-str (keyword (name shadow-build)))
4141
(pr-str (keyword (name shadow-build))))]
42-
(nrepl/eval-code-msg
43-
@nrepl-client-atom start-code {:session cljs-session}
44-
(->> identity
45-
(nrepl/out-err #(log/info %) #(log/info %))
46-
(nrepl/value #(log/info %))
47-
(nrepl/done (fn [_] (log/info "done")))
48-
(nrepl/error (fn [args]
49-
(log/info (pr-str args))
50-
(log/info "ERROR in shadow start")))))
51-
cljs-session))
42+
(log/info "Starting Shadow CLJS...")
43+
(try
44+
(nrepl/eval-code @nrepl-client-atom start-code :session-type :shadow)
45+
(log/info "Shadow CLJS started (or command sent)")
46+
(catch Exception e
47+
(log/error e "ERROR in shadow start")))
48+
:shadow))
5249

5350
;; when having a completely different connection for cljs
5451
(defn shadow-eval-tool-secondary-connection-tool [nrepl-client-atom {:keys [shadow-port _shadow-build _shadow-watch] :as config}]
5552
(let [cljs-nrepl-client-map (core/create-additional-connection nrepl-client-atom {:port shadow-port})
5653
cljs-nrepl-client-atom (atom cljs-nrepl-client-map)]
5754
(start-shadow-repl
5855
cljs-nrepl-client-atom
59-
(nrepl/eval-session cljs-nrepl-client-map)
6056
config)
61-
(-> (eval-tool/eval-code cljs-nrepl-client-atom)
57+
(-> (eval-tool/eval-code cljs-nrepl-client-atom {:session-type :shadow})
6258
(assoc :name tool-name)
59+
(assoc :id (keyword tool-name))
6360
(assoc :description description))))
6461

6562
;; when sharing the clojure and cljs repl
6663
(defn shadow-eval-tool [nrepl-client-atom {:keys [_shadow-build _shadow-watch] :as config}]
67-
(let [cljs-session (nrepl/new-session @nrepl-client-atom)
68-
_ (start-shadow-repl nrepl-client-atom cljs-session config)]
69-
(-> (eval-tool/eval-code nrepl-client-atom {:nrepl-session cljs-session})
70-
(assoc :name tool-name)
71-
(assoc :description description))))
64+
(start-shadow-repl nrepl-client-atom config)
65+
(-> (eval-tool/eval-code nrepl-client-atom {:session-type :shadow})
66+
(assoc :name tool-name)
67+
(assoc :id (keyword tool-name))
68+
(assoc :description description)))
7269

7370
;; So we can set up shadow two ways
7471
;; 1. as a single repl connection using the shadow clojure connection for cloj eval

0 commit comments

Comments
 (0)