Skip to content

Commit f72f4f0

Browse files
author
Bruce Hauman
committed
Add tool filtering opts to main/start (:enable-tools, :disable-tools, :add-tools, :remove-tools)
Allow programmatic and CLI control over which tools are exposed without requiring config files. Absolute opts override config values; relative opts (:add-tools/:remove-tools) modify after config resolution, with :add-tools winning on overlap.
1 parent 8a71f1e commit f72f4f0

File tree

4 files changed

+167
-6
lines changed

4 files changed

+167
-6
lines changed

README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,36 @@ Available profiles:
542542

543543
`:config-profile :cli-assist`
544544

545+
#### `:enable-tools`
546+
**Optional** - Allowlist of tool keywords. When provided, replaces any `:enable-tools` value from config. Only the listed tools will be available.
547+
548+
`:enable-tools [:clojure_eval :read_file]`
549+
550+
#### `:disable-tools`
551+
**Optional** - Blocklist of tool keywords. When provided, replaces any `:disable-tools` value from config. The listed tools will be disabled.
552+
553+
`:disable-tools [:bash :dispatch_agent]`
554+
555+
#### `:add-tools`
556+
**Optional** - Force-enable specific tools after config resolution. Removes tools from the disable list, and adds them to the enable list if one is active. This is useful for selectively re-enabling tools that a config profile disables.
557+
558+
`:add-tools [:my_custom_agent]`
559+
560+
#### `:remove-tools`
561+
**Optional** - Force-disable specific tools after config resolution. Adds tools to the disable list, and removes them from the enable list if one is active. This is useful for selectively disabling tools without replacing the entire config.
562+
563+
`:remove-tools [:clojure_eval]`
564+
565+
#### Tool filtering application order
566+
567+
1. Config loaded (home + project + profile merge)
568+
2. `:enable-tools`/`:disable-tools` from opts replace config values (if provided)
569+
3. `:remove-tools` applied (force-disable)
570+
4. `:add-tools` applied (force-enable — wins over `:remove-tools` on overlap)
571+
5. `ENABLE_TOOLS`/`DISABLE_TOOLS` env vars still win over everything
572+
573+
See [Component Filtering](doc/component-filtering.md) for details on how enable/disable lists work in config files.
574+
545575
### Example Usage
546576

547577
```bash
@@ -572,6 +602,15 @@ clojure -Tmcp start :port 7888 :nrepl-env-type :bb
572602

573603
# Using cli-assist profile for CLI coding assistants
574604
clojure -Tmcp start :config-profile :cli-assist
605+
606+
# cli-assist with a custom agent tool re-enabled
607+
clojure -Tmcp start :config-profile :cli-assist :add-tools '[:my_custom_agent]'
608+
609+
# cli-assist but also remove clojure_eval
610+
clojure -Tmcp start :config-profile :cli-assist :remove-tools '[:clojure_eval]'
611+
612+
# Full override — only these two tools
613+
clojure -Tmcp start :enable-tools '[:clojure_eval :read_file]'
575614
```
576615

577616
**Note**: String values need to be properly quoted for the shell, hence `'"value"'` syntax for strings.

src/clojure_mcp/core.clj

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -272,21 +272,41 @@
272272

273273
(def ^:private cli-config-override-keys
274274
"Keys from the startup opts that override config.edn values."
275-
[:shadow-cljs-repl-message])
275+
[:shadow-cljs-repl-message :enable-tools :disable-tools])
276276

