Skip to content

Commit 64df775

Browse files
Add registry watcher
1 parent e10b155 commit 64df775

File tree

8 files changed

+182
-16
lines changed

8 files changed

+182
-16
lines changed

.github/workflows/prompts-docker.yml

+4-1
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,7 @@ jobs:
3333
context: .
3434
platforms: linux/amd64,linux/arm64
3535
push: true
36-
tags: vonwig/prompts:latest
36+
tags:
37+
- vonwig/prompts:latest
38+
- mcp/run:latest
39+
- mcp/docker:latest

functions/inotifywait/Dockerfile

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
FROM alpine:latest
2+
3+
RUN apk add inotify-tools
4+
5+
ENTRYPOINT ["/usr/bin/inotifywait"]

functions/inotifywait/runbook.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
```sh
2+
docker build -t vonwig/inotifywait .
3+
```
4+
5+
```sh
6+
docker run --rm -v "docker-prompts:/prompts" vonwig/inotifywait -e modify -e create -e delete -m -q /prompts/
7+
```

prompts/catalog.yaml

+18-6
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,23 @@ registry:
33
description: Get information about an NPM project
44
ref: github:docker/labs-ai-tools-for-devs?ref=main&path=prompts/npm-project.md
55
icon: https://cdn.jsdelivr.net/npm/simple-icons@v7/icons/npm.svg
6-
SQL Agent:
7-
description: Run a SQL query against a sqlite database
8-
ref: github:docker/labs-ai-tools-for-devs?ref=main&path=prompts/sql/prompt.md
6+
mcp-sqlite:
7+
description: A prompt to seed the database with initial data and demonstrate what you can do with an SQLite MCP Server + Claude
8+
ref: github:docker/labs-ai-tools-for-devs?ref=main&path=prompts/examples/mcp-sqlite.md
99
icon: https://cdn.jsdelivr.net/npm/simple-icons@v7/icons/sqlite.svg
10-
Recommended Tags:
11-
description: Get recommended tags for a Docker image
12-
ref: github:docker/labs-ai-tools-for-devs?ref=main&path=prompts/recommended_tags.md
10+
curl:
11+
description: Use curl to make HTTP requests
12+
ref: github:docker/labs-ai-tools-for-devs?ref=main&path=prompts/examples/curl.md
13+
icon: https://cdn.jsdelivr.net/npm/simple-icons@v7/icons/curl.svg
14+
hello-world:
15+
description: echo a greeting using a container!
16+
ref: github:docker/labs-ai-tools-for-devs?ref=main&path=prompts/examples/hello_world.md
17+
icon: https://cdn.jsdelivr.net/npm/simple-icons@v7/icons/hello-world.svg
18+
ffmpeg:
19+
description: Use ffmpeg to process video files
20+
ref: github:docker/labs-ai-tools-for-devs?ref=main&path=prompts/examples/ffmpeg.md
21+
icon: https://cdn.jsdelivr.net/npm/simple-icons@v7/icons/ffmpeg.svg
22+
explain_dockerfile:
23+
description: Provide a detailed description, analysis, or annotation of a given Dockerfile.
24+
ref: github:docker/labs-ai-tools-for-devs?ref=main&path=prompts/examples/explain_dockerfile.md
1325
icon: https://cdn.jsdelivr.net/npm/simple-icons@v7/icons/docker.svg

src/docker.clj

+56-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
[babashka.fs :as fs]
55
[cheshire.core :as json]
66
[clojure.core.async :as async]
7+
[clojure.java.io :as io]
78
[clojure.pprint :refer [pprint]]
89
[clojure.string :as string]
910
[creds]
@@ -182,6 +183,14 @@
182183
:as :bytes
183184
:throw false}))
184185

186+
(defn attach-container-stream-stdout [{:keys [Id]}]
187+
;; this assumes no Tty so the output will be multiplexed back
188+
(curl/post
189+
(format "http://localhost/containers/%s/attach?stderr=false&stdout=true&stream=true" Id)
190+
{:raw-args ["--unix-socket" "/var/run/docker.sock"]
191+
:as :stream
192+
:throw false}))
193+
185194
;; should be 200 and then will have a StatusCode
186195
(defn wait-container [{:keys [Id]}]
187196
(curl/post
@@ -242,8 +251,54 @@
242251
(and digest (= digest Id))))
243252
(images {}))))
244253

