diff --git a/CHANGELOG.md b/CHANGELOG.md index 72fb69a82..114f24cf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## 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. - Use a JSON-RPC `ping` (instead of `initialize`) for the OAuth auth-discovery probe, so the probe POST is never counted as a real handshake by servers or tests that track requests by method name. ## 0.134.1 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 62356d15e..e94df0082 100644 --- a/src/eca/features/tools/mcp.clj +++ b/src/eca/features/tools/mcp.clj @@ -860,13 +860,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) @@ -881,7 +879,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 354350ae0..18500c095 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"