From f26c5fe2983871fdf60637a431b29c9a3e705c37 Mon Sep 17 00:00:00 2001 From: Juha Itkonen Date: Mon, 11 May 2026 12:47:23 +0300 Subject: [PATCH] Fix MCP routing and OpenAI optional tool params MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Route MCP tool calls through the server encoded by the selected full tool name so duplicate short names across servers do not collide. Also emit OpenAI Responses function tools with strict disabled to preserve optional MCP parameters. 🤖 Generated with [eca](https://eca.dev) Co-Authored-By: eca-agent --- CHANGELOG.md | 3 +++ src/eca/features/tools.clj | 8 ++++---- src/eca/features/tools/mcp.clj | 14 ++++++------- src/eca/features/tools/mcp/clojure_mcp.clj | 2 +- src/eca/llm_providers/openai.clj | 3 ++- test/eca/features/tools/mcp_test.clj | 24 ++++++++++++++++++++++ test/eca/llm_providers/openai_test.clj | 15 +++++++++++++- 7 files changed, 54 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21cbdd757..a6e621d06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Bugfix: OpenAI Responses tool calls now opt out of strict schema normalization so optional tool parameters remain optional. +- Bugfix: MCP tool calls now route to the selected server when multiple servers expose the same tool name. + ## 0.133.3 - Add unit and integration tests covering parent↔subagent end-to-end communication so regressions like the v0.133.1 spawn-agent breakage are caught automatically. diff --git a/src/eca/features/tools.clj b/src/eca/features/tools.clj index c308e7092..caf9b5ac8 100644 --- a/src/eca/features/tools.clj +++ b/src/eca/features/tools.clj @@ -256,10 +256,10 @@ :call-state-fn call-state-fn :state-transition-fn state-transition-fn :trust trust}) - (f.mcp/call-tool! tool-name arguments {:db db - :db* db* - :config config - :metrics metrics}))) + (f.mcp/call-tool! server-name tool-name arguments {:db db + :db* db* + :config config + :metrics metrics}))) (tools.util/maybe-truncate-output config tool-call-id))] (logger/debug logger-tag "Tool call result: " result) (metrics/count-up! "tool-called" {:name resolved-full-name :error (:error result)} metrics) diff --git a/src/eca/features/tools/mcp.clj b/src/eca/features/tools/mcp.clj index 6351006a8..81f0ccea4 100644 --- a/src/eca/features/tools/mcp.clj +++ b/src/eca/features/tools/mcp.clj @@ -855,13 +855,11 @@ (do-call-tool new-client name arguments nil) (tool-call-error (format "Failed to re-initialize MCP server '%s'" server-name)))) -(defn call-tool! [name arguments {:keys [db db* config metrics]}] - (if-let [[server-name mcp-client needs-reinit?*] - (->> (:mcp-clients db) - (keep (fn [[sn {:keys [client tools needs-reinit?*]}]] - (when (some #(= name (:name %)) tools) - [sn client needs-reinit?*]))) - first)] +(defn call-tool! [server-name name arguments {:keys [db db* config metrics]}] + (if-let [[mcp-client needs-reinit?*] + (when-let [{:keys [client tools needs-reinit?*]} (get-in db [:mcp-clients server-name])] + (when (some #(= name (:name %)) tools) + [client needs-reinit?*]))] (if (and needs-reinit?* @needs-reinit?* db* config metrics) ;; Already flagged — reinit before attempting the call (reinit-and-call-tool! server-name mcp-client db* config metrics name arguments) @@ -876,7 +874,7 @@ (reinit-and-call-tool! server-name mcp-client db* config metrics name arguments) :else result))) - (tool-call-error (format "Tool '%s' not found in any connected MCP server" name)))) + (tool-call-error (format "Tool '%s' not found in MCP server '%s'" name server-name)))) (defn all-prompts [db] (into [] diff --git a/src/eca/features/tools/mcp/clojure_mcp.clj b/src/eca/features/tools/mcp/clojure_mcp.clj index 89620676e..b7ba06f88 100644 --- a/src/eca/features/tools/mcp/clojure_mcp.clj +++ b/src/eca/features/tools/mcp/clojure_mcp.clj @@ -9,7 +9,7 @@ (when (not= "0.1.0" (:version server)) (when ask-approval? (let [path (get args "file_path") - {:keys [error contents]} (f.mcp/call-tool! name (assoc args "dry_run" "new-source") {:db db})] + {:keys [error contents]} (f.mcp/call-tool! (:name server) name (assoc args "dry_run" "new-source") {:db db})] (when-not error (when-let [new-source (some->> contents (filter #(= :text (:type %))) first :text)] (let [{:keys [added removed diff]} (diff/diff (if new-file? diff --git a/src/eca/llm_providers/openai.clj b/src/eca/llm_providers/openai.clj index d5048fc10..7c6d2b35b 100644 --- a/src/eca/llm_providers/openai.clj +++ b/src/eca/llm_providers/openai.clj @@ -183,7 +183,8 @@ {:type "function" :name (:full-name tool) :description (:description tool) - :parameters (:parameters tool)}) + :parameters (:parameters tool) + :strict false}) tools) web-search (conj {:type "web_search"}) image-generation (conj {:type "image_generation" :output_format "png"}))) diff --git a/test/eca/features/tools/mcp_test.clj b/test/eca/features/tools/mcp_test.clj index f44281e95..fe73dda07 100644 --- a/test/eca/features/tools/mcp_test.clj +++ b/test/eca/features/tools/mcp_test.clj @@ -46,6 +46,30 @@ (mcp/all-tools {:mcp-clients {"server1" {:tools tools1} "server2" {:tools tools2}}})))))) +(deftest call-tool-routes-to-server-test + (testing "uses the requested MCP server when tool names collide" + (let [db {:mcp-clients (array-map + "server-a" {:client :client-a + :tools [{:name "shared_tool"}]} + "server-b" {:client :client-b + :tools [{:name "shared_tool"}]})} + calls* (atom [])] + (with-redefs [mcp/do-call-tool (fn [client name arguments needs-reinit?*] + (swap! calls* conj {:client client + :name name + :arguments arguments + :needs-reinit?* needs-reinit?*}) + {:error false + :contents [{:type :text :text (str client)}]})] + (is (= {:error false + :contents [{:type :text :text ":client-b"}]} + (mcp/call-tool! "server-b" "shared_tool" {"query" "value"} {:db db}))) + (is (= [{:client :client-b + :name "shared_tool" + :arguments {"query" "value"} + :needs-reinit?* nil}] + @calls*)))))) + (deftest all-prompts-test (testing "empty db" (is (= [] diff --git a/test/eca/llm_providers/openai_test.clj b/test/eca/llm_providers/openai_test.clj index fdd244921..c9ae03b8a 100644 --- a/test/eca/llm_providers/openai_test.clj +++ b/test/eca/llm_providers/openai_test.clj @@ -409,7 +409,20 @@ {:type "image_generation" :output_format "png"}] (#'llm-providers.openai/->tools [{:full-name "eca__foo" :description "d" :parameters {}}] - true true))))) + true true)))) + (testing "function tools explicitly opt out of Responses strict mode" + (is (= [{:type "function" + :name "mcp__search_records" + :description "Search records" + :parameters {:type "object" + :properties {"limit" {:type "number"}}} + :strict false}] + (#'llm-providers.openai/->tools + [{:full-name "mcp__search_records" + :description "Search records" + :parameters {:type "object" + :properties {"limit" {:type "number"}}}}] + false false))))) (deftest create-response-oauth-preserves-built-in-tools-test (testing "OAuth requests keep web_search and image_generation when capabilities are enabled"