Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 29 additions & 13 deletions src/clojure_mcp/tools/file_edit/pipeline.clj
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
(s/def ::old-string string?)
(s/def ::new-string string?)
(s/def ::nrepl-client-atom (s/nilable #(instance? clojure.lang.Atom %)))

(s/def ::dry-run (s/nilable #{"diff" "new-source"}))
;; Pipeline specific steps

;; Using check-file-modified from form-edit/pipeline instead
Expand Down Expand Up @@ -117,15 +119,16 @@
- file-path: Path to the file to edit
- old-string: String to replace
- new-string: New string to insert
- nrepl-client-atom: Atom containing the nREPL client (optional)
- config: Optional tool configuration map
- dry-run: Optional string, either \"diff\" or \"new-source\" to skip actual file write
- config: Optional tool configuration map with :nrepl-client-atom

Returns:
- A context map with the result of the operation"
[file-path old-string new-string {:keys [nrepl-client-atom] :as config}]
[file-path old-string new-string dry_run {:keys [nrepl-client-atom] :as config}]
(let [initial-ctx {::form-pipeline/file-path file-path
::old-string old-string
::new-string new-string
::dry-run dry_run
::form-pipeline/nrepl-client-atom nrepl-client-atom
::form-pipeline/config config}]
;; Pipeline for existing file edit
Expand All @@ -147,9 +150,14 @@
format-clojure-content ;; Format Clojure files automatically
form-pipeline/determine-file-type ;; This will mark as "update"
form-pipeline/generate-diff ;; Generate diff between old and new
form-pipeline/save-file ;; Save the file
form-pipeline/update-file-timestamp ;; Update the timestamp after save
form-pipeline/highlight-form))) ;; Update the timestamp after save ;; Update the timestamp after save
;; Skip file operations if dry-run is set
(fn [ctx]
(if (::dry-run ctx)
ctx
(-> ctx
form-pipeline/save-file
form-pipeline/update-file-timestamp
form-pipeline/highlight-form)))))) ;; Update the timestamp after save ;; Update the timestamp after save

;; Format result for tool consumption
(defn format-result
Expand All @@ -159,17 +167,25 @@
- ctx: The final context map from the pipeline

Returns:
- A map with :error, :message, and :diff keys, and potentially :repaired"
- A map with :error, :message, and :diff or :new-source keys, and potentially :repaired"
[ctx]
(if (::form-pipeline/error ctx)
{:error true
:message (::form-pipeline/message ctx)}
(cond-> {:error false
:diff (::form-pipeline/diff ctx)
:type (::form-pipeline/type ctx)}
;; Include repaired flag if present
(::form-pipeline/repaired ctx)
(assoc :repaired true))))
(let [dry_run (::dry-run ctx)]
(cond-> {:error false
:type (::form-pipeline/type ctx)}
;; Include repaired flag if present
(::form-pipeline/repaired ctx)
(assoc :repaired true)

;; Return new-source if dry-run is "new-source"
(= dry_run "new-source")
(assoc :new-source (::form-pipeline/output-source ctx))

;; Otherwise return diff (default behavior and for "diff" dry-run)
(not= dry_run "new-source")
(assoc :diff (::form-pipeline/diff ctx))))))

(comment
;; === Examples of using the file-edit pipeline directly ===
Expand Down
8 changes: 4 additions & 4 deletions src/clojure_mcp/tools/file_edit/tool.clj
Original file line number Diff line number Diff line change
Expand Up @@ -81,16 +81,16 @@ To make a file edit, provide the file_path, old_string (the text to replace), an
:new_string new_string))))

