Skip to content

Commit c511fa6

Browse files
committed
Fix error field on tool call outputs
1 parent 3349c02 commit c511fa6

File tree

8 files changed

+99
-49
lines changed

8 files changed

+99
-49
lines changed

docs/protocol.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,11 @@ interface ToolCalledContent {
576576
*/
577577
arguments: string[];
578578

579+
/**
580+
* Whether it was a error
581+
*/
582+
error: boolean;
583+
579584
/**
580585
* the result of the tool call.
581586
*/
@@ -588,12 +593,7 @@ interface ToolCalledContent {
588593
/**
589594
* The content of this output
590595
*/
591-
content: string;
592-
593-
/**
594-
* Whether it was a error
595-
*/
596-
error: boolean;
596+
content: string;
597597
}];
598598
}
599599

src/eca/features/chat.clj

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,17 +213,18 @@
213213
;; Otherwise auto approve
214214
(deliver approved?* true))
215215
(if @approved?*
216-
(let [output (f.tools/call-tool! name arguments @db* config)]
216+
(let [result (f.tools/call-tool! name arguments @db* config)]
217217
(add-to-history! {:role "tool_call" :content tool-call})
218-
(add-to-history! {:role "tool_call_output" :content (assoc tool-call :output output)})
218+
(add-to-history! {:role "tool_call_output" :content (assoc tool-call :output result)})
219219
(swap! tool-call-args-by-id* dissoc id)
220220
(send-content! chat-ctx :assistant
221221
{:type :toolCalled
222222
:origin (tool-name->origin name all-tools)
223223
:name name
224224
:arguments arguments
225+
:error (:error result)
225226
:id id
226-
:outputs (:contents output)})
227+
:outputs (:contents result)})
227228
{:new-messages (get-in @db* [:chats chat-id :messages])})
228229
(do
229230
(add-to-history! {:role "tool_call" :content tool-call})

src/eca/features/tools/mcp.clj

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
McpSchema$CallToolRequest
1515
McpSchema$ClientCapabilities
1616
McpSchema$Content
17+
McpSchema$GetPromptRequest
18+
McpSchema$Prompt
19+
McpSchema$PromptArgument
20+
McpSchema$PromptMessage
1721
McpSchema$Root
1822
McpSchema$TextContent
1923
McpSchema$Tool
@@ -72,6 +76,26 @@
7276
:tools (get-in db [:mcp-clients mcp-name :tools])
7377
:status status})
7478

