Skip to content

Commit 4863ca7

Browse files
Resource Streaming (#74)
* Switch mcp/gdrive over to vonwig while we different auth * Add function call that does not expect exit but still waits for one message * Infra for resource streams is in place
1 parent 02e94b1 commit 4863ca7

File tree

15 files changed

+503
-70
lines changed

15 files changed

+503
-70
lines changed

dev/catalog.clj

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@
66
[flatland.ordered.map :refer [ordered-map]]
77
git
88
[markdown :as markdown-parser]
9+
[mcp.client :as client]
910
[medley.core :as medley]
1011
prompts
1112
repl))
1213

1314
(defn mcp-metadata-cache [f]
1415
(edn/read-string
15-
{:readers {'ordered/map (fn [pairs] (into (ordered-map pairs)))}}
16-
(slurp f)))
16+
{:readers {'ordered/map (fn [pairs] (into (ordered-map pairs)))}}
17+
(slurp f)))
1718

1819
(comment
1920
(mcp-metadata-cache "test/resources/mcp-metadata-cache.edn"))
@@ -66,10 +67,28 @@
6667
:registry
6768
vals
6869
(map :ref)
69-
(map git/prompt-file)))
70+
(map #(conj [%] (git/prompt-file %)))
71+
(into {})))
7072