254+
(defn run-streaming-function-with-no-stdin
255+
"run container function with no stdin, and no timeout, but streaming stdout"
256+
[m cb]
257+
(when (not (has-image? (:image m)))
258+
(-pull m))
259+
(let [x (create m)
260+
finished-channel (async/promise-chan)]
261+
(start x)
262+
263+
(async/go
264+
(try
265+
(let [s (:body (attach-container-stream-stdout x))]
266+
(println s)
267+
(doseq [line (line-seq (java.io.BufferedReader. (java.io.InputStreamReader. s)))]
268+
(cb line)))
269+
(catch Throwable e
270+
(println e))))
271+
272+
;; watch the container
273+
(async/go
274+
(wait x)
275+
(async/>! finished-channel {:done :exited}))
276+
277+
;; body is raw PTY output
278+
(let [finish-reason (async/<!! finished-channel)
279+
s (:body (attach x))
280+
info (inspect x)]
281+
(delete x)
282+
(merge
283+
finish-reason
284+
{:pty-output s
285+
:exit-code (-> info :State :ExitCode)
286+
:info info}))))
287+
288+
(comment
289+
(async/thread
290+
(run-streaming-function-with-no-stdin
291+
{:image "vonwig/inotifywait:latest"
292+
:volumes ["docker-prompts:/prompts"]
293+
:command ["-e" "create" "-e" "modify" "-e" "delete" "-q" "-m" "/prompts"]
294+
:opts {:Tty true
295+
:StdinOnce false
296+
:OpenStdin false
297+
:AttachStdin false}}
298+
println)))
299+
245300
(defn run-function
246-
"run container function with no stdin"
301+
"run container function with no stdin, and no streaming output"
247302
[{:keys [timeout] :or {timeout 600000} :as m}]
248303
(when (not (has-image? (:image m)))
249304
(-pull m))

src/jsonrpc/db.clj

+29-5
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,49 @@
1-
(ns jsonrpc.db
1+
(ns jsonrpc.db
22
(:require
3+
[clj-yaml.core :as yaml]
34
git
45
[jsonrpc.logger :as logger]
56
prompts))
67

78
(def db* (atom {}))
89

910
(defn get-prompt-data [{:keys [register] :as opts}]
11+
(logger/info "get-prompt-data " register)
1012
(->> register
1113
(map (fn [ref] [ref (git/prompt-file ref)]))
1214
(map (fn [[ref f]]
1315
(let [m (prompts/get-prompts (assoc opts :prompts f))]
1416
[(or (-> m :metadata :name) ref) m])))
1517
(into {})))
1618

17-
(defn add [opts]
18-
(logger/info "adding prompts" (:register opts))
19-
(let [m (get-prompt-data opts)]
20-
(swap! db* update-in [:mcp.prompts/registry] (fnil merge {}) m)))
19+
(defn add-static-prompts [db m]
20+
(-> db
21+
(update :mcp.prompts/registry (fnil merge {}) m)
22+
(assoc :mcp.prompts/static m)))
23+
24+
(defn add-dynamic-prompts [db m]
25+
(logger/info "dynamic keys" (keys (:mcp.prompts/registry db)))
26+
(logger/info "static keys" (keys (:mcp.prompts/static db)))
27+
(-> db
28+
(assoc :mcp.prompts/registry (merge m (:mcp.prompts/static db)))))
29+
30+
(defn add
31+
"add any static prompts to db"
32+
[opts]
33+
(logger/info "adding static prompts" (:register opts))
34+
(let [prompt-registry (get-prompt-data opts)]
35+
(swap! db* add-static-prompts prompt-registry)))
2136

2237
(comment
2338
(add {:register ["github:docker/labs-ai-tools-for-devs?path=prompts/examples/explain_dockerfile.md"
2439
"github:docker/labs-ai-tools-for-devs?path=prompts/examples/hello_world.md"]}))
2540

41+
(defn merge [{:keys [registry-content] :as opts}]
42+
(logger/info "adding dynamic prompts" registry-content)
43+
(try
44+
(let [{:keys [registry]} (yaml/parse-string registry-content)
45+
prompt-registry (get-prompt-data (assoc opts :register (map :ref (vals registry))))]
46+
(logger/info "merging" prompt-registry)
47+
(swap! db* add-dynamic-prompts prompt-registry))
48+
(catch Throwable e
49+
(logger/error e "could not merge dynamic prompts"))))

src/jsonrpc/server.clj

+32-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
[clojure.core :as c]
77
[clojure.core.async :as async]
88
[clojure.pprint :as pprint]
9+
[clojure.string :as string]
10+
docker
911
git
1012
graph
1113
jsonrpc
@@ -93,8 +95,8 @@
9395

9496
(defn entry->prompt-listing [k v _messages]
9597
(merge
96-
{:name (str k)}
97-
(select-keys (:metadata v) [:description])))
98+
{:name (str k)}
99+
(select-keys (:metadata v) [:description])))
98100

