Skip to content

Commit f565e9b

Browse files
bhaumanBruce Haumanclaude
authored
Add dry_run parameter to file_write tool (#122)
* Add dry_run parameter to file_write tool Added optional dry_run parameter to file_write tool with two modes: - dry_run="diff": Returns unified diff without writing to file - dry_run="new-source": Returns complete file content without writing Changes: - Updated tool schema to include dry_run parameter - Modified validate-inputs to extract and pass through dry_run - Updated execute-tool to pass dry_run and skip timestamp update in dry_run mode - Modified format-results to handle new-source output - Updated core.clj write-file, write-clojure-file, and write-text-file functions - Fixed all test calls to pass nil for dry_run parameter All 365 tests pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Remove dry_run from file_write tool schema The dry_run parameter is for internal use only and should not be exposed in the public schema. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Return only diff or new-source when dry_run is set When dry_run parameter is present, return just the diff or new-source without any preamble (file type, path, or "Changes:" prefix). Changes: - Modified write-clojure-file to return only diff when dry_run="diff" - Modified write-text-file to return only diff when dry_run="diff" - Updated format-results to detect dry_run mode and return raw output 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Add :dry_run field to file_write results for clarity When dry_run parameter is used, include :dry_run field in the result containing the mode value ("diff" or "new-source") to make the API more self-documenting. Result formats: - dry_run="new-source": {:error false, :dry_run "new-source", :new-source "..."} - dry_run="diff": {:error false, :dry_run "diff", :diff "..."} - Normal operation: {:error false, :type "update", :file-path "...", :diff "..."} 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Bruce Hauman <bhauman@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent c102347 commit f565e9b

File tree

3 files changed

+101
-43
lines changed

3 files changed

+101
-43
lines changed

src/clojure_mcp/tools/file_write/core.clj

Lines changed: 62 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,34 @@
22
"Core implementation for the file-write tool.
33
This namespace contains the pure functionality without any MCP-specific code."
44
(:require
5-
[clojure.java.io :as io]
6-
[clojure-mcp.tools.form-edit.pipeline :as pipeline]
7-
[clojure-mcp.utils.diff :as diff-utils]
8-
[clojure-mcp.linting :as linting]
9-
[clojure-mcp.utils.valid-paths :as valid-paths]
10-
[rewrite-clj.zip :as z]))
5+
[clojure.java.io :as io]
6+
[clojure-mcp.tools.form-edit.pipeline :as pipeline]
7+
[clojure-mcp.utils.diff :as diff-utils]
8+
[clojure-mcp.linting :as linting]
9+
[clojure-mcp.utils.valid-paths :as valid-paths]
10+
[rewrite-clj.zip :as z]))
1111

12-
(defn is-clojure-file?
13-
"Check if a file is a Clojure-related file based on its extension or Babashka shebang.
12+
(defn is-clojure-file?
13+
"Check if a file is a Clojure-related file based on its extension or Babashka shebang.
1414
1515
Parameters:
1616
- file-path: Path to the file to check
1717
1818
Returns true for Clojure extensions (.clj, .cljs, .cljc, .edn, .bb) or files with a `bb` shebang."
19-
[file-path]
20-
(boolean (valid-paths/clojure-file? file-path)))
19+
[file-path]
20+
(boolean (valid-paths/clojure-file? file-path)))
2121

2222
(defn write-clojure-file
2323
"Write content to a Clojure file, with linting, formatting, and diffing.
2424
2525
Parameters:
2626
- file-path: Validated path to the file to write
2727
- content: Content to write to the file
28+
- dry_run: Optional preview mode ('diff' or 'new-source')
2829
2930
Returns:
3031
- A map with :error, :type, :file-path, and :diff keys"
31-
[nrepl-client-atom file-path content]
32+
[nrepl-client-atom file-path content dry_run]
3233
(let [file (io/file file-path)
3334
file-exists? (.exists file)
3435
old-content (if file-exists? (slurp file) "")
@@ -50,27 +51,47 @@
5051
pipeline/format-source ;; Format the content
5152
pipeline/generate-diff ;; Generate diff between old and new content
5253
pipeline/determine-file-type ;; Determine if creating or updating
53-
pipeline/save-file)] ;; Save the file and get offsets
54+
;; Conditionally skip file save if dry_run is set
55+
(fn [ctx]
56+
(if dry_run
57+
ctx
58+
(pipeline/save-file ctx))))]
5459