7173
;; parse all of the current git prompts
72-
(map f->prompt local-prompt-files)
74+
(with-redefs [#'client/get-mcp-tools-from-prompt (constantly [])]
75+
(def all-prompt-files (map (fn [[k v]] [k (f->prompt v)]) local-prompt-files)))
7376

74-
;;
77+
(def all-images
78+
(->>
79+
(concat
80+
(->> (into {} all-prompt-files)
81+
(vals)
82+
(mapcat (comp :mcp :metadata))
83+
(map (comp :image :container))
84+
(into #{}))
85+
(->> (into {} all-prompt-files)
86+
(vals)
87+
(mapcat :functions)
88+
(map (comp :image :container :function))
89+
(into #{})))
90+
(into #{})
91+
(sort)))
92+
93+
;;
7594
(markdown-parser/parse-markdown (slurp "prompts/examples/sequentialthinking.md")))

dev/stdio_client.clj

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
(ns stdio-client
2+
(:require
3+
[babashka.process :as process]
4+
[cheshire.core :as json]
5+
[clojure.core.async :as async]
6+
[clojure.java.io :as io])
7+
(:import
8+
[java.io BufferedOutputStream]))
9+
10+
(defn write [writer m]
11+
(.write writer (json/generate-string m))
12+
(.write writer "\n")
13+
(.flush writer))
14+
15+
(do
16+
(def server (process/process
17+
{:out :stream :in :stream}
18+
"docker" "run" "-i" "--rm" "--workdir=/app"
19+
"-v" "mcp-gdrive:/gdrive-server"
20+
"-e" "GDRIVE_CREDENTIALS_PATH=/gdrive-server/credentials.json"
21+
"-e" "GDRIVE_OAUTH_PATH=/secret/google.gcp-oauth.keys.json"
22+
"--label" "x-secret:google.gcp-oauth.keys.json=/secret/google.gcp-oauth.keys.json"
23+
"vonwig/gdrive:latest"))
24+
25+
(async/thread
26+
(loop []
27+
(let [line (.readLine (io/reader (:out server)))]
28+
(when line
29+
(println "stdout: " line)
30+
(recur)))))
31+
32+
(async/thread
33+
(loop []
34+
(let [line (.readLine (io/reader (:err server)))]
35+
(when line
36+
(println "stderr: " line)
37+
(recur)))))
38+
39+
(def writer (io/writer (:in server)))
40+
41+
(write writer
42+
{:jsonrpc "2.0"
43+
:method "initialize"
44+
:id 0
45+
:params {:protocolVersion "2024-11-05"
46+
:capabilities {}
47+
:clientInfo {:name "Stdio Client" :version "0.1"}}})
48+
(write writer
49+
{:jsonrpc "2.0" :method "notifications/initialized" :params {}}))
50+
51+
52+
(write writer
53+
{:jsonrpc "2.0" :method "resources/list" :params {} :id 1})
54+
55+
(write writer
56+
{:jsonrpc "2.0"
57+
:method "tools/call"
58+
:params
59+
{:name "search"
60+
:arguments {:query "mcp"}}
61+
:id "4"})
62+
63+
(async/thread
64+
(loop []
65+
(let [line (.readLine (io/reader (:out server)))]
66+
(when line
67+
(println "stdout: " line)
68+
(recur)))))
69+
70+
(async/thread
71+
(loop []
72+
(let [line (.readLine (io/reader (:err server)))]
73+
(when line
74+
(println "stderr: " line)
75+
(recur)))))
76+

prompts/catalog.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ registry:
264264
description: |
265265
UNDER CONSTRUCTION (support long running browser cache)
266266
Browser automation and web scraping using Puppeteer.
267-
ref: https://img.icons8.com/officel/80/under-construction.png
267+
ref: github:docker/labs-ai-tools-for-devs?path=prompts/mcp/puppeteer.md
268268
icon: https://img.icons8.com/officel/80/under-construction.png
269269
tools: []
270270
prompts: 0

prompts/mcp/atlassian.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ mcp:
99
JIRA_URL: "{{atlassian.jira.url}}"
1010
JIRA_USERNAME: "{{atlassian.jira.username}}"
1111
secrets:
12-
JIRA_PERSONAL_TOKEN: atlassian.jira.token
12+
atlassian.jira.token: JIRA_PERSONAL_TOKEN
1313
---
1414

1515
# Configuration

prompts/mcp/gdrive.md

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ tools:
33
- name: gdrive_auth
44
description: Authorize this server to use your Google Drive.
55
container:
6-
image: mcp/gdrive:latest
7-
background: true
6+
image: vonwig/gdrive:latest
7+
background-callback: true
88
workdir: /app
99
volumes:
1010
- "mcp-gdrive:/gdrive-server"
@@ -25,5 +25,70 @@ mcp:
2525
- "mcp-gdrive:/gdrive-server"
2626
environment:
2727
GDRIVE_CREDENTIALS_PATH: /gdrive-server/credentials.json
28+
GDRIVE_OAUTH_PATH: /secret/google.gcp-oauth.keys.json
29+
secrets:
30+
google.gcp-oauth.keys.json: GDRIVE
2831
---
2932

33+
# Configuration
34+
35+
Before you can use this server, users will need to add one secret named `google.gcp-oauth.keys.json`. The value
36+
to copy into this secret is the _content_ of the file that you download from Google when you create a new Google
37+
OAuth application. The steps to create a new OAuth client are below.
38+
39+
## Detailed Google Cloud Setup
40+
41+
### Create a Google Cloud Project
42+
* Visit the Google Cloud Console
43+
* Click "New Project"
44+
* Enter a project name (e.g., "MCP GDrive Server")
45+
* Click "Create"
46+
* Wait for the project to be created and select it
47+
48+
### Enable the Google Drive API
49+
* Go to the API Library
50+
* Search for "Google Drive API"
51+
* Click on "Google Drive API"
52+
* Click "Enable"
53+
* Wait for the API to be enabled
54+
55+
### Configure OAuth Consent Screen
56+
57+
* Navigate to OAuth consent screen
58+
* Select User Type:
59+
* "Internal" if you're using Google Workspace
60+
* "External" for personal Google accounts
61+
* Click "Create"
62+
* Fill in the required fields:
63+
* App name: "MCP GDrive Server"
64+
* User support email: your email
65+
* Developer contact email: your email
66+
* Click "Save and Continue"
67+
* On the "Scopes" page:
68+
* Click "Add or Remove Scopes"
69+
* Add https://www.googleapis.com/auth/drive.readonly
70+
* Click "Update"
71+
* Click "Save and Continue"
72+
* Review the summary and click "Back to Dashboard"
73+
74+
### Create OAuth Client ID
75+
* Go to Credentials
76+
* Click "Create Credentials" at the top
77+
* Select "OAuth client ID"
78+
* Choose Application type: "Desktop app"
79+
* Name: "MCP GDrive Server Desktop Client"
80+
* Click "Create"
81+
* Add one Authorized redirect URI which should be `http://localhost:3000/oauth2callback`
82+
* In the popup:
83+
* Click "Download JSON"
84+
* Save the file
85+
* Click "OK"
86+
87+
# Usage
88+
89+
After enabling this server, refresh your MCP client and ask the agent to "Authorize your server for Google Drive", or
90+
something to that effect. This will give you with a URL that you can use to authorize the MCP server to access
91+
Google Drive readonly.
92+
93+
Once you've completed the authorization flow, try searching for some Google drive files.
94+

runbook.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ docker pull mcp/docker:prerelease
2020

2121
```sh
2222
# docker:command=build-release
23-
VERSION="0.0.8"
23+
VERSION="0.0.9"
2424
docker buildx build \
2525
--builder hydrobuild \
2626
--platform linux/amd64,linux/arm64 \

src/docker.clj

Lines changed: 86 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
jsonrpc
1010
[jsonrpc.logger :as logger]
1111
logging
12+
repl
1213
schema
1314
shutdown)
1415
(:import
@@ -339,8 +340,7 @@
339340
(injected-entrypoint {:a "A"} ["BLAH=whatever"] "my command")
340341
(injected-entrypoint nil nil "my command")
341342
(injected-entrypoint {:a "A"} nil "my command")
342-
(injected-entrypoint nil nil nil)
343-
)
343+
(injected-entrypoint nil nil nil))
344344

345345
(defn inject-secret-transform [container-definition]
346346
(check-then-pull container-definition)
@@ -350,18 +350,18 @@
350350
(-> (images {"reference" [(:image container-definition)]})
351351
first))
352352
:Config)
353-
real-entrypoint (string/join " " (concat
354-
(or (:entrypoint container-definition) Entrypoint)
355-
(or (:command container-definition) Cmd)))]
353+
real-entrypoint (string/join " " (concat
354+
(or (:entrypoint container-definition) Entrypoint)
355+
(or (:command container-definition) Cmd)))]
356356
(-> container-definition
357-
(assoc :entrypoint ["/bin/sh" "-c" (injected-entrypoint
358-
(:secrets container-definition)
359-
(concat
360-
Env
361-
(->> (:environment container-definition)
362-
(map (fn [[k v]] (format "%s=%s" k v)))
363-
(into [])))
364-
real-entrypoint)])
357+
(assoc :entrypoint ["/bin/sh" "-c" (injected-entrypoint
358+
(:secrets container-definition)
359+
(concat
360+
Env
361+
(->> (:environment container-definition)
362+
(map (fn [[k v]] (format "%s=%s" (if (keyword? k) (name k) k) v)))
363+
(into [])))
364+
real-entrypoint)])
365365
(dissoc :command))))
366366

367367
(defn run-streaming-function-with-no-stdin
@@ -604,6 +604,46 @@
604604
(start x)
605605
(assoc x :socket (write-stdin (:Id x) (:content m)))))
606606

607+
(defn function-call-with-attached-socket
608+
"create and start a container with an attached socket
609+
this does not wait for the container to finish
610+
returns a map with an
611+
:output-channel for stdout/stderr/:stopped/:timeout/:closed messages
612+
:write function to write to the stdin of the container"
613+
[container]
614+
(check-then-pull container)
615+
(let [x (docker/create (assoc container
616+
:opts {:StdinOnce true
617+
:OpenStdin true
618+
:AttachStding true}))
619+
socket-channel (attach-socket (:Id x))
620+
c (async/chan)
621+
output-channel (async/chan)]
622+
(start x)
623+
(async/thread (read-loop socket-channel c))
624+
(async/go
625+
(docker/wait x)
626+
(async/>! c :stopped)
627+
#_(delete x))
628+
(async/go-loop
629+
[block (async/<! c)]
630+
(logger/info "background socket read loop " block)
631+
(cond
632+
(#{:stopped :timeout} block)
633+
(do
634+
(logger/info "socket read loop " block)
635+
(async/put! output-channel block))
636+
(nil? block)
637+
(async/put! output-channel :closed)
638+
:else
639+
(do
640+
(async/put! output-channel block)
641+
(recur (async/<! c)))))
642+
(assoc
643+
x
644+
:output-channel output-channel
645+
:write (fn [s] (write-to-stdin socket-channel s)))))
646+
607647
(defn finish-call
608648
"This is a blocking call that waits for the container to finish and then returns the output and exit code."
609649
[{:keys [timeout] :or {timeout 10000} :as x}]
@@ -638,6 +678,24 @@
638678
(delete x)
639679
{}))))
640680

681+
(defn run-in-background-with-one-message [m]
682+
(let [{:keys [output-channel]} (function-call-with-attached-socket m)]
683+
(async/go-loop
684+
[]
685+
(let [m (async/<! output-channel)]
686+
(cond
687+
(#{:timeout} m)
688+
{:done :timeout :timeout "timeout waiting for message"}
689+
(#{:stopped :closed} m)
690+
{:done :exited :pty-output "tool stopped before responding on stdout" :exit-code 1}
691+
(and (map? m) (contains? m :stdout))
692+
{:pty-output (:stdout m)
693+
:done :running}
694+
:else
695+
(do
696+
(logger/info "background stderr " m)
697+
(recur)))))))
698+
641699
(defn run-with-stdin-content
642700
"run container with stdin read from file or from string
643701
this is several engine calls
@@ -665,9 +723,24 @@
665723
(run-with-stdin-content m)
666724
(true? (:background m))
667725
(run-background-function m)
726+
(true? (:background-callback m))
727+
(async/<!! (run-in-background-with-one-message m))
668728
:else
669729
(run-function m)))
670730

731+
(comment
732+
(repl/setup-stdout-logger)
733+
(run-container
734+
{:image "vonwig/gdrive:latest"
735+
:background-callback true
736+
:workdir "/app"
737+
:ports ["3000:3000"]
738+
:volumes ["mcp-gdrive:/gdrive-server"]
739+
:environment {"GDRIVE_CREDENTIALS_PATH" "/gdrive-server/credentials.json"
740+
"GDRIVE_OAUTH_PATH" "/secret/google.gcp-oauth.keys.json"}
741+
:secrets {:google.gcp-oauth.keys.json "GDRIVE"}
742+
:command ["auth"]}))
743+
671744
(defn get-login-info-from-desktop-backend
672745
"returns token or nil if not logged in or backend.sock is not available"
673746
[]

src/extension/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
IMAGE?=docker/labs-ai-tools-for-devs
2-
TAG?=0.2.27
2+
TAG?=0.2.29
33

44
BUILDER=buildx-multi-arch
55

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.8
3+
image: mcp/docker:0.0.9
44
ports:
55
- 8811:8811
66
volumes:

0 commit comments

Comments
 (0)