Skip to content
Closed
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
56 changes: 44 additions & 12 deletions src/clojure_mcp/config.clj
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
(:require
[clojure.java.io :as io]
[clojure.string :as str]
[clojure.pprint :as pprint]
[clojure-mcp.dialects :as dialects]
[clojure-mcp.config.schema :as schema]
[clojure.edn :as edn]
Expand Down Expand Up @@ -34,13 +35,46 @@
[]
(io/file (System/getProperty "user.home") ".clojure-mcp" "config.edn"))

(defn- load-home-config
(defn load-home-config
"Loads configuration from ~/.clojure-mcp/config.edn.
Returns empty map if the file doesn't exist."
[]
(let [home-config-file (get-home-config-path)]
(load-config-file (.getPath home-config-file))))

(defn save-config-file
"Saves a config map to the specified path in EDN format.
Creates parent directories if they don't exist."
[config-map file-path]
(let [file (io/file file-path)
parent (.getParentFile file)]
(when (and parent (not (.exists parent)))
(.mkdirs parent))
(try
(spit file (with-out-str (pprint/pprint config-map)))
(log/info "Config saved to:" (.getPath file))
true
(catch Exception e
(log/error e "Failed to save config file:" (.getPath file))
false))))

(defn- get-project-config-path
"Returns the path to the project config file in the given working directory."
[working-dir]
(io/file working-dir ".clojure-mcp" "config.edn"))

(defn load-project-config
"Loads configuration from .clojure-mcp/config.edn in the given working directory.
Returns empty map if the file doesn't exist."
[working-dir]
(let [project-config-file (get-project-config-path working-dir)]
(load-config-file (.getPath project-config-file))))

(defn save-project-config
"Saves a config map to the project's config file."
[config-map working-dir]
(save-config-file config-map (get-project-config-path working-dir)))

(defn- deep-merge
"Deeply merges maps, with the second map taking precedence.
For non-map values, the second value wins."
Expand All @@ -61,11 +95,11 @@
"Validates a sequence of config files with their file paths.
Takes a sequence of maps with :config and :file-path keys.
Validates each config sequentially and throws on the first error found.

Each map should have:
- :config - The configuration map to validate
- :file-path - The path to the config file (for error reporting)

Throws ExceptionInfo with:
- :type ::schema-error
- :errors - Validation errors from Malli
Expand Down Expand Up @@ -137,7 +171,7 @@
(conj {:config home-config
:file-path (.getCanonicalPath home-config-path)})

;; Only validate project config if it exists and has content
;; Only validate project config if it exists and has content
(seq project-config)
(conj {:config project-config
:file-path (.getCanonicalPath project-config-file)})))
Expand Down Expand Up @@ -331,13 +365,13 @@

