Skip to content

Commit

Permalink
Add registry watcher
Browse files Browse the repository at this point in the history
  • Loading branch information
slimslenderslacks committed Jan 24, 2025
1 parent e10b155 commit 64df775
Show file tree
Hide file tree
Showing 8 changed files with 182 additions and 16 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/prompts-docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,7 @@ jobs:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: vonwig/prompts:latest
tags:
- vonwig/prompts:latest
- mcp/run:latest
- mcp/docker:latest
5 changes: 5 additions & 0 deletions functions/inotifywait/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
FROM alpine:latest

RUN apk add inotify-tools

ENTRYPOINT ["/usr/bin/inotifywait"]
7 changes: 7 additions & 0 deletions functions/inotifywait/runbook.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
```sh
docker build -t vonwig/inotifywait .
```

```sh
docker run --rm -v "docker-prompts:/prompts" vonwig/inotifywait -e modify -e create -e delete -m -q /prompts/
```
24 changes: 18 additions & 6 deletions prompts/catalog.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,23 @@ registry:
description: Get information about an NPM project
ref: github:docker/labs-ai-tools-for-devs?ref=main&path=prompts/npm-project.md
icon: https://cdn.jsdelivr.net/npm/simple-icons@v7/icons/npm.svg
SQL Agent:
description: Run a SQL query against a sqlite database
ref: github:docker/labs-ai-tools-for-devs?ref=main&path=prompts/sql/prompt.md
mcp-sqlite:
description: A prompt to seed the database with initial data and demonstrate what you can do with an SQLite MCP Server + Claude
ref: github:docker/labs-ai-tools-for-devs?ref=main&path=prompts/examples/mcp-sqlite.md
icon: https://cdn.jsdelivr.net/npm/simple-icons@v7/icons/sqlite.svg
Recommended Tags:
description: Get recommended tags for a Docker image
ref: github:docker/labs-ai-tools-for-devs?ref=main&path=prompts/recommended_tags.md
curl:
description: Use curl to make HTTP requests
ref: github:docker/labs-ai-tools-for-devs?ref=main&path=prompts/examples/curl.md
icon: https://cdn.jsdelivr.net/npm/simple-icons@v7/icons/curl.svg
hello-world:
description: echo a greeting using a container!
ref: github:docker/labs-ai-tools-for-devs?ref=main&path=prompts/examples/hello_world.md
icon: https://cdn.jsdelivr.net/npm/simple-icons@v7/icons/hello-world.svg
ffmpeg:
description: Use ffmpeg to process video files
ref: github:docker/labs-ai-tools-for-devs?ref=main&path=prompts/examples/ffmpeg.md
icon: https://cdn.jsdelivr.net/npm/simple-icons@v7/icons/ffmpeg.svg
explain_dockerfile:
description: Provide a detailed description, analysis, or annotation of a given Dockerfile.
ref: github:docker/labs-ai-tools-for-devs?ref=main&path=prompts/examples/explain_dockerfile.md
icon: https://cdn.jsdelivr.net/npm/simple-icons@v7/icons/docker.svg
57 changes: 56 additions & 1 deletion src/docker.clj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
[babashka.fs :as fs]
[cheshire.core :as json]
[clojure.core.async :as async]
[clojure.java.io :as io]
[clojure.pprint :refer [pprint]]
[clojure.string :as string]
[creds]
Expand Down Expand Up @@ -182,6 +183,14 @@
:as :bytes
:throw false}))

(defn attach-container-stream-stdout [{:keys [Id]}]
;; this assumes no Tty so the output will be multiplexed back
(curl/post
(format "http://localhost/containers/%s/attach?stderr=false&stdout=true&stream=true" Id)
{:raw-args ["--unix-socket" "/var/run/docker.sock"]
:as :stream
:throw false}))

;; should be 200 and then will have a StatusCode
(defn wait-container [{:keys [Id]}]
(curl/post
Expand Down Expand Up @@ -242,8 +251,54 @@
(and digest (= digest Id))))
(images {}))))

(defn run-streaming-function-with-no-stdin
"run container function with no stdin, and no timeout, but streaming stdout"
[m cb]
(when (not (has-image? (:image m)))
(-pull m))
(let [x (create m)
finished-channel (async/promise-chan)]
(start x)

(async/go
(try
(let [s (:body (attach-container-stream-stdout x))]
(println s)
(doseq [line (line-seq (java.io.BufferedReader. (java.io.InputStreamReader. s)))]
(cb line)))
(catch Throwable e
(println e))))

;; watch the container
(async/go
(wait x)
(async/>! finished-channel {:done :exited}))

;; body is raw PTY output
(let [finish-reason (async/<!! finished-channel)
s (:body (attach x))
info (inspect x)]
(delete x)
(merge
finish-reason
{:pty-output s
:exit-code (-> info :State :ExitCode)
:info info}))))

(comment
(async/thread
(run-streaming-function-with-no-stdin
{:image "vonwig/inotifywait:latest"
:volumes ["docker-prompts:/prompts"]
:command ["-e" "create" "-e" "modify" "-e" "delete" "-q" "-m" "/prompts"]
:opts {:Tty true
:StdinOnce false
:OpenStdin false
:AttachStdin false}}
println)))