(defmethod tool-system/execute-tool :file-edit [{:keys [nrepl-client-atom] :as tool} inputs]
(let [{:keys [file_path old_string new_string]} inputs
result (pipeline/file-edit-pipeline file_path old_string new_string tool)]
(let [{:keys [file_path old_string new_string dry_run]} inputs
result (pipeline/file-edit-pipeline file_path old_string new_string dry_run tool)]
(pipeline/format-result result)))

(defmethod tool-system/format-results :file-edit [_ {:keys [error message diff type repaired]}]
(defmethod tool-system/format-results :file-edit [_ {:keys [error message diff new-source type repaired]}]
(if error
{:error true
:result [message]}
(cond-> {:error false
:result [diff]
:result [(or new-source diff)]
:type type}
;; Include repaired flag if present
repaired (assoc :repaired true))))
Expand Down
15 changes: 7 additions & 8 deletions src/clojure_mcp/tools/form_edit/combined_edit_tool.clj
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,7 @@ Note: For `defmethod` forms, be sure to include the dispatch value (`area :recta
;; Validate inputs implementation
(defmethod tool-system/validate-inputs :clojure-edit-form [{:keys [nrepl-client-atom]} inputs]
(let [file-path (validate-file-path inputs nrepl-client-atom)
;; Accept form_identifier but map it to form_name internally for compatibility
{:keys [form_identifier form_type operation content]} inputs
{:keys [form_identifier form_type operation content dry_run]} inputs
form_name form_identifier]
(when-not form_identifier
(throw (ex-info "Missing required parameter: form_identifier"
Expand All @@ -145,30 +144,30 @@ Note: For `defmethod` forms, be sure to include the dispatch value (`area :recta
(when-not content
(throw (ex-info "Missing required parameter: content"
{:inputs inputs})))
;; Return validated inputs with form_name for backward compatibility
{:file_path file-path
:form_name form_name
:form_type form_type
:operation operation
:content content}))
:content content
:dry_run dry_run}))

;; Execute tool implementation
(defmethod tool-system/execute-tool :clojure-edit-form [{:keys [nrepl-client-atom] :as tool} inputs]
(let [{:keys [file_path form_name form_type operation content]} inputs
(let [{:keys [file_path form_name form_type operation content dry_run]} inputs
edit-type (case operation
"replace" :replace
"insert_before" :before
"insert_after" :after)
result (pipeline/edit-form-pipeline file_path form_name form_type content edit-type tool)
result (pipeline/edit-form-pipeline file_path form_name form_type content edit-type dry_run tool)
formatted-result (pipeline/format-result result)]
formatted-result))

;; Format results implementation
(defmethod tool-system/format-results :clojure-edit-form [_ {:keys [error message diff]}]
(defmethod tool-system/format-results :clojure-edit-form [_ {:keys [error message diff new-source]}]
(if error
{:result [message]
:error true}
{:result [diff]
{:result [(or new-source diff)]
:error false}))

;; Tool registration function
Expand Down
55 changes: 39 additions & 16 deletions src/clojure_mcp/tools/form_edit/pipeline.clj
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,15 @@

(s/def ::nrepl-client-atom (s/nilable #(instance? clojure.lang.Atom %)))

;; Context map that flows through the pipeline
(s/def ::dry-run (s/nilable #{"diff" "new-source"}))

(s/def ::context
(s/keys :req [::file-path]
:opt [::source ::old-content ::new-source-code ::top-level-def-name
::top-level-def-type ::edit-type ::error ::message
::zloc ::offsets ::lint-result ::docstring
::comment-substring ::new-content ::expand-symbols
::diff ::type ::output-source ::nrepl-client-atom]))
::diff ::type ::output-source ::nrepl-client-atom ::dry-run]))

;; Pipeline helper functions

Expand Down Expand Up @@ -536,16 +537,27 @@
- ctx: The final context map from the pipeline

Returns:
- A map with :error, :message, and possibly :offsets and :result keys"
- A map with :error, :message, and possibly :diff or :new-source, and :offsets keys"
[ctx]
(if (::error ctx)
{:error true
:message (::message ctx)}
(let [result-map {:error false}]
(let [dry_run (::dry-run ctx)
result-map {:error false}]
(cond-> result-map
(::offsets ctx) (assoc :offsets (::offsets ctx))
(::output-source ctx) (assoc :result [(::output-source ctx)])
(::diff ctx) (assoc :diff (::diff ctx))))))

;; Return new-source if dry-run is "new-source"
(= dry_run "new-source")
(assoc :new-source (::output-source ctx))

;; Otherwise return diff (default behavior and for "diff" dry-run)
(and (::diff ctx) (not= dry_run "new-source"))
(assoc :diff (::diff ctx))

;; Legacy: include :result if output-source exists
(and (::output-source ctx) (not dry_run))
(assoc :result [(::output-source ctx)])))))

;; Pipeline function definitions

Expand All @@ -558,16 +570,17 @@
- form-type: Type of form (e.g., 'defn', 'defmethod')
- content-str: New content to insert
- edit-type: Type of edit (:replace, :before, :after)
- nrepl-client-atom: Atom containing the nREPL client (optional)
- config: Optional tool configuration map
- dry-run: Optional string, either \"diff\" or \"new-source\" to skip actual file write
- config: Optional tool configuration map with :nrepl-client-atom

Returns a context map with the result of the operation"
[file-path form-name form-type content-str edit-type {:keys [nrepl-client-atom] :as config}]
[file-path form-name form-type content-str edit-type dry_run {:keys [nrepl-client-atom] :as config}]
(let [ctx {::file-path file-path
::top-level-def-name form-name
::top-level-def-type form-type
::new-source-code content-str
::edit-type edit-type
::dry-run dry_run
::nrepl-client-atom nrepl-client-atom
::config config}]
(thread-ctx
Expand All @@ -587,9 +600,13 @@
format-source
determine-file-type
generate-diff
save-file
update-file-timestamp
highlight-form)))
(fn [ctx]
(if (::dry-run ctx)
ctx
(-> ctx
save-file
update-file-timestamp
highlight-form))))))