(defn tool-id-enabled?
"Check if a tool should be enabled based on :enable-tools and :disable-tools config.

Logic:
- If :enable-tools is nil, all tools are enabled (unless in :disable-tools)
- If :enable-tools is [], no tools are enabled
- If :enable-tools is provided, only those tools are enabled
- :disable-tools is then applied to remove tools from the enabled set

Both config lists can contain strings or keywords - they are normalized to keywords."
[nrepl-client-map tool-id]
(let [enable-tools (get-enable-tools nrepl-client-map)
Expand Down Expand Up @@ -365,13 +399,13 @@

(defn prompt-name-enabled?
"Check if a prompt should be enabled based on :enable-prompts and :disable-prompts config.

Logic:
- If :enable-prompts is nil, all prompts are enabled (unless in :disable-prompts)
- If :enable-prompts is [], no prompts are enabled
- If :enable-prompts is provided, only those prompts are enabled
- :disable-prompts is then applied to remove prompts from the enabled set

Prompt names are converted to keywords for comparison.
Both config lists can contain strings or keywords."
[nrepl-client-map prompt-name]
Expand All @@ -398,13 +432,13 @@

(defn resource-name-enabled?
"Check if a resource should be enabled based on :enable-resources and :disable-resources config.

Logic:
- If :enable-resources is nil, all resources are enabled (unless in :disable-resources)
- If :enable-resources is [], no resources are enabled
- If :enable-resources is provided, only those resources are enabled
- :disable-resources is then applied to remove resources from the enabled set

Resource names are used as strings for comparison.
Both config lists should contain strings."
[nrepl-client-map resource-name]
Expand Down Expand Up @@ -444,5 +478,3 @@
Uses set-config* to perform the actual update."
[nrepl-client-atom k v]
(swap! nrepl-client-atom set-config* k v))


9 changes: 5 additions & 4 deletions src/clojure_mcp/tools.clj
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@

(def experimental-tool-syms
"Symbols for experimental tool creation functions"
['clojure-mcp.tools.scratch-pad.tool/scratch-pad-tool])
['clojure-mcp.tools.scratch-pad.tool/scratch-pad-tool
'clojure-mcp.tools.manage-prompts.tool/manage-prompts-tool])

;; Note: introspection tools are already included in read-only-tool-syms
;; This is kept for documentation purposes but not used in all-tool-syms
Expand Down Expand Up @@ -111,7 +112,7 @@

(defn build-custom-tools
"Build a custom set of tools from provided symbols.

Example:
(build-custom-tools nrepl-client-atom
['clojure-mcp.tools.eval.tool/eval-code
Expand All @@ -122,15 +123,15 @@

(defn filter-tools
"Filters tools based on enable/disable lists.

Args:
- all-tools: Vector of all available tools
- enable-tools: Can be:
- nil: returns empty vector (no tools)
- :all: returns all tools (minus disabled)
- [...]: returns only specified tools (minus disabled)
- disable-tools: List of tool IDs to disable

Returns: Filtered vector of tools"
[all-tools enable-tools disable-tools]
(cond
Expand Down
160 changes: 160 additions & 0 deletions src/clojure_mcp/tools/manage_prompts/tool.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
(ns clojure-mcp.tools.manage-prompts.tool
"Implementation of the manage-prompts tool for saving and managing user prompts in config."
(:require
[clojure-mcp.tool-system :as tool-system]
[clojure-mcp.config :as config]
[clojure.string :as str]
[clojure.tools.logging :as log]))

(defmethod tool-system/tool-name :manage-prompts [_]
"manage_prompts")

(defmethod tool-system/tool-description :manage-prompts [_]
"Manages user-defined prompts stored in the project configuration file.

Operations:
- add_prompt: Add or update a prompt with a name and content
- remove_prompt: Remove a prompt by name
- list_prompts: List all saved prompts
- get_prompt: Get a specific prompt by name

Prompts are stored in the project config file (.clojure-mcp/config.edn) under the :prompts key.

Use this tool when users want to:
- Save a prompt they've created for later reuse
- View their saved prompts
- Update or remove existing prompts
- Retrieve a specific prompt

Example usage:
- Save: manage_prompts(op: \"add_prompt\", prompt_name: \"code-reviewer\", prompt_content: \"Review the following code for best practices...\")
- List: manage_prompts(op: \"list_prompts\")
- Get: manage_prompts(op: \"get_prompt\", prompt_name: \"code-reviewer\")
- Remove: manage_prompts(op: \"remove_prompt\", prompt_name: \"code-reviewer\")")

(defmethod tool-system/tool-schema :manage-prompts [_]
{:type "object"
:properties {"op" {:type "string"
:enum ["add_prompt" "remove_prompt" "list_prompts" "get_prompt"]
:description "The operation to perform: add_prompt, remove_prompt, list_prompts, or get_prompt"}
"prompt_name" {:type "string"
:description "Name of the prompt (required for add_prompt, remove_prompt, get_prompt)"}
"prompt_content" {:type "string"
:description "The prompt text content (required for add_prompt)"}}
:required ["op"]})

(defmethod tool-system/validate-inputs :manage-prompts [_ {:keys [op prompt_name prompt_content] :as inputs}]
(when-not op
(throw (ex-info "Missing required parameter: op" {:inputs inputs})))

(when-not (#{"add_prompt" "remove_prompt" "list_prompts" "get_prompt"} op)
(throw (ex-info "Invalid operation. Must be one of: add_prompt, remove_prompt, list_prompts, get_prompt"
{:op op :inputs inputs})))

;; Operation-specific validation
(case op
"add_prompt" (do
(when-not prompt_name
(throw (ex-info "Missing required parameter for add_prompt: prompt_name" {:inputs inputs})))
(when-not prompt_content
(throw (ex-info "Missing required parameter for add_prompt: prompt_content" {:inputs inputs}))))
("remove_prompt" "get_prompt") (when-not prompt_name
(throw (ex-info (str "Missing required parameter for " op ": prompt_name")
{:inputs inputs})))
"list_prompts" nil) ;; no additional validation needed

inputs)

(defmethod tool-system/execute-tool :manage-prompts [{:keys [working-dir]} {:keys [op prompt_name prompt_content]}]
(try
(let [result (case op
"add_prompt"
(let [current-config (config/load-project-config working-dir)
updated-config (assoc-in current-config [:prompts prompt_name] prompt_content)]
(config/save-project-config updated-config working-dir)
{:message (str "Prompt '" prompt_name "' saved successfully")
:prompt_name prompt_name})

"remove_prompt"
(let [current-config (config/load-project-config working-dir)]
(if (get-in current-config [:prompts prompt_name])
(let [updated-config (update current-config :prompts dissoc prompt_name)]
(config/save-project-config updated-config working-dir)
{:message (str "Prompt '" prompt_name "' removed successfully")
:prompt_name prompt_name})
{:message (str "Prompt '" prompt_name "' not found")
:prompt_name prompt_name}))

"list_prompts"
(let [current-config (config/load-project-config working-dir)
prompts (:prompts current-config)]
{:prompts (or prompts {})
:count (count (or prompts {}))})

"get_prompt"
(let [current-config (config/load-project-config working-dir)
prompt (get-in current-config [:prompts prompt_name])]
(if prompt
{:prompt_name prompt_name
:prompt_content prompt}
{:message (str "Prompt '" prompt_name "' not found")
:prompt_name prompt_name})))]
{:result result
:error false})
(catch Exception e
(log/error e (str "Error executing manage_prompts operation: " (.getMessage e)))
{:error true
:result (str "Error: " (.getMessage e))})))

(defmethod tool-system/format-results :manage-prompts [_ {:keys [error result]}]
(if error
{:result [result]
:error true}
(let [formatted (case (:message result)
nil
;; list_prompts or get_prompt with results
(if (:prompts result)
(if (empty? (:prompts result))
"No prompts saved yet."
(str "Saved prompts (" (:count result) "):\n"
(str/join "\n"
(map (fn [[k v]]
(let [desc (if (map? v)
(or (:description v)
(:content v)
(str v))
(str v))
preview (subs desc 0 (min 60 (count desc)))]
(str "- " k ": " preview
(when (> (count desc) 60) "..."))))
(:prompts result)))))
;; get_prompt with result
(let [content (:prompt_content result)]
(str "Prompt: " (:prompt_name result) "\n\n"
(if (map? content)
(str/join "\n"
[(when (:description content)
(str "Description: " (:description content)))
(when (:content content)
(str "Content:\n" (:content content)))
(when (:file-path content)
(str "File: " (:file-path content)))])
content))))
;; Messages from add/remove operations or not found
(:message result))]
{:result [formatted]
:error false})))

(defn create-manage-prompts-tool [nrepl-client-atom]
(let [working-dir (config/get-nrepl-user-dir @nrepl-client-atom)]
{:tool-type :manage-prompts
:nrepl-client-atom nrepl-client-atom
:working-dir working-dir}))

(defn manage-prompts-tool
"Returns the registration map for the manage-prompts tool.

Parameters:
- nrepl-client-atom: Atom containing the nREPL client"
[nrepl-client-atom]
(tool-system/registration-map (create-manage-prompts-tool nrepl-client-atom)))