277277
(defn- apply-cli-config-overrides
278278
"Applies CLI option overrides to the config attached to an nrepl-client-map.
279-
Only overrides keys that are explicitly provided (non-nil) in opts."
279+
Only overrides keys that are explicitly provided (non-nil) in opts.
280+
281+
After absolute overrides, applies relative modifiers:
282+
- :remove-tools — force-disables tools (adds to :disable-tools, removes from :enable-tools)
283+
- :add-tools — force-enables tools (removes from :disable-tools, adds to :enable-tools if set)
284+
:add-tools wins over :remove-tools on overlap."
280285
[nrepl-client-map opts]
281286
(let [overrides (reduce (fn [m k]
282287
(if (some? (get opts k))
283288
(assoc m k (get opts k))
284289
m))
285290
{}
286-
cli-config-override-keys)]
287-
(if (seq overrides)
288-
(update nrepl-client-map ::config/config merge overrides)
289-
nrepl-client-map)))
291+
cli-config-override-keys)
292+
result (if (seq overrides)
293+
(update nrepl-client-map ::config/config merge overrides)
294+
nrepl-client-map)
295+
remove-tools (when-let [rt (:remove-tools opts)] (set (map keyword rt)))
296+
add-tools (when-let [at (:add-tools opts)] (set (map keyword at)))]
297+
(cond-> result
298+
;; remove-tools: force-disable (add to :disable-tools, remove from :enable-tools)
299+
remove-tools
300+
(-> (update-in [::config/config :disable-tools]
301+
(fn [dt] (vec (distinct (concat (or dt []) remove-tools)))))
302+
(update-in [::config/config :enable-tools]
303+
(fn [et] (when et (vec (remove remove-tools et))))))
304+
;; add-tools: force-enable (remove from :disable-tools, add to :enable-tools if set)
305+
add-tools
306+
(-> (update-in [::config/config :disable-tools]
307+
(fn [dt] (vec (remove add-tools (or dt [])))))
308+
(update-in [::config/config :enable-tools]
309+
(fn [et] (when et (vec (distinct (concat et add-tools))))))))))
290310