79+
(defn ^:private list-server-tools [^ObjectMapper obj-mapper ^McpSyncClient client]
80+
(mapv (fn [^McpSchema$Tool tool-client]
81+
{:name (.name tool-client)
82+
:description (.description tool-client)
83+
;; We convert to json to then read so we have a clojure map
84+
;; TODO avoid this converting to clojure map directly
85+
:parameters (json/parse-string (.writeValueAsString obj-mapper (.inputSchema tool-client)) true)})
86+
(.tools (.listTools client))))
87+
88+
(defn ^:private list-server-prompts [^McpSyncClient client]
89+
(mapv (fn [^McpSchema$Prompt prompt-client]
90+
{:name (.name prompt-client)
91+
:description (.description prompt-client)
92+
:arguments (mapv (fn [^McpSchema$PromptArgument content]
93+
{:name (.name content)
94+
:description (.description content)
95+
:required (.required content)})
96+
(.arguments prompt-client))})
97+
(.prompts (.listPrompts client))))
98+
7599
(defn initialize-servers-async! [{:keys [on-server-updated]} db* config]
76100
(let [workspaces (:workspace-folders @db*)
77101
db @db*
@@ -89,14 +113,8 @@
89113
(doseq [{:keys [name uri]} workspaces]
90114
(.addRoot client (McpSchema$Root. uri name)))
91115
(.initialize client)
92-
(let [tools (mapv (fn [^McpSchema$Tool tool-client]
93-
{:name (.name tool-client)
94-
:description (.description tool-client)
95-
;; We convert to json to then read so we have a clojure map
96-
;; TODO avoid this converting to clojure map directly
97-
:parameters (json/parse-string (.writeValueAsString obj-mapper (.inputSchema tool-client)) true)})
98-
(.tools (.listTools client)))]
99-
(swap! db* assoc-in [:mcp-clients name :tools] tools))
116+
(swap! db* assoc-in [:mcp-clients name :tools] (list-server-tools obj-mapper client))
117+
(swap! db* assoc-in [:mcp-clients name :prompts] (list-server-prompts client))
100118
(on-server-updated (->server name server-config :running @db*)))
101119
(catch Exception e
102120
(logger/warn logger-tag (format "Could not initialize MCP server %s. Error: %s" name (.getMessage e)))
@@ -118,20 +136,43 @@
118136
(let [result (.callTool ^McpSyncClient mcp-client
119137
(McpSchema$CallToolRequest. name arguments))]
120138
(logger/debug logger-tag "ToolCall result: " result)
121-
{:contents (map (fn [content]
139+
{:error (.isError result)
140+
:contents (map (fn [content]
122141
(case (.type ^McpSchema$Content content)
123142
"text" {:type :text
124-
:error (.isError result)
125143
:content (.text ^McpSchema$TextContent content)}
126144
nil))
127145
(.content result))})
128146
(catch Exception e
129-
{:contents [{:type :text
130-
:error true
147+
{:error true
148+
:contents [{:type :text
131149
:content (.getMessage e)}]}))))
132150

151+
(defn get-prompt! [^String name ^Map arguments db]
152+
(let [mcp-client (->> (vals (:mcp-clients db))
153+
(keep (fn [{:keys [client prompts]}]
154+
(when (some #(= name (:name %)) prompts)
155+
client)))
156+
first)
157+
prompt (.getPrompt ^McpSyncClient mcp-client (McpSchema$GetPromptRequest. name arguments))]
158+
{:description (.description prompt)
159+
:messages (mapv (fn [^McpSchema$PromptMessage message]
160+
{:role (.role message)
161+
:content (.content message)})
162+
(.messages prompt))}))
163+
133164
(defn shutdown! [db*]
134165
(doseq [[_name {:keys [_client]}] (:mcp-clients @db*)]
135166
;; TODO NoClassDefFound being thrown for some reason
136167
#_(.closeGracefully ^McpSyncClient client))
137168
(swap! db* assoc :mcp-clients {}))
169+
170+
(comment
171+
(def db* (atom user/*db*))
172+
(user/with-workspace-root "/home/greg/dev/eca/eca"
173+
(initialize-servers-async! {:on-server-updated println}
174+
db*
175+
{:mcpTimeoutSeconds 10
176+
:mcpServers {"fetch" {:command "docker" :args ["run" "-i" "--rm" "mcp/fetch"]}}}))
177+
(:prompts (second (first (:mcp-clients @db*))))
178+
(get-prompt! "fetch" {"url" "https://eca.dev"} @db*))

src/eca/features/tools/shell.clj

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,18 +38,16 @@
3838
(logger/debug logger-tag "Command executed:" result)
3939
(if (zero? (:exit result))
4040
(tools.util/single-text-content (:out result))
41-
{:contents (remove nil?
41+
{:error true
42+
:contents (remove nil?
4243
(concat [{:type :text
43-
:content (str "Exit code " (:exit result))
44-
:error true}]
44+
:content (str "Exit code " (:exit result))}]
4545
(when-not (string/blank? err)
4646
[{:type :text
47-
:content (str "Stderr:\n" err)
48-
:error true}])
47+
:content (str "Stderr:\n" err)}])
4948
(when-not (string/blank? out)
5049
[{:type :text
51-
:content (str "Stdout:\n" out)
52-
:error true}])))})))))
50+
:content (str "Stdout:\n" out)}])))})))))
5351

5452
(def definitions
5553
{"eca_shell_command"

src/eca/features/tools/util.clj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
[eca.shared :as shared]))
66

77
(defn single-text-content [text & [error]]
8-
{:contents [{:type :text
9-
:content text
10-
:error (boolean error)}]})
8+
{:error (boolean error)
9+
:contents [{:type :text
10+
:content text}]})
1111

1212
(defn workspace-roots-strs [db]
1313
(->> (:workspace-folders db)

test/eca/features/chat_test.clj

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -218,15 +218,16 @@ for allowed directories and then list files"
218218
(on-message-received {:type :text :text "/foo/bar"})
219219
(on-message-received {:type :finish}))
220220
:call-tool-mock
221-
(constantly {:contents [{:type :text :error false :content "Allowed directories: /foo/bar"}]})})]
221+
(constantly {:error false
222+
:contents [{:type :text :content "Allowed directories: /foo/bar"}]})})]
222223
(is (match?
223224
{chat-id {:id chat-id
224225
:messages [{:role "user" :content "List the files you are allowed to see"}
225226
{:role "assistant" :content "Ok, working on it"}
226227
{:role "tool_call" :content {:id "call-1" :name "list_allowed_directories" :arguments {}}}
227228
{:role "tool_call_output" :content {:id "call-1" :name "list_allowed_directories" :arguments {}
228-
:output {:contents [{:content "Allowed directories: /foo/bar"
229-
:error false
229+
:output {:error false
230+
:contents [{:content "Allowed directories: /foo/bar"
230231
:type :text}]}}}
231232
{:role "assistant" :content "I can see: \n/foo/bar"}]}}
232233
(:chats (h/db))))
@@ -239,7 +240,7 @@ for allowed directories and then list files"
239240
{:role :assistant :content {:type :text :text " working on it"}}
240241
{:role :assistant :content {:type :toolCallPrepare :id "call-1" :name "list_allowed_directories" :arguments-text "" :manual-approval false}}
241242
{:role :assistant :content {:type :toolCallRun :id "call-1" :name "list_allowed_directories" :arguments {} :manual-approval false}}
242-
{:role :assistant :content {:type :toolCalled :id "call-1" :name "list_allowed_directories" :arguments {} :outputs [{:content "Allowed directories: /foo/bar" :error false :type :text}]}}
243+
{:role :assistant :content {:type :toolCalled :id "call-1" :name "list_allowed_directories" :arguments {} :outputs [{:content "Allowed directories: /foo/bar" :type :text}]}}
243244
{:role :assistant :content {:type :text :text "I can see: \n"}}
244245
{:role :assistant :content {:type :text :text "/foo/bar"}}
245246
{:role :system :content {:state :finished :type :progress}}]}

test/eca/features/tools/shell_test.clj

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
(deftest shell-command-test
1111
(testing "inexistent working_directory"
1212
(is (match?
13-
{:contents [{:type :text
14-
:error true
13+
{:error true
14+
:contents [{:type :text
1515
:content (format "working directory %s does not exist" (h/file-path "/baz"))}]}
1616
(with-redefs [fs/exists? (constantly false)]
1717
((get-in f.tools.shell/definitions ["eca_shell_command" :handler])
@@ -20,11 +20,10 @@
2020
{:db {:workspace-folders [{:uri (h/file-uri "file:///project/foo") :name "foo"}]}})))))
2121
(testing "command exited with non-zero exit code"
2222
(is (match?
23-
{:contents [{:type :text
24-
:error true
23+
{:error true
24+
:contents [{:type :text
2525
:content "Exit code 1"}
2626
{:type :text
27-
:error true
2827
:content "Stderr:\nSome error"}]}
2928
(with-redefs [fs/exists? (constantly true)
3029
shell/sh (constantly {:exit 1 :err "Some error"})]
@@ -33,8 +32,8 @@
3332
{:db {:workspace-folders [{:uri (h/file-uri "file:///project/foo") :name "foo"}]}})))))
3433
(testing "command succeeds"
3534
(is (match?
36-
{:contents [{:type :text
37-
:error false
35+
{:error false
36+
:contents [{:type :text
3837
:content "Some text"}]}
3938
(with-redefs [fs/exists? (constantly true)
4039
shell/sh (constantly {:exit 0 :out "Some text" :err "Other text"})]
@@ -43,8 +42,8 @@
4342
{:db {:workspace-folders [{:uri (h/file-uri "file:///project/foo") :name "foo"}]}})))))
4443
(testing "command succeeds with different working directory"
4544
(is (match?
46-
{:contents [{:type :text
47-
:error false
45+
{:error false
46+
:contents [{:type :text
4847
:content "Some text"}]}
4948
(with-redefs [fs/exists? (constantly true)
5049
shell/sh (constantly {:exit 0 :out "Some text" :err "Other text"})]
@@ -54,8 +53,8 @@
5453
{:db {:workspace-folders [{:uri (h/file-uri "file:///project/foo") :name "foo"}]}})))))
5554
(testing "command does not fails if not in excluded config"
5655
(is (match?
57-
{:contents [{:type :text
58-
:error false
56+
{:error false
57+
:contents [{:type :text
5958
:content "Some text"}]}
6059
(with-redefs [fs/exists? (constantly true)
6160
shell/sh (constantly {:exit 0 :out "Some text" :err "Other text"})]
@@ -66,8 +65,8 @@
6665
:excludeCommands ["ls" "cd"]}}}})))))
6766
(testing "command fails if in excluded config"
6867
(is (match?
69-
{:contents [{:type :text
70-
:error true
68+
{:error true
69+
:contents [{:type :text
7170
:content "Cannot run command 'rm' because it is excluded by eca config."}]}
7271
(with-redefs [fs/exists? (constantly true)]
7372
((get-in f.tools.shell/definitions ["eca_shell_command" :handler])

test/eca/main_test.clj

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,14 @@
3131
(testing "final options"
3232
(is (string? (:exit-message (#'main/parse ["--help"]))))
3333
(is (string? (:exit-message (#'main/parse ["-h"]))))
34-
(is (string? (:exit-message (#'main/parse ["--version"]))))))
34+
(is (string? (:exit-message (#'main/parse ["--version"])))))
35+
(testing "options + commands"
36+
(is (match?
37+
{:action "server"
38+
:options {:log-level "debug"}}
39+
(#'main/parse ["--log-level" "debug" "server"]))))
40+
(testing "commands + options"
41+
(is (match?
42+
{:action "server"
43+
:options {:log-level "debug"}}
44+
(#'main/parse ["server" "--log-level" "debug"])))))

0 commit comments

Comments
 (0)