Skip to content

Commit b580349

Browse files
Cache list tools calls
1 parent 2e655a0 commit b580349

File tree

5 files changed

+102
-40
lines changed

5 files changed

+102
-40
lines changed

runbook.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@ docker pull mcp/docker:prerelease
2020

2121
```sh
2222
# docker:command=build-release
23+
VERSION="0.0.5"
2324
docker buildx build \
2425
--builder hydrobuild \
2526
--platform linux/amd64,linux/arm64 \
26-
--tag mcp/docker:0.0.4 \
27+
--tag mcp/docker:$VERSION \
2728
--file Dockerfile \
2829
--push .
29-
docker pull mcp/docker:0.0.4
30+
docker pull mcp/docker:$VERSION
3031
```
3132

3233
```sh

src/extension/docker-compose.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
services:
22
mcp_docker:
3-
image: mcp/docker:0.0.4
3+
image: mcp/docker:0.0.5
44
ports:
55
- 8811:8811
66
volumes:

src/jsonrpc/server.clj

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,23 @@
77
[clojure.core.async :as async]
88
[clojure.pprint :as pprint]
99
[clojure.string :as string]
10-
[jsonrpc.extras]
1110
docker
1211
git
1312
graph
1413
jsonrpc
1514
[jsonrpc.db :as db]
15+
[jsonrpc.extras]
1616
[jsonrpc.logger :as logger]
1717
[jsonrpc.producer :as producer]
18+
[jsonrpc.socket-server :as socket-server]
1819
[lsp4clj.coercer :as coercer]
1920
[lsp4clj.io-chan :as io-chan]
2021
[lsp4clj.io-server :refer [stdio-server]]
21-
[jsonrpc.socket-server :as socket-server]
2222
[lsp4clj.server :as lsp.server]
23+
[mcp.client :as client]
2324
[medley.core :as medley]
2425
[promesa.core :as p]
26+
[prompts.core :refer [get-prompts-dir registry]]
2527
shutdown
2628
state
2729
tools
@@ -82,7 +84,7 @@
8284
(logger/info "Initialized!"))
8385

8486
; level is debug info notice warning error critical alert emergency
85-
(defmethod lsp.server/receive-request "logging/setLevel" [_ {:keys [logger db*]} {:keys [level]}]
87+
(defmethod lsp.server/receive-request "logging/setLevel" [_ {:keys [db*]} {:keys [level]}]
8688
(swap! db* assoc :mcp.log/level level)
8789
{})
8890

@@ -115,7 +117,7 @@
115117

116118
(defmethod lsp.server/receive-request "prompts/get" [_ {:keys [db*]} {:keys [name arguments]}]
117119
(logger/info "prompts/get " name)
118-
(let [{:keys [prompt-function description] :as m}
120+
(let [{:keys [prompt-function description]}
119121
(get
120122
(->> @db*
121123
:mcp.prompts/registry
@@ -164,11 +166,6 @@
164166

165167
(defmethod lsp.server/receive-request "tools/list" [_ {:keys [db*]} _]
166168
;; TODO cursors
167-
(logger/info "tools/list " (->> (:mcp.prompts/registry @db*)
168-
(vals)
169-
(mapcat :functions)
170-
(map :function)
171-
(into [])))
172169
{:tools (->> (:mcp.prompts/registry @db*)
173170
(vals)
174171
(mapcat :functions)
@@ -271,13 +268,6 @@
271268

272269
(def producers (atom []))
273270

274-
(defn get-prompts-dir []
275-
(if (fs/exists? (fs/file "/prompts"))
276-
"/prompts"
277-
(format "%s/prompts" (System/getenv "HOME"))))
278-
279-
(def registry "/prompts/registry.yaml")
280-
281271
(defn- init-dynamic-prompt-watcher [opts]
282272
(async/thread
283273
(let [{x :container}
@@ -290,7 +280,7 @@
290280
(let [[_dir _event f] (string/split line #"\s+")]
291281
(when (= f "registry.yaml")
292282
(try
293-
(db/add-refs (logger/trace (into [] (db/registry-refs registry))))
283+
(db/add-refs (into [] (db/registry-refs registry)))
294284
(doseq [producer @producers]
295285
(try
296286
(producer/publish-tool-list-changed producer {})
@@ -315,6 +305,8 @@
315305
(docker/delete x))))))
316306

317307
(defn initialize-prompts [opts]
308+
;; initialize mcp cache
309+
(client/initialize-cache)
318310
;; register static prompts
319311
(doseq [[s content] (->> (fs/list-dir (get-prompts-dir))
320312
(filter (fn [f] (= "md" (fs/extension f))))

src/mcp/client.clj

Lines changed: 78 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,30 @@
11
(ns mcp.client
22
(:require
3+
[babashka.fs :as fs]
34
[cheshire.core :as json]
45
[clojure.core.async :as async]
6+
[clojure.edn :as edn]
57
[docker]
6-
[jsonrpc.logger :as logger]))
8+
[flatland.ordered.map :refer [ordered-map]]
9+
[jsonrpc.logger :as logger]
10+
[prompts.core :refer [get-prompts-dir]]
11+
repl))
712

813
(def counter (atom 0))
914

10-
(defn- mcp-stdio-stateless-server [container]
15+
(defn- mcp-stdio-stateless-server
16+
"create a running container with an attached socket-client
17+
and a running Thread that is reading messages from the socket (both stdout and stderr)
18+
- there is also one go process blocked waiting for the container to exit
19+
20+
returns
21+
a map with the container info plus request and notification functions
22+
to write jsonrpc messages to the socket,
23+
and a dead-channel channel that will emit :stopped or :closed when the container stops for any reason
24+
the request function will return a promise channel for the matching response
25+
the notification function is just a side-effect
26+
"
27+
[container]
1128
(docker/check-then-pull container)
1229
(let [x (docker/create (assoc container
1330
:opts {:StdinOnce true
@@ -41,7 +58,6 @@
4158

4259
;; real stdout message
4360
(and block (:stdout block))
44-
4561
(let [message (try (json/parse-string (:stdout block) keyword) (catch Throwable _))]
4662
(if-let [p (get @response-promises (:id message))]
4763
(async/put! p message)
@@ -50,11 +66,9 @@
5066
c ([v _] v)
5167
(async/timeout 15000) :timeout)))
5268

53-
;; channel is closed
69+
;; channel is closed
5470
(nil? block)
55-
(do
56-
(logger/info "channel closed")
57-
(async/put! dead-channel :closed))
71+
(async/put! dead-channel :closed)
5872

5973
;; non-stdout message probably
6074
:else
@@ -78,13 +92,13 @@
7892
:notification
7993
(fn [message]
8094
(try
81-
(docker/write-to-stdin socket-channel (str (json/generate-string (assoc message :jsonrpc "2.0")) "\n\n"))
82-
(catch Throwable t
83-
(println "error closing " t))))
95+
(docker/write-to-stdin socket-channel (str (json/generate-string (assoc message :jsonrpc "2.0")) "\n\n"))
96+
(catch Throwable t
97+
(println "error closing " t))))
8498
:dead-channel dead-channel)))))
8599

86100
(defn with-running-mcp
87-
"send a message to an mcp servers and then shut it down
101+
"send a message to an mcp server and then shuts it down
88102
params
89103
container-definition - for the mcp server
90104
f - function to generate a jsonrpc request to send post initialize
@@ -111,7 +125,6 @@
111125
(request (f)) ([v _] v)
112126
dead-channel ([v _] v)
113127
(async/timeout 15000) :timeout)]
114-
(logger/debug (format "%s response %s" (:image container-definition) response))
115128
(f1 response)))
116129
(do
117130
(logger/error (format
@@ -155,19 +168,63 @@
155168
{:name "create_customer"
156169
:arguments {:name "Jim Clark"}})))
157170

171+
(defn -get-tools [container-definition]
172+
(async/<!!
173+
(with-running-mcp
174+
(docker/inject-secret-transform container-definition)
175+
(fn [] {:method "tools/list" :params {}})
176+
(fn [response]
177+
(->> (-> response :result :tools)
178+
(map #(assoc % :container (assoc container-definition :type :mcp)))
179+
(into []))))))
180+
181+
(defn mcp-metadata-cache-file [] (fs/file (get-prompts-dir) "mcp-metadata-cache.edn"))
182+
(def mcp-metadata-cache (atom {}))
183+
(def cache-channel (async/chan))
184+
(defn initialize-cache []
185+
(swap! mcp-metadata-cache (constantly
186+
(try
187+
(edn/read-string
188+
{:readers {'ordered/map (fn [pairs] (into (ordered-map pairs)))}}
189+
(slurp (mcp-metadata-cache-file)))
190+
(catch Throwable e
191+
(logger/error "error initializing cache " e)
192+
{})))))
193+
194+
(async/go-loop []
195+
(let [[k v] (async/<! cache-channel)
196+
updated-cache (swap! mcp-metadata-cache assoc k v)]
197+
(spit
198+
(mcp-metadata-cache-file)
199+
(pr-str updated-cache))
200+
(recur)))
201+
202+
(defn inspect-image [container-definition]
203+
(:Id
204+
(docker/image-inspect
205+
(-> (docker/images {"reference" [(:image container-definition)]})
206+
first))))
207+
208+
(defn add-digest [container-definition]
209+
(docker/check-then-pull container-definition)
210+
(assoc container-definition :digest (inspect-image container-definition)))
211+
212+
(def cached-mcp-get-tools
213+
(memoize (fn [{:keys [digest] :as container-definition}]
214+
(if-let [m (get @mcp-metadata-cache digest)]
215+
m
216+
(let [m (-get-tools container-definition)]
217+
(async/>!! cache-channel [digest m])
218+
m)))))
219+
158220
(defn get-tools [container-definition]
159-
(with-running-mcp
160-
(docker/inject-secret-transform container-definition)
161-
(fn [] {:method "tools/list" :params {}})
162-
(fn [response]
163-
(->> (-> response :result :tools)
164-
(map #(assoc % :container (assoc container-definition :type :mcp)))
165-
(into [])))))
221+
(cached-mcp-get-tools
222+
(add-digest container-definition)))
166223

167224
(defn get-mcp-tools-from-prompt
168225
[coll]
169226
(->> coll
170-
(mapcat (comp async/<!! get-tools :container))
227+
(mapcat (comp get-tools :container))
171228
(map (fn [tool]
172229
{:type "function"
173230
:function (-> tool
@@ -176,6 +233,7 @@
176233
(into [])))
177234

178235
(comment
236+
(repl/setup-stdout-logger)
179237
(docker/inject-secret-transform {:image "mcp/stripe:latest"
180238
:secrets {:stripe.api_key "API_KEY"}
181239
:command ["--tools=all"

src/prompts/core.clj

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
(ns prompts.core
2+
(:require
3+
[babashka.fs :as fs]))
4+
5+
(defn get-prompts-dir []
6+
(if (fs/exists? (fs/file "/prompts"))
7+
"/prompts"
8+
(format "%s/prompts" (System/getenv "HOME"))))
9+
10+
(def registry "/prompts/registry.yaml")
11+

0 commit comments

Comments
 (0)