291311
(defn create-and-start-nrepl-connection
292312
"Creates an nREPL client map and loads configuration.
@@ -313,6 +333,7 @@
313333
(try
314334
(let [nrepl-client-map (nrepl/create (apply dissoc initial-config
315335
:project-dir :nrepl-env-type :config-profile
336+
:add-tools :remove-tools
316337
cli-config-override-keys))
317338
cli-env-type (:nrepl-env-type initial-config)
318339
_ (log/info "nREPL client map created")

src/clojure_mcp/main.clj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@
6262
Options:
6363
- :not-cwd - If true, does NOT set project-dir to cwd (default: false)
6464
- :port - Optional nREPL port (REPL is optional when project-dir is set)
65+
- :enable-tools - Allowlist of tool keywords, replaces config's :enable-tools
66+
- :disable-tools - Blocklist of tool keywords, replaces config's :disable-tools
67+
- :add-tools - Force-enable tools (removes from :disable-tools, adds to :enable-tools if set)
68+
- :remove-tools - Force-disable tools (adds to :disable-tools, removes from :enable-tools)
69+
:add-tools wins over :remove-tools on overlap.
6570
- All other options supported by start-mcp-server"
6671
[opts]
6772
(let [not-cwd? (get opts :not-cwd false)

test/clojure_mcp/core_test.clj

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
(ns clojure-mcp.core-test
2+
(:require [clojure.test :refer [deftest is testing]]
3+
[clojure-mcp.config :as config]
4+
[clojure-mcp.core]))
5+
6+
(def ^:private apply-cli-config-overrides
7+
@#'clojure-mcp.core/apply-cli-config-overrides)
8+
9+
(defn- make-client [config]
10+
{::config/config config})
11+
12+
(deftest enable-tools-override-test
13+
(testing ":enable-tools from opts replaces config value"
14+
(let [client (make-client {:enable-tools [:foo :bar]})
15+
result (apply-cli-config-overrides client {:enable-tools [:baz]})]
16+
(is (= [:baz] (get-in result [::config/config :enable-tools])))))
17+
18+
(testing ":disable-tools from opts replaces config value"
19+
(let [client (make-client {:disable-tools [:foo]})
20+
result (apply-cli-config-overrides client {:disable-tools [:bar :baz]})]
21+
(is (= [:bar :baz] (get-in result [::config/config :disable-tools]))))))
22+
23+
(deftest remove-tools-test
24+
(testing ":remove-tools adds to :disable-tools"
25+
(let [client (make-client {:disable-tools [:foo]})
26+
result (apply-cli-config-overrides client {:remove-tools [:bar]})]
27+
(is (= [:foo :bar] (get-in result [::config/config :disable-tools])))))
28+
29+
(testing ":remove-tools removes from :enable-tools"
30+
(let [client (make-client {:enable-tools [:foo :bar :baz]})
31+
result (apply-cli-config-overrides client {:remove-tools [:bar]})]
32+
(is (= [:foo :baz] (get-in result [::config/config :enable-tools])))))
33+
34+
(testing ":remove-tools with no existing :disable-tools"
35+
(let [client (make-client {})
36+
result (apply-cli-config-overrides client {:remove-tools [:foo]})]
37+
(is (= [:foo] (get-in result [::config/config :disable-tools])))))
38+
39+
(testing ":remove-tools does not create :enable-tools when nil"
40+
(let [client (make-client {})
41+
result (apply-cli-config-overrides client {:remove-tools [:foo]})]
42+
(is (nil? (get-in result [::config/config :enable-tools]))))))
43+
44+
(deftest add-tools-test
45+
(testing ":add-tools removes from :disable-tools"
46+
(let [client (make-client {:disable-tools [:foo :bar :baz]})
47+
result (apply-cli-config-overrides client {:add-tools [:bar]})]
48+
(is (= [:foo :baz] (get-in result [::config/config :disable-tools])))))
49+
50+
(testing ":add-tools adds to :enable-tools when set"
51+
(let [client (make-client {:enable-tools [:foo]})
52+
result (apply-cli-config-overrides client {:add-tools [:bar]})]
53+
(is (= [:foo :bar] (get-in result [::config/config :enable-tools])))))
54+
55+
(testing ":add-tools does not create :enable-tools when nil"
56+
(let [client (make-client {})
57+
result (apply-cli-config-overrides client {:add-tools [:bar]})]
58+
(is (nil? (get-in result [::config/config :enable-tools]))))))
59+
60+
(deftest add-tools-wins-over-remove-tools-test
61+
(testing ":add-tools wins over :remove-tools on overlap"
62+
(let [client (make-client {:enable-tools [:foo]
63+
:disable-tools [:baz]})
64+
result (apply-cli-config-overrides client {:remove-tools [:bar :qux]
65+
:add-tools [:bar]})]
66+
;; :bar should NOT be in :disable-tools (add-tools removed it)
67+
;; :qux should be in :disable-tools (remove-tools added it, add-tools didn't touch it)
68+
(is (not (some #{:bar} (get-in result [::config/config :disable-tools]))))
69+
(is (some #{:qux} (get-in result [::config/config :disable-tools])))
70+
;; :bar should be in :enable-tools (add-tools added it back)
71+
(is (some #{:bar} (get-in result [::config/config :enable-tools]))))))
72+
73+
(deftest combination-with-config-profile-test
74+
(testing "cli-assist profile with :add-tools re-enables a tool"
75+
;; Simulate cli-assist having a limited enable-tools list
76+
(let [client (make-client {:enable-tools [:clojure_eval :read_file]
77+
:disable-tools []})
78+
result (apply-cli-config-overrides client {:add-tools [:my_custom_agent]})]
79+
(is (some #{:my_custom_agent} (get-in result [::config/config :enable-tools])))))
80+
81+
(testing "cli-assist profile with :remove-tools disables a tool"
82+
(let [client (make-client {:enable-tools [:clojure_eval :read_file :bash]
83+
:disable-tools []})
84+
result (apply-cli-config-overrides client {:remove-tools [:clojure_eval]})]
85+
(is (not (some #{:clojure_eval} (get-in result [::config/config :enable-tools]))))
86+
(is (some #{:clojure_eval} (get-in result [::config/config :disable-tools]))))))
87+
88+
(deftest no-ops-test
89+
(testing "no opts produces no changes"
90+
(let [client (make-client {:enable-tools [:foo] :disable-tools [:bar]})]
91+
(is (= client (apply-cli-config-overrides client {})))))
92+
93+
(testing "string tool names are converted to keywords"
94+
(let [client (make-client {:disable-tools [:foo]})
95+
result (apply-cli-config-overrides client {:remove-tools ["bar"]})]
96+
(is (= [:foo :bar] (get-in result [::config/config :disable-tools]))))))

0 commit comments

Comments
 (0)