99101
(defmethod lsp.server/receive-request "prompts/list" [_ {:keys [db*]} params]
100102
;; TODO might contain a cursor
@@ -211,7 +213,8 @@
211213
(defrecord TimbreLogger []
212214
logger/ILogger
213215
(setup [this]
214-
(let [log-path (str (fs/file "/prompts/docker-mcp-server.out"))]
216+
(fs/create-dirs (fs/file "/prompts" "log"))
217+
(let [log-path (str (fs/file "/prompts/log/docker-mcp-server.out"))]
215218
(timbre/merge-config! {:middleware [#(assoc % :hostname_ "")]
216219
:appenders {:println {:enabled? false}
217220
:spit (appenders/spit-appender {:fname log-path})}})
@@ -253,6 +256,7 @@
253256
(->> params (lsp.server/send-notification server "notifications/message")))
254257

255258
(publish-prompt-list-changed [_ params]
259+
(logger/info "send prompt list changed")
256260
(->> params (lsp.server/send-notification server "notifications/prompts/list_changed")))
257261

258262
(publish-resource-list-changed [_ params]
@@ -262,6 +266,7 @@
262266
(->> params (lsp.server/send-notification server "notifications/resources/updated")))
263267

264268
(publish-tool-list-changed [_ params]
269+
(logger/info "send tool list changed")
265270
(->> params (lsp.server/send-notification server "notifications/tools/list_changed")))
266271
(publish-docker-notify [_ method params]
267272
(lsp.server/send-notification server method params)))
@@ -289,11 +294,35 @@
289294
:producer producer
290295
:server server}]
291296
(swap! db* merge {:log-path log-path} (dissoc opts :in))
297+
;; register static prompts
292298
(when (:register opts)
293299
(try
294300
(db/add opts)
295301
(catch Throwable t
296302
(logger/error t))))
303+
;; register dynamic prompts
304+
(when (fs/exists? (fs/file "/prompts/registry.yaml"))
305+
(db/merge (assoc opts :registry-content (slurp "/prompts/registry.yaml"))))
306+
;; watch dynamic prompts in background
307+
(async/thread
308+
(docker/run-streaming-function-with-no-stdin
309+
{:image "vonwig/inotifywait:latest"
310+
:volumes ["docker-prompts:/prompts"]
311+
:command ["-e" "create" "-e" "modify" "-e" "delete" "-q" "-m" "/prompts"]
312+
:opts {:Tty true
313+
:StdinOnce false
314+
:OpenStdin false
315+
:AttachStdin false}}
316+
(fn [line]
317+
(logger/info "registry changed" line)
318+
(let [[_dir _event f] (string/split line #"\s+")]
319+
(when (= f "registry.yaml")
320+
(try
321+
(db/merge (assoc opts :registry-content (slurp "/prompts/registry.yaml")))
322+
(producer/publish-tool-list-changed producer {})
323+
(producer/publish-prompt-list-changed producer {})
324+
(catch Throwable t
325+
(logger/error t "unable to parse registry.yaml"))))))))
297326
(monitor-server-logs log-ch)
298327
(logger/info "Starting server...")
299328
[producer (lsp.server/start server components)])))

src/jsonrpc/watch.clj

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
(ns jsonrpc.watch
2+
(:require
3+
[babashka.process :as process]
4+
[clj-yaml.core :as yaml]
5+
[clojure.core.async :as async]
6+
[clojure.java.io :as io]
7+
[clojure.string :as string]
8+
[jsonrpc.logger :as logger]))
9+
10+
(def watcher-args
11+
["inotifywait" "-e" "modify" "-e" "create" "-e" "delete" "-m" "-q" "/prompts"])
12+
13+
; split on white space
14+
; only care about registry.yaml
15+
;/prompts/ DELETE registry.yaml
16+
;/prompts/ CREATE registry.yaml
17+
;/prompts/ MODIFY registry.yaml
18+
19+
(defn init [cb]
20+
(async/go
21+
(let [p (apply process/process {:out :stream} watcher-args)
22+
rdr (io/reader (:out p))]
23+
(for [line (line-seq rdr) :let [[_ event f] (string/split line #"\s+")]]
24+
(do
25+
(logger/info (format "event: %s file: %s" event f))
26+
(when (= f "registry.yaml")
27+
(cb
28+
(try
29+
(yaml/parse-string (slurp "/prompts/registry.yaml"))
30+
(catch Throwable _
31+
{})))))))))

0 commit comments

Comments
 (0)