Skip to content

Commit 736cf9a

Browse files
committed
Base MCP server support for openai
1 parent c40d0e2 commit 736cf9a

File tree

18 files changed

+242
-26
lines changed

18 files changed

+242
-26
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- Fix ollama servers discovery
66
- Fix `.eca/config.json` read from workspace root
7+
- Add support for MCP servers
78

89
## 0.0.2
910

deps.edn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
org.clojure/core.async {:mvn/version "1.8.741"}
44
org.babashka/cli {:mvn/version "0.8.65"}
55
com.github.clojure-lsp/lsp4clj {:mvn/version "1.13.1"}
6+
io.modelcontextprotocol.sdk/mcp {:mvn/version "0.10.0"}
67
borkdude/dynaload {:mvn/version "0.3.5"}
78
babashka/fs {:mvn/version "0.5.26"}
89
hato/hato {:mvn/version "1.0.0"}

docs/protocol.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,8 @@ interface ChatContentReceivedParams {
394394
type ChatContent =
395395
| TextContent
396396
| ProgressContent
397-
| FileChangeContent;
397+
| FileChangeContent
398+
| MCPToolCallContent;
398399

399400
/**
400401
* Simple text message from the LLM
@@ -487,6 +488,33 @@ interface FileChangeContent {
487488
};
488489
}];
489490
}
491+
492+
/**
493+
* MCP tool calls that LLM may trigger.
494+
*/
495+
interface MCPToolCallContent {
496+
type: 'mcpToolCall';
497+
498+
/**
499+
* id of the tool call
500+
*/
501+
id: string;
502+
503+
/**
504+
* Name of the tool
505+
*/
506+
name: string;
507+
508+
/*
509+
* Arguments of this tool call
510+
*/
511+
arguments: string[];
512+
513+
/**
514+
* Whether this call requires manual approval from the user.
515+
*/
516+
manualApproval: boolean;
517+
}
490518
```
491519

492520
### Chat Query Context (↩️)

src/eca/config.clj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@
1313
[clojure.string :as string]
1414
[eca.shared :as shared]))
1515

16+
(set! *warn-on-reflection* true)
17+
1618
(def ^:private initial-config
1719
{:openai-api-key nil
1820
:anthropic-api-key nil
1921
:rules []
22+
:mcp-timeout-seconds 10
2023
:mcp-servers []
2124
:ollama {:host "http://localhost"
2225
:port 11434}

src/eca/db.clj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
(ns eca.db)
22

3+
(set! *warn-on-reflection* true)
4+
35
(defonce initial-db
46
{:client-info {}
57
:workspace-folders []

src/eca/features/chat.clj

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55
[clojure.string :as string]
66
[eca.config :as config]
77
[eca.features.index :as f.index]
8+
[eca.features.mcp :as f.mcp]
89
[eca.features.rules :as f.rules]
910
[eca.llm-api :as llm-api]
1011
[eca.messenger :as messenger]
1112
[eca.shared :as shared]))
1213

14+
(set! *warn-on-reflection* true)
15+
1316
(defn ^:private raw-contexts->refined [contexts]
1417
(mapcat (fn [{:keys [type path]}]
1518
(case type
@@ -50,6 +53,19 @@
5053
ollama-model
5154
(:default-model db)))
5255

56+
(defn ^:private all-mcp-tools! [chat-id request-id messenger db*]
57+
(when-not (f.mcp/tools-cached? @db*)
58+
(messenger/chat-content-received
59+
messenger
60+
{:chat-id chat-id
61+
:request-id request-id
62+
:role :system
63+
:content {:type :progress
64+
:state :running
65+
:text "Finding MCPs"}})
66+
(f.mcp/cache-tools! db*))
67+
(f.mcp/list-tools @db*))
68+
5369
(defn prompt
5470
[{:keys [message model behavior contexts chat-id request-id]}
5571
db*
@@ -86,6 +102,7 @@
86102
chosen-model (or model (default-model db))
87103
past-messages (get-in db [:chats chat-id :messages] [])
88104
user-prompt message
105+
mcp-tools (all-mcp-tools! chat-id request-id messenger db*)
89106
received-msgs* (atom "")]
90107
(swap! db* update-in [:chats chat-id :messages] (fnil conj []) {:role "user" :content user-prompt})
91108
(messenger/chat-content-received
@@ -102,6 +119,7 @@
102119
:context context-str
103120
:past-messages past-messages
104121
:config config
122+
:mcp-tools mcp-tools
105123
:on-first-message-received (fn [_]
106124
(messenger/chat-content-received
107125
messenger
@@ -133,6 +151,17 @@
133151
:role :system
134152
:content {:type :progress
135153
:state :finished}})))
154+
:on-tool-called (fn [{:keys [name arguments]}]
155+
(messenger/chat-content-received
156+
messenger
157+
{:chat-id chat-id
158+
:request-id request-id
159+
:role :assistant
160+
:content {:type :mcpToolCall
161+
:name name
162+
:arguments arguments
163+
:manual-approval false}})
164+
(f.mcp/call-tool! name arguments @db*))
136165
:on-error (fn [{:keys [message exception]}]
137166
(messenger/chat-content-received
138167
messenger

src/eca/features/index.clj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
(:require
33
[clojure.java.shell :as shell]))
44

5+
(set! *warn-on-reflection* true)
6+
57
(defn ignore? [filename root-filename config]
68
(boolean
79
(some

src/eca/features/mcp.clj

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
(ns eca.features.mcp
2+
(:require
3+
[cheshire.core :as json]
4+
[eca.logger :as logger])
5+
(:import
6+
[com.fasterxml.jackson.databind ObjectMapper]
7+
[io.modelcontextprotocol.client McpClient McpSyncClient]
8+
[io.modelcontextprotocol.client.transport ServerParameters StdioClientTransport]
9+
[io.modelcontextprotocol.spec
10+
McpSchema$CallToolRequest
11+
McpSchema$ClientCapabilities
12+
McpSchema$Content
13+
McpSchema$Root
14+
McpSchema$TextContent
15+
McpSchema$Tool
16+
McpTransport]
17+
[java.time Duration]
18+
[java.util List Map]))
19+
20+
(set! *warn-on-reflection* true)
21+
22+
(def ^:private logger-tag "[MCP]")
23+
24+
(defn ^:private ->transport ^McpTransport [{:keys [command args]}]
25+
(StdioClientTransport.
26+
(-> (ServerParameters/builder ^String command)
27+
(.args ^List args)
28+
(.build))))
29+
30+
(defn ^:private ->client ^McpSyncClient [transport config]
31+
(-> (McpClient/sync transport)
32+
(.requestTimeout (Duration/ofSeconds (:mcp-timeout-seconds config)))
33+
(.capabilities (-> (McpSchema$ClientCapabilities/builder)
34+
(.roots true)
35+
(.build)))
36+
(.build)))
37+
38+
(defn initialize! [db* config]
39+
(doseq [[name server-config] (:mcp-servers config)]
40+
(try
41+
(when-not (get-in @db* [:mcp-clients name])
42+
(let [transport (->transport server-config)
43+
client (->client transport config)]
44+
(swap! db* assoc-in [:mcp-clients name :client] client)
45+
(doseq [{:keys [name uri]} (:workspace-folders @db*)]
46+
(.addRoot client (McpSchema$Root. uri name)))
47+
(.initialize client)))
48+
(catch Exception e
49+
(logger/warn logger-tag (format "Could not initialize MCP server %s. Error: %s" name (.getMessage e)))))))
50+
51+
(defn tools-cached? [db]
52+
(boolean (:mcp-tools db)))
53+
54+
(defn cache-tools! [db*]
55+
(let [obj-mapper (ObjectMapper.)]
56+
(doseq [[name {:keys [client]}] (:mcp-clients @db*)]
57+
(doseq [^McpSchema$Tool tool-client (.tools (.listTools ^McpSyncClient client))]
58+
(let [tool {:name (.name tool-client)
59+
:mcp-name name
60+
:mcp-client client
61+
:description (.description tool-client)
62+
;; We convert to json to then read so we have the clojrue map
63+
;; TODO avoid this converting to clojure map directly
64+
:parameters (json/parse-string (.writeValueAsString obj-mapper (.inputSchema tool-client)) true)}]
65+
(swap! db* assoc-in [:mcp-tools (:name tool)] tool))))))
66+
67+
(defn list-tools [db]
68+
(vals (:mcp-tools db)))
69+
70+
(defn call-tool! [^String name ^Map arguments db]
71+
(let [result (.callTool ^McpSyncClient (get-in db [:mcp-tools name :mcp-client])
72+
(McpSchema$CallToolRequest. name arguments))]
73+
(if (.isError result)
74+
{:error (.content result)}
75+
{:contents (map (fn [content]
76+
(case (.type ^McpSchema$Content content)
77+
"text" {:type :text
78+
:content (.text ^McpSchema$TextContent content)}
79+
nil))
80+
(.content result))})))
81+
82+
(defn shutdown! [db*]
83+
(doseq [[_name {:keys [_client]}] (:mcp-clients @db*)]
84+
;; TODO NoClassDefFound being thrown for some reason
85+
#_(.closeGracefully ^McpSyncClient client))
86+
(swap! db* assoc
87+
:mcp-clients {}
88+
:mcp-tools {}))

src/eca/features/rules.clj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
[clojure.string :as string]
55
[eca.shared :as shared]))
66

7+
(set! *warn-on-reflection* true)
8+
79
(defn ^:private file-rules [roots]
810
(->> roots
911
(mapcat (fn [{:keys [uri]}]

src/eca/handlers.clj

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
[eca.config :as config]
44
[eca.db :as db]
55
[eca.features.chat :as f.chat]
6+
[eca.features.mcp :as f.mcp]
67
[eca.llm-api :as llm-api]
78
[eca.logger :as logger]))
89

10+
(set! *warn-on-reflection* true)
11+
912
(defn ^:private initialize-extra-models! [db*]
1013
(let [config (config/all @db*)]
1114
(when-let [ollama-models (seq (llm-api/extra-models config))]
@@ -14,13 +17,15 @@
1417
(defn initialize [{:keys [db*]} params]
1518
(logger/logging-task
1619
:eca/initialize
20+
(swap! db* assoc
21+
:client-info (:client-info params)
22+
:workspace-folders (:workspace-folders params)
23+
:client-capabilities (:capabilities params)
24+
:chat-behavior (or (-> params :initialization-options :chat-behavior) (:chat-behavior @db*)))
1725
(let [config (config/all @db*)]
18-
(swap! db* assoc
19-
:client-info (:client-info params)
20-
:workspace-folders (:workspace-folders params)
21-
:client-capabilities (:capabilities params)
22-
:chat-behavior (or (-> params :initialization-options :chat-behavior) (:chat-behavior @db*)))
2326
(initialize-extra-models! db*)
27+
;; TODO initialize async with progress support
28+
(f.mcp/initialize! db* config)
2429
{:models (:models @db*)
2530
:chat-default-model (f.chat/default-model @db*)
2631
:chat-behaviors (:chat-behaviors @db*)
@@ -30,6 +35,7 @@
3035
(defn shutdown [{:keys [db*]}]
3136
(logger/logging-task
3237
:eca/shutdown
38+
(f.mcp/shutdown! db*)
3339
(reset! db* db/initial-db)
3440
nil))
3541

0 commit comments

Comments
 (0)