5560
;; Format the result for tool consumption
5661
(if (::pipeline/error result)
5762
{:error true
5863
:message (::pipeline/message result)}
59-
{:error false
60-
:type (::pipeline/type result)
61-
:file-path (::pipeline/file-path result)
62-
:diff (::pipeline/diff result)})))
64+
(cond
65+
;; Return new-source for dry_run="new-source"
66+
(= dry_run "new-source")
67+
{:error false
68+
:dry_run "new-source"
69+
:new-source (::pipeline/output-source result)}
70+
71+
;; Return just diff for dry_run="diff"
72+
(= dry_run "diff")
73+
{:error false
74+
:dry_run "diff"
75+
:diff (::pipeline/diff result)}
76+
77+
;; Return full result for normal operation
78+
:else
79+
{:error false
80+
:type (::pipeline/type result)
81+
:file-path (::pipeline/file-path result)
82+
:diff (::pipeline/diff result)}))))
6383

6484
(defn write-text-file
6585
"Write content to a non-Clojure text file, with diffing but no linting or formatting.
6686
6787
Parameters:
6888
- file-path: Validated path to the file to write
6989
- content: Content to write to the file
90+
- dry_run: Optional preview mode ('diff' or 'new-source')
7091
7192
Returns:
7293
- A map with :error, :type, :file-path, and :diff keys"
73-
[file-path content]
94+
[file-path content dry_run]
7495
(try
7596
(let [file (io/file file-path)
7697
file-exists? (.exists file)
@@ -82,13 +103,27 @@
82103
(diff-utils/generate-unified-diff old-content content))
83104
"")]
84105

85-
;; Write the content directly
86-
(spit file content)
106+
(cond
107+
;; Return new-source if dry_run="new-source"
108+
(= dry_run "new-source")
109+
{:error false
110+
:dry_run "new-source"
111+
:new-source content}
112+
113+
;; Return just diff for dry_run="diff"
114+
(= dry_run "diff")
115+
{:error false
116+
:dry_run "diff"
117+
:diff diff}
87118

88-
{:error false
89-
:type (if file-exists? "update" "create")
90-
:file-path file-path
91-
:diff diff})
119+
;; Normal operation - write and return full result
120+
:else
121+
(do
122+
(spit file content)
123+
{:error false
124+
:type (if file-exists? "update" "create")
125+
:file-path file-path
126+
:diff diff})))
92127
(catch Exception e
93128
{:error true
94129
:message (str "Error writing file: " (.getMessage e))})))
@@ -101,10 +136,11 @@
101136
Parameters:
102137
- file-path: Validated path to the file to write
103138
- content: Content to write to the file
139+
- dry_run: Optional preview mode ('diff' or 'new-source')
104140
105141
Returns:
106142
- A map with :error, :type, :file-path, and :diff keys"
107-
[nrepl-client-atom file-path content]
143+
[nrepl-client-atom file-path content dry_run]
108144
(if (is-clojure-file? file-path)
109-
(write-clojure-file nrepl-client-atom file-path content)
110-
(write-text-file file-path content)))
145+
(write-clojure-file nrepl-client-atom file-path content dry_run)
146+
(write-text-file file-path content dry_run)))

src/clojure_mcp/tools/file_write/tool.clj

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ Before using this tool:
6363
:required [:file_path :content]})
6464

6565
(defmethod tool-system/validate-inputs :file-write [{:keys [nrepl-client-atom]} inputs]
66-
(let [{:keys [file_path content]} inputs
66+
(let [{:keys [file_path content dry_run]} inputs
6767
nrepl-client (and nrepl-client-atom @nrepl-client-atom)]
6868
(when-not file_path
6969
(throw (ex-info "Missing required parameter: file_path" {:inputs inputs})))
@@ -88,10 +88,11 @@ Before using this tool:
8888

8989
;; Return validated inputs with normalized path
9090
{:file-path validated-path
91-
:content content})))
91+
:content content
92+
:dry_run dry_run})))
9293

9394
(defmethod tool-system/execute-tool :file-write [{:keys [nrepl-client-atom]} inputs]
94-
(let [{:keys [file-path content]} inputs
95+
(let [{:keys [file-path content dry_run]} inputs
9596
;; Capture original content - empty string for new files
9697
_ (when nrepl-client-atom
9798
(let [file (io/file file-path)]
@@ -105,18 +106,31 @@ Before using this tool:
105106
nrepl-client-atom
106107
file-path
107108
""))))
108-
result (core/write-file nrepl-client-atom file-path content)]
109+
result (core/write-file nrepl-client-atom file-path content dry_run)]
109110
;; Update the timestamp if write was successful and we have a client atom
110-
(when (and nrepl-client-atom (not (:error result)))
111+
(when (and nrepl-client-atom (not (:error result)) (not dry_run))
111112
(file-timestamps/update-file-timestamp-to-current-mtime! nrepl-client-atom file-path))
112113
result))
113114