(defn docstring-edit-pipeline
"Pipeline for editing a docstring in a file.
Expand Down Expand Up @@ -769,17 +786,19 @@
- operation: The operation to perform (:replace, :insert-before, :insert-after)
- replace-all: Whether to apply the operation to all occurrences
- whitespace-sensitive: Whether to match forms exactly as written
- dry_run: Optional string, either \"diff\" or \"new-source\" to skip actual file write
- config: Optional tool configuration map with nrepl-client-atom

Returns:
- A context map with the result of the operation"
[file-path match-form new-form operation replace-all whitespace-sensitive {:keys [nrepl-client-atom] :as config}]
[file-path match-form new-form operation replace-all whitespace-sensitive dry_run {:keys [nrepl-client-atom] :as config}]
(let [ctx {::file-path file-path
::match-form match-form
::new-form new-form
::operation operation
::replace-all replace-all
::whitespace-sensitive whitespace-sensitive
::dry-run dry_run
::nrepl-client-atom nrepl-client-atom
::config config}]
(thread-ctx
Expand All @@ -797,9 +816,13 @@
format-source
determine-file-type
generate-diff
save-file
update-file-timestamp
highlight-form)))
(fn [ctx]
(if (::dry-run ctx)
ctx
(-> ctx
save-file
update-file-timestamp
highlight-form))))))