(defn run-function
"run container function with no stdin"
"run container function with no stdin, and no streaming output"
[{:keys [timeout] :or {timeout 600000} :as m}]
(when (not (has-image? (:image m)))
(-pull m))
Expand Down
34 changes: 29 additions & 5 deletions src/jsonrpc/db.clj
Original file line number Diff line number Diff line change
@@ -1,25 +1,49 @@
(ns jsonrpc.db
(ns jsonrpc.db
(:require
[clj-yaml.core :as yaml]
git
[jsonrpc.logger :as logger]
prompts))

(def db* (atom {}))

(defn get-prompt-data [{:keys [register] :as opts}]
(logger/info "get-prompt-data " register)
(->> register
(map (fn [ref] [ref (git/prompt-file ref)]))
(map (fn [[ref f]]
(let [m (prompts/get-prompts (assoc opts :prompts f))]
[(or (-> m :metadata :name) ref) m])))
(into {})))

(defn add [opts]
(logger/info "adding prompts" (:register opts))
(let [m (get-prompt-data opts)]
(swap! db* update-in [:mcp.prompts/registry] (fnil merge {}) m)))
(defn add-static-prompts [db m]
(-> db
(update :mcp.prompts/registry (fnil merge {}) m)
(assoc :mcp.prompts/static m)))

(defn add-dynamic-prompts [db m]
(logger/info "dynamic keys" (keys (:mcp.prompts/registry db)))
(logger/info "static keys" (keys (:mcp.prompts/static db)))
(-> db
(assoc :mcp.prompts/registry (merge m (:mcp.prompts/static db)))))

(defn add
"add any static prompts to db"
[opts]
(logger/info "adding static prompts" (:register opts))
(let [prompt-registry (get-prompt-data opts)]
(swap! db* add-static-prompts prompt-registry)))

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

(defn merge [{:keys [registry-content] :as opts}]
(logger/info "adding dynamic prompts" registry-content)
(try
(let [{:keys [registry]} (yaml/parse-string registry-content)
prompt-registry (get-prompt-data (assoc opts :register (map :ref (vals registry))))]
(logger/info "merging" prompt-registry)
(swap! db* add-dynamic-prompts prompt-registry))
(catch Throwable e
(logger/error e "could not merge dynamic prompts"))))
35 changes: 32 additions & 3 deletions src/jsonrpc/server.clj
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
[clojure.core :as c]
[clojure.core.async :as async]
[clojure.pprint :as pprint]
[clojure.string :as string]
docker
git
graph
jsonrpc
Expand Down Expand Up @@ -93,8 +95,8 @@

(defn entry->prompt-listing [k v _messages]
(merge
{:name (str k)}
(select-keys (:metadata v) [:description])))
{:name (str k)}
(select-keys (:metadata v) [:description])))

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

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

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

(publish-tool-list-changed [_ params]
(logger/info "send tool list changed")
(->> params (lsp.server/send-notification server "notifications/tools/list_changed")))
(publish-docker-notify [_ method params]
(lsp.server/send-notification server method params)))
Expand Down Expand Up @@ -289,11 +294,35 @@
:producer producer
:server server}]
(swap! db* merge {:log-path log-path} (dissoc opts :in))
;; register static prompts
(when (:register opts)
(try
(db/add opts)
(catch Throwable t
(logger/error t))))
;; register dynamic prompts
(when (fs/exists? (fs/file "/prompts/registry.yaml"))
(db/merge (assoc opts :registry-content (slurp "/prompts/registry.yaml"))))
;; watch dynamic prompts in background
(async/thread
(docker/run-streaming-function-with-no-stdin
{:image "vonwig/inotifywait:latest"
:volumes ["docker-prompts:/prompts"]
:command ["-e" "create" "-e" "modify" "-e" "delete" "-q" "-m" "/prompts"]
:opts {:Tty true
:StdinOnce false
:OpenStdin false
:AttachStdin false}}
(fn [line]
(logger/info "registry changed" line)
(let [[_dir _event f] (string/split line #"\s+")]
(when (= f "registry.yaml")
(try
(db/merge (assoc opts :registry-content (slurp "/prompts/registry.yaml")))
(producer/publish-tool-list-changed producer {})
(producer/publish-prompt-list-changed producer {})
(catch Throwable t
(logger/error t "unable to parse registry.yaml"))))))))
(monitor-server-logs log-ch)
(logger/info "Starting server...")
[producer (lsp.server/start server components)])))
Expand Down
31 changes: 31 additions & 0 deletions src/jsonrpc/watch.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
(ns jsonrpc.watch
(:require
[babashka.process :as process]
[clj-yaml.core :as yaml]
[clojure.core.async :as async]
[clojure.java.io :as io]
[clojure.string :as string]
[jsonrpc.logger :as logger]))

(def watcher-args
["inotifywait" "-e" "modify" "-e" "create" "-e" "delete" "-m" "-q" "/prompts"])

; split on white space
; only care about registry.yaml
;/prompts/ DELETE registry.yaml
;/prompts/ CREATE registry.yaml
;/prompts/ MODIFY registry.yaml

(defn init [cb]
(async/go
(let [p (apply process/process {:out :stream} watcher-args)
rdr (io/reader (:out p))]
(for [line (line-seq rdr) :let [[_ event f] (string/split line #"\s+")]]
(do
(logger/info (format "event: %s file: %s" event f))
(when (= f "registry.yaml")
(cb
(try
(yaml/parse-string (slurp "/prompts/registry.yaml"))
(catch Throwable _
{})))))))))

0 comments on commit 64df775

Please sign in to comment.