114115
(defmethod tool-system/format-results :file-write [_ result]
115-
(if (:error result)
116+
(cond
116117
;; If there's an error, return the error message
118+
(:error result)
117119
{:result [(:message result)]
118120
:error true}
119-
;; Otherwise, format a successful result
121+
122+
;; Check if this is a dry_run with new-source
123+
(:new-source result)
124+
{:result [(:new-source result)]
125+
:error false}
126+
127+
;; Check if this is a dry_run with diff only (no file-path)
128+
(and (:diff result) (not (:file-path result)))
129+
{:result [(:diff result)]
130+
:error false}
131+
132+
;; Otherwise, format a successful result with preamble
133+
:else
120134
(let [file-type (if (core/is-clojure-file? (:file-path result)) "Clojure" "Text")
121135
response (str file-type " file " (:type result) "d: " (:file-path result))]
122136
(if (seq (:diff result))

test/clojure_mcp/tools/file_write/core_test.clj

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
(testing "Creating a new text file"
6767
(let [new-file (io/file *test-dir* "new-text-file.txt")
6868
content "This is a brand new file."
69-
result (file-write-core/write-text-file (.getPath new-file) content)]
69+
result (file-write-core/write-text-file (.getPath new-file) content nil)]
7070
(is (not (:error result)))
7171
(is (= "create" (:type result)))
7272
(is (= (.getPath new-file) (:file-path result)))
@@ -79,7 +79,7 @@
7979
(let [path (.getPath *test-txt-file*)
8080
original-content (slurp *test-txt-file*)
8181
new-content "This is updated content.\nWith new lines."
82-
result (file-write-core/write-text-file path new-content)]
82+
result (file-write-core/write-text-file path new-content nil)]
8383
(is (not (:error result)))
8484
(is (= "update" (:type result)))
8585
(is (= path (:file-path result)))
@@ -93,7 +93,8 @@
9393
result (file-write-core/write-clojure-file
9494
test-utils/*nrepl-client-atom*
9595
(.getPath new-file)
96-
content)]
96+
content
97+
nil)]
9798
(is (not (:error result)))
9899
(is (= "create" (:type result)))
99100
(is (= (.getPath new-file) (:file-path result)))
@@ -107,7 +108,8 @@
107108
result (file-write-core/write-clojure-file
108109
test-utils/*nrepl-client-atom*
109110
path
110-
new-content)]
111+
new-content
112+
nil)]
111113
(is (not (:error result)))
112114
(is (= "update" (:type result)))
113115
(is (= path (:file-path result)))
@@ -120,7 +122,8 @@
120122
result (file-write-core/write-clojure-file
121123
test-utils/*nrepl-client-atom*
122124
path
123-
unformatted-content)]
125+
unformatted-content
126+
nil)]
124127
(is (not (:error result)))
125128
(is (= "update" (:type result)))
126129
(is (not (str/includes? (slurp *test-clj-file*) "poorly-formatted-fn[x]")))
@@ -132,7 +135,8 @@
132135
result (file-write-core/write-clojure-file
133136
test-utils/*nrepl-client-atom*
134137
path
135-
content-with-missing-paren)]
138+
content-with-missing-paren
139+
nil)]
136140
(is (not (:error result)))
137141
(is (= "update" (:type result)))
138142
(let [saved-content (slurp *test-clj-file*)]
@@ -146,7 +150,8 @@
146150
result (file-write-core/write-clojure-file
147151
test-utils/*nrepl-client-atom*
148152
path
149-
content-with-mismatched-brackets)]
153+
content-with-mismatched-brackets
154+
nil)]
150155
(is (not (:error result)))
151156
(is (= "update" (:type result)))
152157
(let [saved-content (slurp *test-clj-file*)]
@@ -161,7 +166,8 @@
161166
result (file-write-core/write-clojure-file
162167
test-utils/*nrepl-client-atom*
163168
path
164-
content-with-syntax-error)]
169+
content-with-syntax-error
170+
nil)]
165171
;; The test should fail with a specific error
166172
(is (:error result) "Should have error for non-repairable syntax error")
167173
(when (:message result)
@@ -176,7 +182,8 @@
176182
content "(ns dispatch.test)"
177183
result (file-write-core/write-file test-utils/*nrepl-client-atom*
178184
(.getPath clj-file)
179-
content)]
185+
content
186+
nil)]
180187
(is (not (:error result)))
181188
(is (= "create" (:type result)))
182189
(is (.exists clj-file))
@@ -187,7 +194,8 @@
187194
content "Plain text content"
188195
result (file-write-core/write-file test-utils/*nrepl-client-atom*
189196
(.getPath txt-file)
190-
content)]
197+
content
198+
nil)]
191199
(is (not (:error result)))
192200
(is (= "create" (:type result)))
193201
(is (.exists txt-file))

0 commit comments

Comments
 (0)