(comment
;; Example usage of the pipelines
Expand Down
29 changes: 10 additions & 19 deletions src/clojure_mcp/tools/form_edit/tool.clj
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ Note: For `defmethod` forms, be sure to include the dispatch value (`area :recta

(defmethod tool-system/execute-tool :clojure-edit-replace-form [{:keys [nrepl-client-atom] :as tool} inputs]
(let [{:keys [file_path form_name form_type content]} inputs
result (pipeline/edit-form-pipeline file_path form_name form_type content :replace tool)
result (pipeline/edit-form-pipeline file_path form_name form_type content :replace nil tool)
formatted-result (pipeline/format-result result)]
formatted-result))

Expand Down Expand Up @@ -256,7 +256,7 @@ Note: For `defmethod` forms, be sure to include the dispatch value (`area :recta

(defmethod tool-system/execute-tool :clojure-edit-insert-before-form [{:keys [nrepl-client-atom] :as tool} inputs]
(let [{:keys [file_path form_name form_type content]} inputs
result (pipeline/edit-form-pipeline file_path form_name form_type content :before tool)
result (pipeline/edit-form-pipeline file_path form_name form_type content :before nil tool)
formatted-result (pipeline/format-result result)]
formatted-result))

Expand Down Expand Up @@ -348,7 +348,7 @@ Note: For `defmethod` forms, be sure to include the dispatch value (`area :recta

(defmethod tool-system/execute-tool :clojure-edit-insert-after-form [{:keys [nrepl-client-atom] :as tool} inputs]
(let [{:keys [file_path form_name form_type content]} inputs
result (pipeline/edit-form-pipeline file_path form_name form_type content :after tool)
result (pipeline/edit-form-pipeline file_path form_name form_type content :after nil tool)
formatted-result (pipeline/format-result result)]
formatted-result))

Expand Down Expand Up @@ -545,7 +545,7 @@ For reliable results, use a unique substring that appears in only one comment bl
(defmethod tool-system/validate-inputs :clojure-update-sexp
[{:keys [nrepl-client-atom multi-op]} inputs]
(let [file-path (validate-file-path inputs nrepl-client-atom)
{:keys [match_form new_form operation replace_all whitespace_sensitive]} inputs]
{:keys [match_form new_form operation replace_all whitespace_sensitive dry_run]} inputs]
(when-not match_form
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

Validate dry_run values and document in schema; also expose whitespace_sensitive

  • dry_run isn’t validated; accept only "diff" or "new-source" and give a helpful error otherwise.
  • Tool schema lacks dry_run and whitespace_sensitive, so clients may reject these inputs. Add both as optional properties with descriptions.

Suggested changes:

Validation (enforce enum):

 (defmethod tool-system/validate-inputs :clojure-update-sexp
   [{:keys [nrepl-client-atom multi-op]} inputs]
   (let [file-path (validate-file-path inputs nrepl-client-atom)
-        {:keys [match_form new_form operation replace_all whitespace_sensitive dry_run]} inputs]
+        {:keys [match_form new_form operation replace_all whitespace_sensitive dry_run]} inputs]
 ...
+    (when (some? dry_run)
+      (when-not (#{"diff" "new-source"} dry_run)
+        (throw (ex-info "Invalid dry_run: must be \"diff\" or \"new-source\""
+                        {:inputs inputs :dry_run dry_run}))))
 ...
     {:file_path file-path
 ...
-     :whitespace_sensitive (boolean (or whitespace_sensitive false))
-     :dry_run dry_run}))
+     :whitespace_sensitive (boolean (or whitespace_sensitive false))
+     :dry_run dry_run}))

Schema (add properties):

 (defmethod tool-system/tool-schema :clojure-update-sexp [{:keys [multi-op]}]
   {:type :object
    :properties
-   (cond-> {:file_path {:type :string
+   (cond-> {:file_path {:type :string
                         :description "Path to the file to edit"}
            :match_form {:type :string
                         :description "The s-expression to find (include # for anonymous functions)"}
            :new_form {:type :string
                        :description "The s-expression to use for the operation"}
            :replace_all {:type :boolean
                          :description
                          (format "Whether to %s all occurrences (default: false)"
                                  (if multi-op "apply operation to" "replace"))}
+           :whitespace_sensitive {:type :boolean
+                                  :description "When true, treat insignificant whitespace as significant during matching (default: false)"}
+           :dry_run {:type :string
+                     :enum ["diff" "new-source"]
+                     :description "Dry run mode: \"diff\" returns unified diff; \"new-source\" returns full modified source; no file writes performed"}}
      multi-op
      (assoc :operation {:type :string
                         :enum ["replace" "insert_before" "insert_after"]
                         :description "The editing operation to perform"}))

Also applies to: 596-597, 599-606

🤖 Prompt for AI Agents
In src/clojure_mcp/tools/form_edit/tool.clj around line 549 (and also apply same
changes at lines ~596-597 and 599-606): add validation that dry_run accepts only
the strings "diff" or "new-source" and return a clear error if any other value
is provided (e.g., check the param and when-not in the same place as match_form
to throw a helpful error). Then update the tool schema definition to include
optional properties "dry_run" (string, enum ["diff","new-source"], description
"If set, controls output mode; allowed values: 'diff' or 'new-source'") and
"whitespace_sensitive" (boolean, optional, description "If true, treat
whitespace as significant during edits"). Ensure the schema keys are optional
and documented, and wire the validated dry_run value into existing code paths
where dry_run is used.

(throw (ex-info "Missing required parameter: match_form"
{:inputs inputs})))
Expand All @@ -567,16 +567,9 @@ For reliable results, use a unique substring that appears in only one comment bl
(throw (ex-info "Bad parameter: match-form can not be a blank string."
{:inputs inputs})))

;; TODO we can get more sophisticated here... we are handling
;; code repairs deeper inside the actually tools evaluation and
;; this prevents it. Also I think we can actually do comment
;; replacement now but we might need special handling for that.

;; Special handling for empty string
(when-not (str/blank? match_form)
(try
(let [parsed (p/parse-string-all match_form)]
;; Check if there's at least one non-whitespace, non-comment node
(when (zero? (count (n/child-sexprs parsed)))
(throw (ex-info "match_form must contain at least one S-expression (not just comments or whitespace)"
{:inputs inputs}))))
Expand All @@ -587,41 +580,39 @@ For reliable results, use a unique substring that appears in only one comment bl
{:inputs inputs}))))))

(when-not (str/blank? new_form)
;; Validate that new_form is valid Clojure code
(try
(p/parse-string-all new_form)
(catch Exception e
(throw (ex-info (str "Invalid Clojure code in new_form: " (.getMessage e))
{:inputs inputs})))))
;; Return validated inputs
{:file_path file-path
:match_form match_form
:new_form new_form
:operation operation
:replace_all (boolean (if (#{"insert_before" "insert_after"} operation)
false
(or replace_all false)))
:whitespace_sensitive (boolean (or whitespace_sensitive false))}))
:whitespace_sensitive (boolean (or whitespace_sensitive false))
:dry_run dry_run}))

(defmethod tool-system/execute-tool :clojure-update-sexp [{:keys [multi-op nrepl-client-atom] :as tool} inputs]
(let [{:keys [file_path match_form new_form operation replace_all whitespace_sensitive]} inputs
;; Convert operation string to keyword for the pipeline
(let [{:keys [file_path match_form new_form operation replace_all whitespace_sensitive dry_run]} inputs
operation-kw (if-not multi-op
:replace
(condp = operation
"replace" :replace
"insert_before" :insert-before
"insert_after" :insert-after))
result (pipeline/sexp-edit-pipeline
file_path match_form new_form operation-kw replace_all whitespace_sensitive tool)
file_path match_form new_form operation-kw replace_all whitespace_sensitive dry_run tool)
formatted-result (pipeline/format-result result)]
formatted-result))

(defmethod tool-system/format-results :clojure-update-sexp [_ {:keys [error message diff]}]
(defmethod tool-system/format-results :clojure-update-sexp [_ {:keys [error message diff new-source]}]
(if error
{:result [message]
:error true}
{:result [diff]
{:result [(or new-source diff)]
:error false}))

;; Function to register the tool
Expand Down
2 changes: 2 additions & 0 deletions test/clojure_mcp/tools/form_edit/pipeline_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@
"defn"
"(defn example-fn [x y]\n (* x y))"
:replace
nil
*nrepl-client-atom*)
result (sut/format-result pipeline-result)
file-content (slurp file-path)]
Expand All @@ -316,6 +317,7 @@
"comment"
"(comment some test comment)"
:replace
nil
*nrepl-client-atom*)]
(is (true? (::sut/error pipeline-result)))
(is (string? (::sut/message pipeline-result)))
Expand Down