├── resources └── .keep ├── examples ├── resources │ ├── .keep │ └── log4j2-mcp.xml ├── .clj-kondo │ └── config.edn ├── bb.edn ├── .gitignore ├── build.clj ├── src │ ├── code_analysis_server.clj │ ├── vegalite_server.clj │ └── calculator_server.clj ├── deps.edn ├── Makefile ├── README.md └── LICENSE ├── .zprint.edn ├── .clj-kondo ├── imports │ ├── http-kit │ │ └── http-kit │ │ │ ├── config.edn │ │ │ └── httpkit │ │ │ └── with_channel.clj │ ├── rewrite-clj │ │ └── rewrite-clj │ │ │ └── config.edn │ ├── funcool │ │ └── promesa │ │ │ └── config.edn │ └── io.pedestal │ │ └── pedestal.log │ │ ├── config.edn │ │ └── hooks │ │ └── io │ │ └── pedestal │ │ └── log.clj_kondo └── config.edn ├── .dir-locals.el ├── test └── io │ └── modelcontext │ └── clojure_sdk │ ├── test_helper.clj │ ├── specs_test.clj │ ├── specs_gen_test.clj │ └── server_test.clj ├── src └── io │ └── modelcontext │ └── clojure_sdk │ ├── mcp │ └── errors.clj │ ├── stdio_server.clj │ ├── io_chan.clj │ ├── server.clj │ └── specs.clj ├── .gitignore ├── LICENSE ├── CHANGELOG.md ├── utils └── logger │ ├── README.org │ ├── resources │ └── logger │ │ └── log4j2-mcp.xml │ ├── src │ └── me │ │ └── vedang │ │ └── logger │ │ └── interface.cljc │ └── deps.edn ├── .aider.conf.yml ├── CONVENTIONS.md ├── todo.org ├── deps.edn ├── Makefile ├── doc └── lsp4clj.md └── README.md /resources/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/resources/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.zprint.edn: -------------------------------------------------------------------------------- 1 | {:fn-map {"with-context" "with-meta"}, :map {:indent 0}} 2 | -------------------------------------------------------------------------------- /examples/.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:linters {:redundant-ignore {:exclude [:clojure-lsp/unused-value]}}} 2 | -------------------------------------------------------------------------------- /.clj-kondo/imports/http-kit/http-kit/config.edn: -------------------------------------------------------------------------------- 1 | 2 | {:hooks 3 | {:analyze-call {org.httpkit.server/with-channel httpkit.with-channel/with-channel}}} 4 | -------------------------------------------------------------------------------- /.clj-kondo/imports/rewrite-clj/rewrite-clj/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as 2 | {rewrite-clj.zip/subedit-> clojure.core/-> 3 | rewrite-clj.zip/subedit->> clojure.core/->> 4 | rewrite-clj.zip/edit-> clojure.core/-> 5 | rewrite-clj.zip/edit->> clojure.core/->>}} 6 | -------------------------------------------------------------------------------- /.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:config-in-ns {me.vedang.logger.interface {:linters {:unused-binding {:level 2 | :off}}}}, 3 | :exclude-files ".clj-kondo/*|vendors/*|examples/*", 4 | :linters {:redundant-ignore {:exclude [:clojure-lsp/unused-public-var]}}} 5 | -------------------------------------------------------------------------------- /examples/bb.edn: -------------------------------------------------------------------------------- 1 | ;;; Run with `bb run-calculator` 2 | {:deps {io.modelcontextprotocol/mcp-clojure-sdk 3 | {:git/sha "52279524b90d09e6962f13d9e975cb4cbd910dbd", 4 | :git/url "git@github.com:unravel-team/mcp-clojure-sdk.git"}}, 5 | :paths ["." "resources"], 6 | :tasks {run-calculator {:doc "Run the calculator server example", 7 | :requires ([calculator-server :as calc]), 8 | :task (apply calc/-main *command-line-args*)}}} 9 | -------------------------------------------------------------------------------- /.clj-kondo/imports/funcool/promesa/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as {promesa.core/-> clojure.core/-> 2 | promesa.core/->> clojure.core/->> 3 | promesa.core/as-> clojure.core/as-> 4 | promesa.core/let clojure.core/let 5 | promesa.core/plet clojure.core/let 6 | promesa.core/loop clojure.core/loop 7 | promesa.core/recur clojure.core/recur 8 | promesa.core/with-redefs clojure.core/with-redefs 9 | promesa.core/doseq clojure.core/doseq}} 10 | -------------------------------------------------------------------------------- /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ;;; Directory Local Variables -*- no-byte-compile: t; -*- 2 | ;;; For more information see (info "(emacs) Directory Variables") 3 | ((clojure-dart-ts-mode . ((apheleia-formatter . (zprint)))) 4 | (clojure-jank-ts-mode . ((apheleia-formatter . (zprint)))) 5 | (clojure-mode . ((apheleia-formatter . (zprint)))) 6 | (clojure-ts-mode . ((apheleia-formatter . (zprint)))) 7 | (clojurec-mode . ((apheleia-formatter . (zprint)))) 8 | (clojurec-ts-mode . ((apheleia-formatter . (zprint)))) 9 | (clojurescript-mode . ((apheleia-formatter . (zprint)))) 10 | (clojurescript-ts-mode . ((apheleia-formatter . (zprint))))) 11 | -------------------------------------------------------------------------------- /.clj-kondo/imports/http-kit/http-kit/httpkit/with_channel.clj: -------------------------------------------------------------------------------- 1 | (ns httpkit.with-channel 2 | (:require [clj-kondo.hooks-api :as api])) 3 | 4 | (defn with-channel [{node :node}] 5 | (let [[request channel & body] (rest (:children node))] 6 | (when-not (and request channel) (throw (ex-info "No request or channel provided" {}))) 7 | (when-not (api/token-node? channel) (throw (ex-info "Missing channel argument" {}))) 8 | (let [new-node 9 | (api/list-node 10 | (list* 11 | (api/token-node 'let) 12 | (api/vector-node [channel (api/vector-node [])]) 13 | request 14 | body))] 15 | 16 | {:node new-node}))) 17 | -------------------------------------------------------------------------------- /test/io/modelcontext/clojure_sdk/test_helper.clj: -------------------------------------------------------------------------------- 1 | (ns io.modelcontext.clojure-sdk.test-helper 2 | (:require [clojure.core.async :as async] 3 | [clojure.test :refer [is]])) 4 | 5 | (defn take-or-timeout 6 | ([ch] (take-or-timeout ch 100)) 7 | ([ch timeout-ms] (take-or-timeout ch timeout-ms :timeout)) 8 | ([ch timeout-ms timeout-val] 9 | (let [timeout (async/timeout timeout-ms) 10 | [result ch] (async/alts!! [ch timeout])] 11 | (if (= ch timeout) timeout-val result)))) 12 | 13 | (defn assert-no-take [ch] (is (= :nothing (take-or-timeout ch 500 :nothing)))) 14 | 15 | (defn assert-take 16 | [ch] 17 | (let [result (take-or-timeout ch)] 18 | (is (not= :timeout result)) 19 | result)) 20 | -------------------------------------------------------------------------------- /.clj-kondo/imports/io.pedestal/pedestal.log/config.edn: -------------------------------------------------------------------------------- 1 | ; Copyright 2024 Nubank NA 2 | 3 | ; The use and distribution terms for this software are covered by the 4 | ; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0) 5 | ; which can be found in the file epl-v10.html at the root of this distribution. 6 | ; 7 | ; By using this software in any fashion, you are agreeing to be bound by 8 | ; the terms of this license. 9 | ; 10 | ; You must not remove this notice, or any other, from this software. 11 | 12 | {:hooks 13 | {:analyze-call 14 | {io.pedestal.log/trace hooks.io.pedestal.log/log-expr 15 | io.pedestal.log/debug hooks.io.pedestal.log/log-expr 16 | io.pedestal.log/info hooks.io.pedestal.log/log-expr 17 | io.pedestal.log/warn hooks.io.pedestal.log/log-expr 18 | io.pedestal.log/error hooks.io.pedestal.log/log-expr}}} 19 | -------------------------------------------------------------------------------- /src/io/modelcontext/clojure_sdk/mcp/errors.clj: -------------------------------------------------------------------------------- 1 | (ns io.modelcontext.clojure-sdk.mcp.errors 2 | (:require [lsp4clj.lsp.errors :as lsp.errors])) 3 | 4 | (set! *warn-on-reflection* true) 5 | 6 | (def by-key 7 | (assoc lsp.errors/by-key 8 | :tool-not-found {:code -32601, :message "Tool not found"} 9 | :resource-not-found {:code -32601, :message "Resource not found"} 10 | :prompt-not-found {:code -32601, :message "Prompt not found"})) 11 | 12 | (defn body 13 | "Returns a JSON-RPC error object with the code and, if provided, the message 14 | and data. 15 | 16 | `error-code` should be a keyword as per `by-key`." 17 | ([error-code data] 18 | (-> (by-key error-code) 19 | (assoc :data data))) 20 | ([error-code message data] 21 | (-> (by-key error-code) 22 | (assoc :message message) 23 | (assoc :data data)))) 24 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | # Artifacts 2 | **/classes 3 | **/target 4 | **/.artifacts 5 | **/.cpcache 6 | **/.DS_Store 7 | **/.gradle 8 | logs/ 9 | 10 | # 12-factor App Configuration 11 | .envrc 12 | 13 | # User-specific stuff 14 | .idea/**/workspace.xml 15 | .idea/**/tasks.xml 16 | .idea/**/usage.statistics.xml 17 | .idea/**/shelf 18 | .idea/**/statistic.xml 19 | .idea/dictionaries/** 20 | .idea/libraries/** 21 | 22 | # File-based project format 23 | *.iws 24 | *.ipr 25 | 26 | # Cursive Clojure plugin 27 | .idea/replstate.xml 28 | *.iml 29 | 30 | /example/example/** 31 | artifacts 32 | projects/**/pom.xml 33 | 34 | # nrepl 35 | .nrepl-port 36 | 37 | # clojure-lsp 38 | .lsp/.cache 39 | 40 | # clj-kondo 41 | .clj-kondo/.cache 42 | 43 | # Calva VS Code Extension 44 | .calva/output-window/output.calva-repl 45 | 46 | # Aider files 47 | .aider* 48 | 49 | # Metaclj tempfiles 50 | .antqtool.lastupdated 51 | .enrich-classpath-repl 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Artifacts 2 | **/classes 3 | **/target 4 | **/.artifacts 5 | **/.cpcache 6 | **/.DS_Store 7 | **/.gradle 8 | logs/ 9 | 10 | # 12-factor App Configuration 11 | .envrc 12 | 13 | # User-specific stuff 14 | .idea/**/workspace.xml 15 | .idea/**/tasks.xml 16 | .idea/**/usage.statistics.xml 17 | .idea/**/shelf 18 | .idea/**/statistic.xml 19 | .idea/dictionaries/** 20 | .idea/libraries/** 21 | 22 | # File-based project format 23 | *.iws 24 | *.ipr 25 | 26 | # Cursive Clojure plugin 27 | .idea/replstate.xml 28 | *.iml 29 | 30 | /example/example/** 31 | artifacts 32 | projects/**/pom.xml 33 | 34 | # nrepl 35 | .nrepl-port 36 | 37 | # clojure-lsp 38 | .lsp/.cache 39 | 40 | # clj-kondo 41 | .clj-kondo/.cache 42 | 43 | # Calva VS Code Extension 44 | .calva/output-window/output.calva-repl 45 | 46 | # Aider files 47 | .aider* 48 | 49 | # Metaclj tempfiles 50 | .antqtool.lastupdated 51 | .enrich-classpath-repl 52 | 53 | # Tempfiles for easy reference 54 | vendors/ 55 | -------------------------------------------------------------------------------- /.clj-kondo/imports/io.pedestal/pedestal.log/hooks/io/pedestal/log.clj_kondo: -------------------------------------------------------------------------------- 1 | ; Copyright 2024 Nubank NA 2 | 3 | ; The use and distribution terms for this software are covered by the 4 | ; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0) 5 | ; which can be found in the file epl-v10.html at the root of this distribution. 6 | ; 7 | ; By using this software in any fashion, you are agreeing to be bound by 8 | ; the terms of this license. 9 | ; 10 | ; You must not remove this notice, or any other, from this software. 11 | 12 | (ns hooks.io.pedestal.log 13 | (:require [clj-kondo.hooks-api :as api])) 14 | 15 | (defn log-expr 16 | "Expands (log-expr :a :A :b :B ... ) 17 | to (hash-map :a :A :b :B ... ) per clj-kondo examples." 18 | [{:keys [:node]}] 19 | (let [[k v & _kvs] (rest (:children node))] 20 | (when-not (and k v) 21 | (throw (ex-info "No kv pair provided" {}))) 22 | (let [new-node (api/list-node 23 | (list* 24 | (api/token-node 'hash-map) 25 | (rest (:children node))))] 26 | {:node (vary-meta new-node assoc :clj-kondo/ignore [:unused-value])}))) 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 mcp-clojure-sdk maintainers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/). 3 | 4 | ## [Unreleased] 5 | ### Changed 6 | - Add a new arity to `make-widget-async` to provide a different widget shape. 7 | 8 | ## [1.0.105] - 2025-03-18 9 | ### Changed 10 | - Internals change: Created Clojure specs for the entire MCP specification 11 | - The SDK stubs out all the request and notification methods that it does not currently support 12 | - Improves the error reporting of servers built on top of `mcp-clojure-sdk` 13 | - Bumped version of `examples` jar to `1.2.0` to highlight improved internals 14 | 15 | ### Removed 16 | 17 | ### Fixed 18 | 19 | ## 1.0.65 - 2025-03-16 20 | ### Added 21 | - `stdio_server` implementation of MCP 22 | - `examples` folder shows `tools` and `prompts` based servers 23 | 24 | [Unreleased]: https://github.com/io.modelcontext/clojure-sdk/compare/fb947ebc8dd59fc778b886d832850f38974cbdc6...HEAD 25 | [1.1.105]: https://github.com/io.modelcontext/clojure-sdk/compare/e0e410ee115256362d964df1272ea42428bf9a21...fb947ebc8dd59fc778b886d832850f38974cbdc6 26 | -------------------------------------------------------------------------------- /utils/logger/README.org: -------------------------------------------------------------------------------- 1 | #+title: Structured Logging in Clojure 2 | 3 | * What is this project? 4 | This is a template Clojure project showcasing how to configure Log4J2 as the logging backend properly, to output structured logs from the project. These logs can then be consumed by a collector like [[https://vector.dev/][Vector]] and forwarded to a tool like [[https://grafana.com/oss/loki/][Loki]] for visualisation. 5 | 6 | To install the template, run the following code: 7 | #+begin_src sh :eval no 8 | clojure -Sdeps '{:deps {io.github.vedang/clj-logging {:git/sha "e009d366c827705f513ef9018ffd920a49ce19da"}}}' -Tnew create :template me.vedang/logger :name yourprojectname/logger 9 | #+end_src 10 | 11 | Note that this assumes you have installed ~deps-new~ as your ~new~ "tool" via: 12 | 13 | #+begin_src sh 14 | clojure -Ttools install-latest :lib io.github.seancorfield/deps-new :as new 15 | #+end_src 16 | 17 | * References used to build this project: 18 | - https://github.com/stuartsierra/log.dev 19 | - https://www.loggly.com/blog/benchmarking-java-logging-frameworks/ 20 | - https://logging.apache.org/log4j/2.x/manual/async.html 21 | - https://krishankantsinghal.medium.com/logback-slf4j-log4j2-understanding-them-and-learn-how-to-use-d33deedd0c46 22 | -------------------------------------------------------------------------------- /examples/build.clj: -------------------------------------------------------------------------------- 1 | (ns build 2 | (:refer-clojure :exclude [test]) 3 | (:require [clojure.tools.build.api :as b])) 4 | 5 | (def lib 'io.modelcontextprotocol.clojure-sdk/examples) 6 | (def version "1.2.0") 7 | (def class-dir "target/classes") 8 | 9 | (defn test 10 | "Run all the tests." 11 | [opts] 12 | (let [basis (b/create-basis {:aliases [:test]}) 13 | cmds (b/java-command {:basis basis, 14 | :main 'clojure.main, 15 | :main-args ["-m" "cognitect.test-runner"]}) 16 | {:keys [exit]} (b/process cmds)] 17 | (when-not (zero? exit) (throw (ex-info "Tests failed" {})))) 18 | opts) 19 | 20 | (defn- uber-opts 21 | [opts] 22 | (assoc opts 23 | :lib lib 24 | :main 'vegalite-server 25 | :uber-file (format "target/%s-%s.jar" lib version) 26 | :basis (b/create-basis {}) 27 | :class-dir class-dir 28 | :src-dirs ["src"] 29 | :ns-compile ['vegalite-server 'code-analysis-server 'calculator-server])) 30 | 31 | (defn ci 32 | "Run the CI pipeline of tests (and build the uberjar)." 33 | [opts] 34 | (test opts) 35 | (b/delete {:path "target"}) 36 | (let [opts (uber-opts opts)] 37 | (println "\nCopying source...") 38 | (b/copy-dir {:src-dirs ["resources" "src"], :target-dir class-dir}) 39 | (println "\nCompiling ...") 40 | (b/compile-clj opts) 41 | (println "\nBuilding JAR..." (:uber-file opts)) 42 | (b/uber opts)) 43 | opts) 44 | -------------------------------------------------------------------------------- /examples/resources/log4j2-mcp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /utils/logger/resources/logger/log4j2-mcp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /.aider.conf.yml: -------------------------------------------------------------------------------- 1 | ## Mark aider commits specifically 2 | attribute-commit-message-author: true 3 | 4 | ## Watch is my preferred way to use Aider 5 | watch-files: true 6 | 7 | ## Specify a read-only file (can be used multiple times) 8 | read: 9 | - CONVENTIONS.md 10 | - src/io/modelcontext/clojure_sdk/specs.clj 11 | 12 | ## Specify a custom prompt for generating commit messages 13 | commit-prompt: | 14 | You are an expert software engineer that generates clear Git commit messages based on the provided diffs. 15 | Review the provided context and diffs which are about to be committed to a git repo. 16 | Review the diffs carefully. 17 | Generate a commit message for those changes. 18 | The commit message should be structured as follows: 19 | 20 | [optional scope]: 21 | 22 | [optional body] 23 | 24 | [optional footer(s)] 25 | 26 | Use these for : fix, feat, build, chore, ci, docs, style, refactor, perf, test 27 | 28 | Ensure the commit message: 29 | - Starts with the appropriate prefix. 30 | - Is in the imperative mood (e.g., "Add feature" not "Added feature" or "Adding feature"). 31 | - Does not exceed 72 characters. 32 | 33 | Ensure that you write a meaningful for bigger changes. 34 | 35 | Reply only with the commit message, without any additional text or explanations. 36 | 37 | ## Main model (Architect) and editor model (Editor) 38 | model: gemini-exp 39 | architect: true 40 | editor-model: gemini-exp 41 | 42 | ## Testing and Linting of my code 43 | lint-cmd: make format 44 | auto-lint: true 45 | test-cmd: make test 46 | auto-test: true 47 | -------------------------------------------------------------------------------- /src/io/modelcontext/clojure_sdk/stdio_server.clj: -------------------------------------------------------------------------------- 1 | (ns io.modelcontext.clojure-sdk.stdio-server 2 | (:require [clojure.core.async :as async] 3 | [io.modelcontext.clojure-sdk.server :as core] 4 | [io.modelcontext.clojure-sdk.io-chan :as mcp.io-chan] 5 | [lsp4clj.server :as lsp.server] 6 | [me.vedang.logger.interface :as log]) 7 | (:refer-clojure :exclude [run!])) 8 | 9 | (defn- monitor-server-logs 10 | [log-ch] 11 | ;; NOTE: We don't do this in `initialize`, because if anything bad 12 | ;; happened before `initialize`, we wouldn't get any logs. 13 | (async/go-loop [] 14 | (when-let [log-args (async/input-chan in) 33 | output-ch (mcp.io-chan/output-stream->output-chan out)] 34 | (lsp.server/chan-server (assoc opts 35 | :in in 36 | :out out 37 | :input-ch input-ch 38 | :output-ch output-ch)))) 39 | 40 | #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} 41 | (defn run! 42 | [spec] 43 | (log/with-context {:server-id (:server-id spec)} 44 | (log/trace :fn run! :msg "Starting server!") 45 | (core/validate-spec! spec) 46 | (let [log-ch (async/chan (async/sliding-buffer 20)) 47 | server (stdio-server {:log-ch log-ch})] 48 | (start! server spec)))) 49 | -------------------------------------------------------------------------------- /examples/src/code_analysis_server.clj: -------------------------------------------------------------------------------- 1 | (ns code-analysis-server 2 | (:gen-class) 3 | (:require [io.modelcontext.clojure-sdk.stdio-server :as io-server] 4 | [me.vedang.logger.interface :as log])) 5 | 6 | (def prompt-analyze-code 7 | {:name "analyze-code", 8 | :description "Analyze code for potential improvements", 9 | :arguments 10 | [{:name "language", :description "Programming language", :required true} 11 | {:name "code", :description "The code to analyze", :required true}], 12 | :handler (fn analyze-code [args] 13 | {:messages [{:role "assistant", 14 | :content 15 | {:type "text", 16 | :text (str "Analysis of " 17 | (:language args) 18 | " code:\n" 19 | "Here are potential improvements for:\n" 20 | (:code args))}}]})}) 21 | 22 | (def prompt-poem-about-code 23 | {:name "poem-about-code", 24 | :description "Write a poem describing what this code does", 25 | :arguments 26 | [{:name "poetry_type", 27 | :description 28 | "The style in which to write the poetry: sonnet, limerick, haiku", 29 | :required true} 30 | {:name "code", 31 | :description "The code to write poetry about", 32 | :required true}], 33 | :handler (fn [args] 34 | {:messages [{:role "assistant", 35 | :content {:type "text", 36 | :text (str "Write a " (:poetry_type args) 37 | " Poem about:\n" (:code 38 | args))}}]})}) 39 | 40 | (def code-analysis-server-spec 41 | {:name "code-analysis", 42 | :version "1.0.0", 43 | :prompts [prompt-analyze-code prompt-poem-about-code]}) 44 | 45 | (defn -main 46 | [& _args] 47 | (let [server-id (random-uuid)] 48 | (log/debug "[MAIN] Starting code analysis server: " server-id) 49 | @(io-server/run! (assoc code-analysis-server-spec :server-id server-id)))) 50 | 51 | (comment 52 | ;; Test power / maybe overflow 53 | "What's 2 to the power of 1000?" 54 | ;; Test array operations 55 | "What's the average of [1, 2, 3, 4, 5]?" 56 | "Sum up the numbers [10, 20, 30, 40, 50]" 57 | "Calculate the average of []" ; Test empty array 58 | ;; Test long computation 59 | "What's the factorial of 15?" ; Should take > 1 second 60 | ;; Test error handling 61 | "What's the square root of -4? Use the square-root tool" 62 | "Calculate the factorial of -1") 63 | -------------------------------------------------------------------------------- /utils/logger/src/me/vedang/logger/interface.cljc: -------------------------------------------------------------------------------- 1 | (ns me.vedang.logger.interface 2 | #?(:bb (:require [babashka.json :as json] 3 | [taoensso.timbre :as log]) 4 | :clj (:require [babashka.json :as json] 5 | [io.pedestal.log :as log]))) 6 | ;; [ref: babashka_json] 7 | ;; [ref: babashka_logging] 8 | ;; [ref: babashka_reader_conditionals] 9 | ;; [ref: reader_conditionals] 10 | 11 | (defmacro trace 12 | [& keyvals] 13 | #?(:bb `(log/trace ~@keyvals) 14 | :clj `(log/trace ::log/formatter json/write-str ~@keyvals))) 15 | 16 | (defmacro debug 17 | [& keyvals] 18 | #?(:bb `(log/debug ~@keyvals) 19 | :clj `(log/debug ::log/formatter json/write-str ~@keyvals))) 20 | 21 | (defmacro info 22 | [& keyvals] 23 | #?(:bb `(log/info ~@keyvals) 24 | :clj `(log/info ::log/formatter json/write-str ~@keyvals))) 25 | 26 | (defmacro warn 27 | [& keyvals] 28 | #?(:bb `(log/warn ~@keyvals) 29 | :clj `(log/warn ::log/formatter json/write-str ~@keyvals))) 30 | 31 | (defmacro error 32 | [& keyvals] 33 | #?(:bb `(log/error ~@keyvals) 34 | :clj `(log/error ::log/formatter json/write-str ~@keyvals))) 35 | 36 | (defmacro spy 37 | "Logs expr and its value at DEBUG level, returns value." 38 | [expr] 39 | (let [value' (gensym "value") 40 | expr' (gensym "expr")] 41 | `(let [~value' ~expr 42 | ~expr' ~(list 'quote expr)] 43 | #?(:bb (log/debug :spy ~expr' :returns ~value') 44 | :clj (log/debug :spy ~expr' 45 | :returns ~value' 46 | ::log/formatter json/write-str)) 47 | ~value'))) 48 | 49 | (defmacro with-context 50 | [ctx-map & body] 51 | ;; [ref: babashka_reader_conditionals] 52 | #?(:bb `(do ~@body) 53 | :clj `(log/with-context (assoc ~ctx-map ::log/formatter json/write-str) 54 | ~@body))) 55 | 56 | ;;; [tag: babashka_reader_conditionals] 57 | ;;; 58 | ;;; From: https://book.babashka.org/#_reader_conditionals 59 | ;;; 60 | ;;; Babashka supports reader conditionals by taking either the :bb or :clj 61 | ;;; branch, **whichever comes first**. NOTE: the :clj branch behavior was added 62 | ;;; in version 0.0.71, before that version the :clj branch was ignored. 63 | ;;; 64 | ;;; Remember this when defining reader conditional branches. 65 | 66 | ;;; [tag: reader_conditionals] 67 | ;;; 68 | ;;; An important point to keep in mind: Reader Conditionals only work in .cljc 69 | ;;; files 70 | ;;; 71 | ;;; From: https://clojure.org/guides/reader_conditionals 72 | ;;; 73 | ;;; Reader conditionals are integrated into the Clojure reader, and don’t 74 | ;;; require any extra tooling. To use reader conditionals, all you need is for 75 | ;;; your file to have a .cljc extension. 76 | -------------------------------------------------------------------------------- /examples/deps.edn: -------------------------------------------------------------------------------- 1 | {:aliases 2 | {:build {:deps {io.github.clojure/tools.build {:mvn/version "0.10.5"}}, 3 | :ns-default build}, 4 | :logs 5 | {:jvm-opts 6 | ["-Dclojure.tools.logging.factory=clojure.tools.logging.impl/log4j2-factory" 7 | "-Dorg.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.Slf4jLog" 8 | "-Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector" 9 | "-Dlog4j2.configurationFile=log4j2-mcp.xml" 10 | "-Dbabashka.json.provider=metosin/jsonista" "-Dlogging.level=INFO"]}, 11 | :run-calculator 12 | {:command "java", 13 | :jvm-opts 14 | ["-Dclojure.tools.logging.factory=clojure.tools.logging.impl/log4j2-factory" 15 | "-Dorg.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.Slf4jLog" 16 | "-Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector" 17 | "-Dlog4j2.configurationFile=log4j2-mcp.xml" 18 | "-Dbabashka.json.provider=metosin/jsonista" "-Dlogging.level=INFO" "-cp" 19 | "/examples/target/io.modelcontextprotocol.clojure-sdk/examples-1.2.0.jar" 20 | "calculator_server"]}, 21 | :run-code-analysis 22 | {:command "java", 23 | :jvm-opts 24 | ["-Dclojure.tools.logging.factory=clojure.tools.logging.impl/log4j2-factory" 25 | "-Dorg.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.Slf4jLog" 26 | "-Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector" 27 | "-Dlog4j2.configurationFile=log4j2-mcp.xml" 28 | "-Dbabashka.json.provider=metosin/jsonista" "-Dlogging.level=INFO" "-cp" 29 | "/examples/target/io.modelcontextprotocol.clojure-sdk/examples-1.2.0.jar" 30 | "code_analysis_server"]}, 31 | :run-vegalite 32 | {:command "java", 33 | :jvm-opts 34 | ["-Dclojure.tools.logging.factory=clojure.tools.logging.impl/log4j2-factory" 35 | "-Dorg.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.Slf4jLog" 36 | "-Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector" 37 | "-Dlog4j2.configurationFile=log4j2-mcp.xml" 38 | "-Dmcp.vegalite.vl_convert_executable=/Users/vedang/.cargo/bin/vl-convert" 39 | "-Dbabashka.json.provider=metosin/jsonista" "-Dlogging.level=INFO" "-cp" 40 | "/examples/target/io.modelcontextprotocol.clojure-sdk/examples-1.2.0.jar" 41 | "vegalite_server"]}, 42 | :test {:extra-deps {io.github.cognitect-labs/test-runner {:git/sha "dfb30dd", 43 | :git/tag "v0.5.1"}, 44 | org.clojure/test.check {:mvn/version "1.1.1"}}, 45 | :extra-paths ["test"]}}, 46 | :deps {babashka/process {:mvn/version "0.5.22"}, 47 | io.modelcontextprotocol/mcp-clojure-sdk {:local/root "../"}, 48 | org.clojure/clojure {:mvn/version "1.12.0"}}, 49 | :paths ["src" "resources"]} 50 | -------------------------------------------------------------------------------- /utils/logger/deps.edn: -------------------------------------------------------------------------------- 1 | {:aliases 2 | {:logs-dev 3 | {:jvm-opts 4 | ["-Dclojure.tools.logging.factory=clojure.tools.logging.impl/log4j2-factory" 5 | "-Dorg.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.Slf4jLog" 6 | "-Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector" 7 | "-Dlog4j2.configurationFile=log4j2-dev.xml" "-Dlogging.level=DEBUG"]}, 8 | :logs-prod 9 | {:jvm-opts 10 | ["-Dclojure.tools.logging.factory=clojure.tools.logging.impl/log4j2-factory" 11 | "-Dorg.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.Slf4jLog" 12 | "-Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector" 13 | "-Dlog4j2.configurationFile=log4j2-prod.xml" "-Dlogging.level=INFO"]}, 14 | :test {:extra-deps {io.github.cognitect-labs/test-runner {:git/sha "dfb30dd", 15 | :git/tag "v0.5.1"}, 16 | org.clojure/test.check {:mvn/version "1.1.1"}}, 17 | :extra-paths ["test"]}}, 18 | :deps {;;; Logging setup is a complex art. Here are the steps as I 19 | ;;; understand them at the moment, to set up Log4J2 as the logging 20 | ;;; backend with proper log library conflict resolution. 21 | ;; Use log4j2 as the primary logging library 22 | org.apache.logging.log4j/log4j-api {:mvn/version "2.24.3"}, 23 | org.apache.logging.log4j/log4j-core {:mvn/version "2.24.3"}, 24 | ;; Needed for logging from inside javax.servlet 25 | org.apache.logging.log4j/log4j-web {:mvn/version "2.24.3"}, 26 | ;; Use pedestal.log as the primary clojure facade. On 27 | ;; Babashka, we use taoensso.timbre (which is built-in) 28 | ;; instead of pedestal.log. [tag: babashka_logging] 29 | io.pedestal/pedestal.log {:mvn/version "0.7.2"}, 30 | ;; Use jsonista.core to format log as json. On Babashka, we 31 | ;; use Cheshire, which is built-in. babashka.json makes the 32 | ;; choice transparently. [tag: babashka_json] 33 | metosin/jsonista {:mvn/version "0.3.13"}, 34 | org.babashka/json {:mvn/version "0.1.6"}, 35 | ;; Use SLF4j as the primary facade for gathering all logs. 36 | org.apache.logging.log4j/log4j-slf4j2-impl {:mvn/version "2.24.3"}, 37 | org.slf4j/slf4j-api {:mvn/version "2.0.17"}, 38 | ;; Use LMAX Disruptor to enable Async Logging 39 | com.lmax/disruptor {:mvn/version "4.0.0"}, 40 | ;;; Redirect everything via the SLF4J API 41 | ;; Apache Commons logging 42 | org.slf4j/jcl-over-slf4j {:mvn/version "2.0.17"}, 43 | ;; Log4j 1.x 44 | org.slf4j/log4j-over-slf4j {:mvn/version "2.0.17"}, 45 | ;; OSGI LogService 46 | org.slf4j/osgi-over-slf4j {:mvn/version "2.0.17"}, 47 | ;; Java util logging 48 | org.slf4j/jul-to-slf4j {:mvn/version "2.0.17"}}, 49 | :paths ["src" "resources"]} 50 | -------------------------------------------------------------------------------- /CONVENTIONS.md: -------------------------------------------------------------------------------- 1 | # General rules 2 | 3 | - When reasoning, you perform step-by-step thinking before you answer the question. 4 | - If you speculate or predict something, always inform me. 5 | - When asked for information you do not have, do not make up an answer; always be honest about not knowing. 6 | - Document the project in such a way that an intern or LLM can easily pick it up and make progress on it. 7 | 8 | ## Rules for writing documentation 9 | 10 | - When proposing an edit to a markdown file, indent any code snippets inside it with two spaces. Indentation levels 0 and 4 are not allowed. 11 | - If a markdown code block is indented with any value other than 2 spaces, automatically fix it. 12 | 13 | ## When writing or planning code: 14 | 15 | - Always write a test for the code you are changing 16 | - Look for the simplest possible fix 17 | - Don’t introduce new technologies without asking. 18 | - Respect existing patterns and code structure 19 | - Do not edit or delete comments. 20 | - Don't remove debug logging code. 21 | - When asked to generate code to implement a stub, do not delete docstrings 22 | - When proposing sweeping changes to code, instead of proposing the whole thing at once and leaving "to implement" blocks, work through the proposed changes incrementally in cooperation with me 23 | 24 | ## IMPORTANT: Don't Forget 25 | 26 | - When I add PLAN! at the end of my request, write a detailed technical plan on how you are going to implement my request, step by step, with short code snippets, but don't implement it yet, instead ask me for confirmation. 27 | - When I add DISCUSS! at the end of my request, give me the ideas I've requested, but don't write code yet, instead ask me for confirmation. 28 | - When I add STUB! at the end of my request, instead of implementing functions/methods, stub them and raise NotImplementedError. 29 | - When I add EXPLORE! at the end of my request, do not give me your opinion immediately. Ask me questions first to make sure you fully understand the context before making suggestions. 30 | 31 | # Guidelines for Clojure code 32 | 33 | ## Build/Test Commands 34 | 35 | - Build: `make build` 36 | - Install locally: `make install` 37 | - Run all tests: `make test` 38 | - Full check: `make check` (runs linters and formatting checks) 39 | - Single test: `clojure -X:test :only your.test.ns/test-name` 40 | - Format the code: `make format`. Uses zprint for formatting. 41 | 42 | ## Dependencies and Code Style 43 | 44 | - Use `metosin/jsonista` for working with JSON 45 | - Use `pedestal/pedestal` for creating an HTTP servers. Use the Jetty server. 46 | - Use `com.seancorfield/next.jdbc` for working with SQL 47 | - Use `clj-http/clj-http` for creating an HTTP client 48 | - Use `org.clojure/core.async` for message passing and async operations 49 | - Use `test.check` for generative testing. Prefer writing generative tests. 50 | - Use `io.github.cognitect-labs/test-runner` as a test-runner 51 | - Write all specs, data as well as function specs, in a single `specs.clj` file. 52 | 53 | ## Error Handling 54 | 55 | - Use `ex-info` for exceptions with detailed context 56 | - Include error codes and descriptive messages in responses 57 | - Maintain existing debug logging code 58 | - Set `:is-error` flag in tool responses when errors occur 59 | -------------------------------------------------------------------------------- /todo.org: -------------------------------------------------------------------------------- 1 | * DONE Create a Stdio-transport based server implementation 2 | * DONE Create a template project for easily creating MCP servers 3 | CLOSED: [2025-04-27 Sun 18:30] 4 | :LOGBOOK: 5 | - State "DONE" from "TODO" [2025-04-27 Sun 18:30] 6 | - State "TODO" from [2025-04-27 Sun 18:28] 7 | :END: 8 | See: 9 | 1. [[https://github.com/unravel-team/mcp-clojure-server-deps-new][mcp-clojure-server-deps-new]] for a ~deps-new~ based template 10 | 2. [[https://github.com/unravel-team/example-cool-mcp-server][example-cool-mcp-server]] for a Github template project 11 | * WORKING Add client support to mcp-clojure-sdk 12 | :LOGBOOK: 13 | - State "WORKING" from "TODO" [2025-05-24 Sat 14:38] 14 | :END: 15 | ** WORKING Add basic client support for the stdio transport, based on what python-sdk and typescript-sdk do 16 | :LOGBOOK: 17 | - State "WORKING" from "TODO" [2025-05-24 Sat 14:38] 18 | :END: 19 | :CLOCK: 20 | CLOCK: [2025-05-25 Sun 11:07]--[2025-05-25 Sun 11:41] => 0:34 21 | CLOCK: [2025-05-24 Sat 14:38]--[2025-05-25 Sun 08:29] => 17:51 22 | :END: 23 | ** WORKING Create an integration test for the current server code using such a client, based on clojure-lsp integration tests 24 | :LOGBOOK: 25 | - State "WORKING" from "TODO" [2025-05-25 Sun 11:41] 26 | :END: 27 | :CLOCK: 28 | CLOCK: [2025-05-25 Sun 11:41]--[2025-05-25 Sun 12:32] => 0:51 29 | :END: 30 | - See: 31 | 32 | * TODO Update the README of mcp-clojure-sdk. 33 | Refer to typescript-sdk as a reference 34 | 35 | * TODO Write an example mcp-client-tool to run mcp-servers locally and list what they do/don't provide 36 | ** TODO Support servers launched using npx/node 37 | ** TODO Support servers launched using uv/python 38 | ** TODO Support servers launched using java 39 | * TODO Add support for dynamic server capability negotiation 40 | * TODO Add support for the SSE / Streaming HTTP transport to mcp-clojure-sdk 41 | ** TODO Add support for SSE transport 42 | ** TODO Add support for Streaming HTTP transport 43 | 44 | * WORKING Implement the entire roots section of the protocol 45 | :LOGBOOK: 46 | - State "WORKING" from "TODO" [2025-05-03 Sat 16:38] 47 | :END: 48 | * WORKING Bring the spec up-to-date and track the differences 49 | 50 | * TODO Add an integration test for end-to-end testing the server 51 | See ~integration.client~ from ~lsp4clj~ for inspiration 52 | * TODO Implement the HTTP-SSE Transport 53 | ** TODO Implement the SSE-transport based server 54 | ** TODO Implement the SSE-transport based client 55 | * TODO Checks and Balances in the mcp-cljc-sdk code 56 | ** TODO Make sure that handle-* functions are implement proper checks 57 | Read through the python decorator code to double-check if we are doing the right thing here. 58 | ** TODO Implement best practices for prompts 59 | ** TODO Implement best practices for tools 60 | ** TODO Implement best practices for resources 61 | ** TODO Ensure that error handling is correctly done for transports 62 | ** TODO Implement best practices for transports 63 | * TODO Implement the entire sampling section of the protocol 64 | * TODO Create a CLI tool for how tools, prompts, resources should be defined 65 | 1. Easily create new projects, in ~deps-new~ style 66 | 2. Organize them properly, making it possible to build servers fast. 67 | 3. Make it language-agnostic, allowing the tool to create Clojure, Python, TS projects. 68 | * TODO Explore babashka as the runner in Claude Desktop / Inspector 69 | The current code is wildly incompatible with bb, so this is a long-shot. But bb compatibility will mean speed and ease of use. 70 | -------------------------------------------------------------------------------- /src/io/modelcontext/clojure_sdk/io_chan.clj: -------------------------------------------------------------------------------- 1 | (ns io.modelcontext.clojure-sdk.io-chan 2 | (:require [babashka.json :as json] 3 | [camel-snake-kebab.core :as csk] 4 | [camel-snake-kebab.extras :as cske] 5 | [clojure.core.async :as async] 6 | [clojure.java.io :as io] 7 | [me.vedang.logger.interface :as log])) 8 | 9 | (set! *warn-on-reflection* true) 10 | 11 | ;;;; IO <-> chan 12 | 13 | ;; Follow the MCP spec for reading and writing JSON-RPC messages. Convert the 14 | ;; messages to and from Clojure hashmaps and shuttle them to core.async 15 | ;; channels. 16 | 17 | ;; https://modelcontextprotocol.io/specification 18 | 19 | (defn ^:private read-message 20 | [^java.io.BufferedReader input] 21 | (try (let [content (.readLine input)] 22 | (log/trace :fn :read-message :line content) 23 | (json/read-str content)) 24 | (catch Exception ex (log/error :fn :read-message :ex ex) :parse-error))) 25 | 26 | (defn ^:private kw->camelCaseString 27 | "Convert keywords to camelCase strings, but preserve capitalization of things 28 | that are already strings." 29 | [k] 30 | (cond-> k (keyword? k) csk/->camelCaseString)) 31 | 32 | (def ^:private write-lock (Object.)) 33 | 34 | (defn ^:private write-message 35 | [^java.io.BufferedWriter output msg] 36 | (let [content (json/write-str (cske/transform-keys kw->camelCaseString msg))] 37 | (locking write-lock 38 | (doto output (.write ^String content) (.newLine) (.flush))))) 39 | 40 | (defn input-stream->input-chan 41 | "Returns a channel which will yield parsed messages that have been read off 42 | the `input`. When the input is closed, closes the channel. By default when the 43 | channel closes, will close the input, but can be determined by `close?`. 44 | 45 | Reads in a thread to avoid blocking a go block thread." 46 | [input] 47 | (log/trace :fn :input-stream->input-chan :msg "Creating new input-chan") 48 | (let [messages (async/chan 1)] 49 | ;; close output when channel closes 50 | (async/thread 51 | (with-open [reader (io/reader (io/input-stream input))] 52 | (loop [] 53 | (let [msg (read-message reader)] 54 | (cond 55 | ;; input closed; also close channel 56 | (= msg :parse-error) (do (log/debug :fn :input-stream->input-chan 57 | :error true 58 | :msg "Parse error or EOF") 59 | (async/close! messages)) 60 | :else (do (log/trace :fn :input-stream->input-chan :msg msg) 61 | (when (async/>!! messages msg) 62 | ;; wait for next message 63 | (recur)))))))) 64 | messages)) 65 | 66 | (defn output-stream->output-chan 67 | "Returns a channel which expects to have messages put on it. nil values are 68 | not allowed. Serializes and writes the messages to the output. When the 69 | channel is closed, closes the output. 70 | 71 | Writes in a thread to avoid blocking a go block thread." 72 | [output] 73 | (let [messages (async/chan 1)] 74 | ;; close output when channel closes 75 | (async/thread (with-open [writer (io/writer (io/output-stream output))] 76 | (loop [] 77 | (when-let [msg (async/output-chan :msg msg) 79 | (try 80 | (write-message writer msg) 81 | (catch Throwable e (async/close! messages) (throw e))) 82 | (recur))))) 83 | messages)) 84 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | ;;; Instructions: 2 | ;; Run a REPL with `make repl` 3 | {:aliases 4 | {:build {:deps {io.github.clojure/tools.build {:mvn/version "0.10.8"}, 5 | slipset/deps-deploy {:mvn/version "0.2.2"}}, 6 | :ns-default build}, 7 | :cider 8 | {:extra-deps {cider/cider-nrepl {:mvn/version "0.55.7"}, 9 | djblue/portal {:mvn/version "0.59.0"}, 10 | mx.cider/tools.deps.enrich-classpath {:mvn/version "1.19.3"}, 11 | nrepl/nrepl {:mvn/version "1.3.1"}, 12 | refactor-nrepl/refactor-nrepl {:mvn/version "3.10.0"}}, 13 | :main-opts 14 | ["-m" "nrepl.cmdline" "--interactive" "--color" "--middleware" 15 | "[cider.nrepl/cider-middleware,refactor-nrepl.middleware/wrap-refactor,portal.nrepl/wrap-portal]"]}, 16 | :cider-storm 17 | {:classpath-overrides {org.clojure/clojure nil}, 18 | :extra-deps {cider/cider-nrepl {:mvn/version "0.55.7"}, 19 | com.github.flow-storm/clojure {:mvn/version "1.12.0-9"}, 20 | com.github.flow-storm/flow-storm-dbg {:mvn/version "4.4.0"}, 21 | djblue/portal {:mvn/version "0.59.0"}, 22 | mx.cider/tools.deps.enrich-classpath {:mvn/version "1.19.3"}, 23 | nrepl/nrepl {:mvn/version "1.3.1"}, 24 | refactor-nrepl/refactor-nrepl {:mvn/version "3.10.0"}}, 25 | :jvm-opts 26 | ["-Dflowstorm.startRecording=false" "-Dclojure.storm.instrumentEnable=true" 27 | "-Dflowstorm.jarEditorCommand=emacsclient --eval '(let ((b (cider-find-file \"jar:file:<>!/<>\"))) (with-current-buffer b (switch-to-buffer b) (goto-char (point-min)) (forward-line (1- <>))))'" 28 | "-Dflowstorm.fileEditorCommand=emacsclient -n +<>:0 <>" 29 | "-Dclojure.storm.instrumentOnlyPrefixes=me.vedang., io.modelcontext.cljc-sdk."], 30 | :main-opts 31 | ["-m" "nrepl.cmdline" "--interactive" "--color" "--middleware" 32 | "[cider.nrepl/cider-middleware,refactor-nrepl.middleware/wrap-refactor,portal.nrepl/wrap-portal]"]}, 33 | :logs-dev 34 | {:jvm-opts 35 | ["-Dclojure.tools.logging.factory=clojure.tools.logging.impl/log4j2-factory" 36 | "-Dorg.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.Slf4jLog" 37 | "-Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector" 38 | "-Dlog4j2.configurationFile=logger/log4j2-mcp.xml" 39 | "-Dbabashka.json.provider=metosin/jsonista" "-Dlogging.level=DEBUG"]}, 40 | :test {:extra-deps {io.github.cognitect-labs/test-runner {:git/sha "dfb30dd", 41 | :git/tag "v0.5.1"}, 42 | org.clojure/test.check {:mvn/version "1.1.1"}}, 43 | :extra-paths ["test"]}}, 44 | ;; [tag: deps_rules_of_thumb] 45 | ;; 46 | ;; 1. Arrange deps alphabetically 47 | ;; 2. Write a short description about what the dep does 48 | :deps {;; STDIO / SSE transport implementation: Build on top of lsp4clj, 49 | ;; the excellent library provided by the clojure-lsp team. 50 | com.github.clojure-lsp/lsp4clj {:mvn/version "1.13.1"}, 51 | ;; Logging: Use me.vedang/logger as a thin wrapper over 52 | ;; pedestal.log and timbre. Uses log for Clojure, timbre for 53 | ;; Babashka projects. Uses SLF4J + Log4J2 54 | me.vedang/logger {:local/root "utils/logger"}, 55 | ;; JSON: Use babashka/json as a thin wrapper over metosin/jsonista 56 | ;; for Clojure, clojure.data/json for Babashka 57 | metosin/jsonista {:mvn/version "0.3.13"}, 58 | org.babashka/json {:mvn/version "0.1.6"}, 59 | ;; Clojure version 60 | org.clojure/clojure {:mvn/version "1.12.0"}, 61 | ;; Messaging: All messaging over the transport uses core.async to 62 | ;; hand-over work 63 | org.clojure/core.async {:mvn/version "1.8.741"}}, 64 | :paths ["src" "resources"]} 65 | -------------------------------------------------------------------------------- /examples/src/vegalite_server.clj: -------------------------------------------------------------------------------- 1 | (ns vegalite-server 2 | (:gen-class) 3 | (:require [babashka.fs :as fs] 4 | [babashka.json :as json] 5 | [babashka.process :as process] 6 | [io.modelcontext.clojure-sdk.stdio-server :as io-server] 7 | [me.vedang.logger.interface :as log])) 8 | 9 | (def saved-data 10 | (atom {"sample-data" [{:name "Alice", :age 25, :city "New York"} 11 | {:name "Bob", :age 30, :city "San Francisco"} 12 | {:name "Charlie", :age 35, :city "Los Angeles"}]})) 13 | 14 | (def vl-convert-executable 15 | (System/getProperty "mcp.vegalite.vl_convert_executable" "vl-convert")) 16 | 17 | (def tool-save-data 18 | {:name "save-data", 19 | :description 20 | "A tool to save data tables for later visualization. 21 | - Use when you have data to visualize later 22 | - Provide table name and data array", 23 | :inputSchema {:type "object", 24 | :properties {"name" {:type "string", 25 | :description 26 | "Table name to save data under"}, 27 | "data" {:type "array", 28 | :items {:type "object"}, 29 | :description "Data rows as objects"}}, 30 | :required ["name" "data"]}, 31 | :handler (fn [{:keys [name data]}] 32 | (swap! saved-data assoc name data) 33 | {:type "text", :text (format "Data saved to table '%s'" name)})}) 34 | 35 | (defn- vl2png 36 | [spec] 37 | (try (let [spec-file (str (fs/create-temp-file {:prefix "vegalite-spec-", 38 | :suffix ".json"})) 39 | output-file (str (fs/create-temp-file {:prefix "vegalite-", 40 | :suffix ".png"}))] 41 | ;; Write the spec to a temporary file 42 | (spit spec-file (json/write-str spec)) 43 | ;; Run vl-convert with the temp files 44 | (let [result (process/sh vl-convert-executable 45 | "vl2png" 46 | "--input" spec-file 47 | "--output" output-file)] 48 | ;; Clean up the spec file regardless of result 49 | (fs/delete spec-file) 50 | (if (zero? (:exit result)) 51 | (let [png-data (String. (.encode (java.util.Base64/getEncoder) 52 | (fs/read-all-bytes output-file)))] 53 | (fs/delete output-file) ; Clean up the temporary file 54 | {:type "image", :data png-data, :mimeType "image/png"}) 55 | (do (fs/delete output-file) ; Clean up even on error 56 | {:type "text", 57 | :text (str "PNG conversion error: " (:err result)), 58 | :is-error true})))) 59 | (catch Exception e 60 | {:type "text", 61 | :text (str "Conversion failed: " (.getMessage e)), 62 | :is-error true}))) 63 | 64 | (defn- visualize-data 65 | [{:keys [table-name vegalite-spec output-type], :or {output-type "png"}}] 66 | (try (if-let [data (get @saved-data table-name)] 67 | (let [spec (try (json/read-str vegalite-spec) 68 | (catch Exception e 69 | {:type "text", 70 | :text (str "Spec parse error: " e), 71 | :is-error true}))] 72 | (if (:is-error spec) 73 | spec 74 | (let [full-spec (assoc spec :data {:values data})] 75 | (case output-type 76 | "png" (vl2png full-spec) 77 | "txt" {:type "text", :text full-spec})))) 78 | {:type "text", 79 | :text (format "Data table '%s' not found" table-name), 80 | :is-error true}) 81 | (catch Exception e 82 | {:type "text", :text (str "Unexpected error: " e), :is-error true}))) 83 | 84 | (def tool-visualize-data 85 | {:name "visualize-data", 86 | :description 87 | "Tool to visualize data using Vega-Lite specs. 88 | - Use for complex data visualization 89 | - Requires pre-saved data table name 90 | - Provide Vega-Lite spec (without data)", 91 | :inputSchema 92 | {:type "object", 93 | :properties 94 | {"table-name" {:type "string", :description "Name of saved data table"}, 95 | "vegalite-spec" {:type "string", 96 | :description "Vega-Lite JSON spec (string)"}, 97 | "output-type" {:type "string", 98 | :description "One of `png` or `txt`, defines return type"}}, 99 | :required ["table-name" "vegalite-spec"]}, 100 | :handler visualize-data}) 101 | 102 | (def vegalite-server-spec 103 | {:name "vegalite", 104 | :version "1.0.0", 105 | :tools [;; Save Data 106 | tool-save-data 107 | ;; Visualize Data 108 | tool-visualize-data]}) 109 | 110 | (defn -main 111 | [& _args] 112 | (let [server-id (random-uuid)] 113 | (log/debug "[MAIN] Starting vegalite server: " server-id) 114 | @(io-server/run! (assoc vegalite-server-spec :server-id server-id)))) 115 | -------------------------------------------------------------------------------- /test/io/modelcontext/clojure_sdk/specs_test.clj: -------------------------------------------------------------------------------- 1 | (ns io.modelcontext.clojure-sdk.specs-test 2 | (:require [clojure.test :refer [deftest testing is]] 3 | [io.modelcontext.clojure-sdk.specs :as specs])) 4 | 5 | (deftest test-tool-validation 6 | (testing "Valid tool definitions" 7 | (let [simple-tool {:name "greet", :inputSchema {:type "object"}}] 8 | (is (specs/valid-tool? simple-tool))) 9 | (let [full-tool {:name "add-numbers", 10 | :description "Add two numbers together", 11 | :inputSchema 12 | {:type "object", 13 | :properties 14 | {"a" {:type "number", :description "First number"}, 15 | "b" {:type "number", :description "Second number"}}, 16 | :required ["a" "b"]}}] 17 | (is (specs/valid-tool? full-tool)))) 18 | (testing "Invalid tool definitions" 19 | (testing "Missing required fields" 20 | (let [no-name {:input-schema {:type "object"}}] 21 | (is (not (specs/valid-tool? no-name)))) 22 | (let [no-schema {:name "test"}] (is (not (specs/valid-tool? no-schema))))) 23 | (testing "Invalid schema type" 24 | (let [invalid-type {:name "test", :inputSchema {:type "invalid"}}] 25 | (is (not (specs/valid-tool? invalid-type))))) 26 | (testing "Invalid property types" 27 | (let [invalid-prop {:name "test", 28 | :inputSchema {:type "not-object", 29 | :properties {"test" "anything"}}}] 30 | (is (not (specs/valid-tool? invalid-prop))))))) 31 | 32 | (deftest test-resource-validation 33 | (testing "Valid resource definitions" 34 | (let [simple-resource {:uri "file:///test.txt", :name "Test File"}] 35 | (is (specs/valid-resource? simple-resource))) 36 | (let [full-resource {:uri "https://example.com/data.json", 37 | :name "Data", 38 | :description "Example data file", 39 | :mimeType "application/json"}] 40 | (is (specs/valid-resource? full-resource)))) 41 | (testing "Invalid resource definitions" 42 | (testing "Missing required fields" 43 | (let [no-uri {:name "Test"}] (is (not (specs/valid-resource? no-uri)))) 44 | (let [no-name {:uri "file:///test.txt"}] 45 | (is (not (specs/valid-resource? no-name))))))) 46 | 47 | (deftest test-prompt-validation 48 | (testing "Valid prompt definitions" 49 | (let [simple-prompt {:name "greet"}] 50 | (is (specs/valid-prompt? simple-prompt))) 51 | (let [full-prompt {:name "analyze-code", 52 | :description "Analyze code for improvements", 53 | :arguments [{:name "language", 54 | :description "Programming language", 55 | :required true} 56 | {:name "code", 57 | :description "Code to analyze", 58 | :required true}]}] 59 | (is (specs/valid-prompt? full-prompt)))) 60 | (testing "Invalid prompt definitions" 61 | (testing "Missing required fields" 62 | (let [no-name {}] (is (not (specs/valid-prompt? no-name))))) 63 | (testing "Invalid argument structure" 64 | (let [invalid-args {:name "test", 65 | :arguments [{:description "Missing name"}]}] 66 | (is (not (specs/valid-prompt? invalid-args)))) 67 | (let [invalid-arg-type {:name "test", :arguments "not-a-list"}] 68 | (is (not (specs/valid-prompt? invalid-arg-type))))))) 69 | 70 | (deftest test-message-content-validation 71 | (testing "Valid message contents" 72 | (let [text-content {:type "text", :text "Hello world"}] 73 | (is (specs/valid-text-content? text-content))) 74 | (let [image-content 75 | {:type "image", :data "base64data...", :mimeType "image/png"}] 76 | (is (specs/valid-image-content? image-content))) 77 | (let [audio-content 78 | {:type "audio", :data "base64data...", :mimeType "audio/mp3"}] 79 | (is (specs/valid-audio-content? audio-content)))) 80 | (testing "Invalid message contents" 81 | (testing "Missing required fields" 82 | (is (not (specs/valid-text-content? {:type "text"}))) 83 | (is (not (specs/valid-image-content? {:type "image", :data "test"}))) 84 | (is (not (specs/valid-audio-content? {:type "audio", 85 | :mimeType "audio/mp3"})))))) 86 | 87 | (deftest test-sampling-validation 88 | (testing "Valid sampling messages" 89 | (let [message {:role "assistant", 90 | :content {:type "text", :text "any text is fine"}}] 91 | (is (specs/valid-sampling-message? message)))) 92 | (testing "Valid model preferences" 93 | (let [preferences {:hints [{:name "claude-3"}], 94 | :costPriority 0.8, 95 | :speedPriority 0.5, 96 | :intelligencePriority 0.9}] 97 | (is (specs/valid-model-preferences? preferences))))) 98 | 99 | (deftest test-root-validation 100 | (testing "Valid root definitions" 101 | (let [simple-root {:uri "file:///workspace"}] 102 | (is (specs/valid-root? simple-root))) 103 | (let [named-root {:uri "file:///projects", :name "Projects Directory"}] 104 | (is (specs/valid-root? named-root)))) 105 | (testing "Invalid root definitions" 106 | (testing "Missing required fields" 107 | (let [no-uri {:name "Invalid Root"}] 108 | (is (not (specs/valid-root? no-uri))))))) 109 | 110 | (deftest test-implementation-validation 111 | (testing "Valid implementation info" 112 | (let [impl {:name "test-client", :version "1.0.0"}] 113 | (is (specs/valid-implementation? impl)))) 114 | (testing "Invalid implementation info" 115 | (testing "Missing required fields" 116 | (is (not (specs/valid-implementation? {:name "test-only"}))) 117 | (is (not (specs/valid-implementation? {:version "1.0.0"})))))) 118 | -------------------------------------------------------------------------------- /test/io/modelcontext/clojure_sdk/specs_gen_test.clj: -------------------------------------------------------------------------------- 1 | (ns io.modelcontext.clojure-sdk.specs-gen-test 2 | (:require [clojure.test.check.clojure-test :refer [defspec]] 3 | [clojure.test.check.generators :as gen] 4 | [clojure.test.check.properties :as prop] 5 | [io.modelcontext.clojure-sdk.specs :as specs])) 6 | 7 | ;; Custom generators 8 | (def gen-uri 9 | (gen/fmap #(str (gen/generate (gen/elements ["file" "http" "https" 10 | "resource"])) 11 | "://" 12 | %) 13 | (gen/not-empty gen/string-alphanumeric))) 14 | 15 | (def gen-property-type 16 | (gen/elements ["string" "number" "boolean" "array" "object"])) 17 | 18 | (def gen-property-with-description 19 | (gen/hash-map :type gen-property-type :description gen/string-alphanumeric)) 20 | 21 | (def gen-property-without-description (gen/hash-map :type gen-property-type)) 22 | 23 | (def gen-property 24 | (gen/frequency [[9 gen-property-with-description] 25 | [1 gen-property-without-description]])) 26 | 27 | (def gen-properties (gen/map gen/string-alphanumeric gen-property)) 28 | 29 | (def gen-inputSchema-with-properties 30 | (gen/hash-map :type (gen/return "object") 31 | :properties gen-properties 32 | :required (gen/vector gen/string-alphanumeric))) 33 | 34 | (def gen-inputSchema-without-properties 35 | (gen/hash-map :type (gen/return "object") 36 | :required (gen/vector gen/string-alphanumeric))) 37 | 38 | (def gen-inputSchema 39 | (gen/frequency [[9 gen-inputSchema-with-properties] 40 | [1 gen-inputSchema-without-properties]])) 41 | 42 | 43 | ;; Tool property tests 44 | (def gen-tool-with-description 45 | (gen/hash-map :name gen/string-alphanumeric 46 | :description gen/string-alphanumeric 47 | :inputSchema gen-inputSchema)) 48 | 49 | (def gen-tool-without-description 50 | (gen/hash-map :name gen/string-alphanumeric :inputSchema gen-inputSchema)) 51 | 52 | (def gen-tool 53 | (gen/frequency [[9 gen-tool-with-description] 54 | [1 gen-tool-without-description]])) 55 | 56 | (declare tool-validity ;; [ref: clj_kondo_needs_forward_declaration] 57 | resource-validity 58 | prompt-validity 59 | invalid-tool-rejection 60 | invalid-resource-rejection 61 | invalid-prompt-rejection) 62 | 63 | (defspec tool-validity 64 | 100 65 | (prop/for-all [tool gen-tool] (specs/valid-tool? tool))) 66 | 67 | ;; Resource property tests 68 | (def gen-resource-with-description-and-mime 69 | (gen/hash-map :uri gen-uri 70 | :name gen/string-alphanumeric 71 | :description gen/string-alphanumeric 72 | :mime-type gen/string-alphanumeric)) 73 | 74 | (def gen-resource-with-only-description 75 | (gen/hash-map :uri gen-uri 76 | :name gen/string-alphanumeric 77 | :description gen/string-alphanumeric)) 78 | 79 | (def gen-resource-with-only-mime 80 | (gen/hash-map :uri gen-uri 81 | :name gen/string-alphanumeric 82 | :mime-type gen/string-alphanumeric)) 83 | 84 | (def gen-resource-basic 85 | (gen/hash-map :uri gen-uri :name gen/string-alphanumeric)) 86 | 87 | (def gen-resource 88 | (gen/frequency [[5 gen-resource-with-description-and-mime] 89 | [2 gen-resource-with-only-description] 90 | [2 gen-resource-with-only-mime] [1 gen-resource-basic]])) 91 | 92 | (defspec resource-validity 93 | 100 94 | (prop/for-all [resource gen-resource] 95 | (specs/valid-resource? resource))) 96 | 97 | ;; Prompt property tests 98 | (def gen-argument 99 | (gen/hash-map :name gen/string-alphanumeric 100 | :description gen/string-alphanumeric 101 | :required gen/boolean)) 102 | 103 | (def gen-argument-without-description 104 | (gen/hash-map :name gen/string-alphanumeric :required gen/boolean)) 105 | 106 | (def gen-argument-without-required 107 | (gen/hash-map :name gen/string-alphanumeric 108 | :description gen/string-alphanumeric)) 109 | 110 | (def gen-argument-basic (gen/hash-map :name gen/string-alphanumeric)) 111 | 112 | (def gen-arguments 113 | (gen/vector (gen/frequency 114 | [[5 gen-argument] [2 gen-argument-without-description] 115 | [2 gen-argument-without-required] [1 gen-argument-basic]]))) 116 | 117 | (def gen-prompt-with-description 118 | (gen/hash-map :name gen/string-alphanumeric 119 | :description gen/string-alphanumeric 120 | :arguments gen-arguments)) 121 | 122 | (def gen-prompt-basic 123 | (gen/hash-map :name gen/string-alphanumeric :arguments gen-arguments)) 124 | 125 | (def gen-prompt 126 | (gen/frequency [[4 gen-prompt-with-description] [1 gen-prompt-basic]])) 127 | 128 | (defspec prompt-validity 129 | 100 130 | (prop/for-all [prompt gen-prompt] (specs/valid-prompt? prompt))) 131 | 132 | ;; Mutation tests - verify that invalid data is rejected 133 | (defspec invalid-tool-rejection 134 | 100 135 | (prop/for-all [tool 136 | (gen/hash-map :name (gen/one-of [gen/small-integer 137 | gen/boolean gen/ratio]) 138 | :inputSchema 139 | (gen/hash-map :type gen/small-integer))] 140 | (not (specs/valid-tool? tool)))) 141 | 142 | (defspec invalid-resource-rejection 143 | 100 144 | (prop/for-all 145 | [resource 146 | (gen/hash-map 147 | :uri (gen/one-of [gen/small-integer gen/boolean gen/ratio]) 148 | :name (gen/one-of [gen/small-integer gen/boolean gen/ratio]))] 149 | (not (specs/valid-resource? resource)))) 150 | 151 | (defspec invalid-prompt-rejection 152 | 100 153 | (prop/for-all 154 | [prompt 155 | (gen/hash-map 156 | :name (gen/one-of [gen/small-integer gen/boolean gen/ratio]) 157 | :arguments (gen/one-of [gen/small-integer gen/string gen/ratio]))] 158 | (not (specs/valid-prompt? prompt)))) 159 | 160 | ;;; [tag: clj_kondo_needs_forward_declaration] 161 | ;;; 162 | ;;; Macros like `defspec` create functions with the name we pass as the first 163 | ;;; argument. Since these variables are created in the namespace, we need to 164 | ;;; tell clj-kondo about them, otherwise it gets confused and throws Unresolved 165 | ;;; Symbol 166 | ;;; 167 | ;;; From: 168 | ;;; https://stackoverflow.com/questions/61727582/how-to-avoid-unresolved-symbol-with-clj-kond-when-using-hugsql-def-db-fns-macro 169 | -------------------------------------------------------------------------------- /examples/src/calculator_server.clj: -------------------------------------------------------------------------------- 1 | (ns calculator-server 2 | (:gen-class) 3 | (:require [io.modelcontext.clojure-sdk.stdio-server :as io-server] 4 | [me.vedang.logger.interface :as log])) 5 | 6 | (defn validate-array 7 | "Helper function to validate array inputs" 8 | [arr] 9 | (when-not (and (sequential? arr) (every? number? arr)) 10 | (throw (ex-info "Invalid input: Expected array of numbers" {:input arr})))) 11 | 12 | (def tool-add 13 | {:name "add", 14 | :description "Add two numbers together", 15 | :inputSchema {:type "object", 16 | :properties {"a" {:type "number", :description "First number"}, 17 | "b" {:type "number", 18 | :description "Second number"}}, 19 | :required ["a" "b"]}, 20 | :handler (fn [{:keys [a b]}] {:type "text", :text (str (+ a b))})}) 21 | 22 | (def tool-subtract 23 | {:name "subtract", 24 | :description "Subtract second number from first", 25 | :inputSchema {:type "object", 26 | :properties {"a" {:type "number", :description "First number"}, 27 | "b" {:type "number", 28 | :description "Second number"}}, 29 | :required ["a" "b"]}, 30 | :handler (fn [{:keys [a b]}] {:type "text", :text (str (- a b))})}) 31 | 32 | (def tool-multiply 33 | {:name "multiply", 34 | :description "Multiply two numbers", 35 | :inputSchema {:type "object", 36 | :properties {"a" {:type "number", :description "First number"}, 37 | "b" {:type "number", 38 | :description "Second number"}}, 39 | :required ["a" "b"]}, 40 | :handler (fn [{:keys [a b]}] {:type "text", :text (str (* a b))})}) 41 | 42 | (def tool-divide 43 | {:name "divide", 44 | :description "Divide first number by second", 45 | :inputSchema {:type "object", 46 | :properties {"a" {:type "number", :description "First number"}, 47 | "b" {:type "number", 48 | :description "Second number (non-zero)"}}, 49 | :required ["a" "b"]}, 50 | :handler 51 | (fn [{:keys [a b]}] 52 | (if (zero? b) 53 | {:type "text", :text "Error: Cannot divide by zero", :is-error true} 54 | {:type "text", :text (str (/ a b))}))}) 55 | 56 | (def tool-power 57 | {:name "power", 58 | :description "Raise a number to a power", 59 | :inputSchema {:type "object", 60 | :properties 61 | {"base" {:type "number", :description "Base number"}, 62 | "exponent" {:type "number", :description "Exponent"}}, 63 | :required ["base" "exponent"]}, 64 | :handler (fn [{:keys [base exponent]}] 65 | {:type "text", :text (str (Math/pow base exponent))})}) 66 | 67 | (def tool-square-root 68 | {:name "square-root", 69 | :description "Calculate the square root of a number", 70 | :inputSchema {:type "object", 71 | :properties {"number" {:type "number", 72 | :description 73 | "Number to find square root of"}}, 74 | :required ["number"]}, 75 | :handler (fn [{:keys [number]}] 76 | (if (neg? number) 77 | (throw (ex-info 78 | "Cannot calculate square root of negative number" 79 | {:input number})) 80 | {:type "text", :text (str (Math/sqrt number))}))}) 81 | 82 | (def tool-sum-array 83 | {:name "sum-array", 84 | :description "Calculate the sum of an array of numbers", 85 | :inputSchema {:type "object", 86 | :properties {"numbers" {:type "array", 87 | :items {:type "number"}, 88 | :description 89 | "Array of numbers to sum"}}, 90 | :required ["numbers"]}, 91 | :handler (fn [{:keys [numbers]}] 92 | #_{:clj-kondo/ignore [:clojure-lsp/unused-value]} 93 | (validate-array numbers) 94 | {:type "text", :text (str (reduce + numbers))})}) 95 | 96 | (def tool-average 97 | {:name "average", 98 | :description "Calculate the average of an array of numbers", 99 | :inputSchema {:type "object", 100 | :properties {"numbers" {:type "array", 101 | :items {:type "number"}, 102 | :description 103 | "Array of numbers to average"}}, 104 | :required ["numbers"]}, 105 | :handler (fn [{:keys [numbers]}] 106 | (validate-array numbers) 107 | (if (empty? numbers) 108 | {:type "text", 109 | :text "Error: Cannot calculate average of empty array", 110 | :isError true} 111 | {:type "text", 112 | :text (str (double (/ (reduce + numbers) 113 | (count numbers))))}))}) 114 | 115 | (def tool-factorial 116 | {:name "factorial", 117 | :description 118 | "Calculate the factorial of a number (demonstrates longer computation)", 119 | :inputSchema {:type "object", 120 | :properties {"number" {:type "number", 121 | :description 122 | "Number to calculate factorial of"}}, 123 | :required ["number"]}, 124 | :handler (fn [{:keys [number]}] 125 | (if (or (neg? number) (not (integer? number))) 126 | {:type "text", 127 | :text "Error: Factorial requires a non-negative integer", 128 | :isError true} 129 | ;; Simulate longer computation for large numbers 130 | (do (when (> number 10) (Thread/sleep 1000)) 131 | {:type "text", 132 | :text (str (reduce * (range 1 (inc number))))})))}) 133 | 134 | (def calculator-server-spec 135 | {:name "calculator", 136 | :version "1.0.0", 137 | :tools [;; Basic arithmetic operations 138 | tool-add tool-subtract tool-multiply tool-divide 139 | ;; Advanced mathematical operations 140 | tool-power tool-square-root 141 | ;; Array operations to test complex inputs 142 | tool-sum-array tool-average 143 | ;; Test long-running operation 144 | tool-factorial]}) 145 | 146 | (defn -main 147 | [& _args] 148 | (let [server-id (random-uuid)] 149 | (log/debug "[MAIN] Starting calculator server: " server-id) 150 | @(io-server/run! (assoc calculator-server-spec :server-id server-id)))) 151 | 152 | (comment 153 | ;; Test power / maybe overflow 154 | "What's 2 to the power of 1000?" 155 | ;; Test array operations 156 | "What's the average of [1, 2, 3, 4, 5]?" 157 | "Sum up the numbers [10, 20, 30, 40, 50]" 158 | "Calculate the average of []" ; Test empty array 159 | ;; Test long computation 160 | "What's the factorial of 15?" ; Should take > 1 second 161 | ;; Test error handling 162 | "What's the square root of -4? Use the square-root tool" 163 | "Calculate the factorial of -1") 164 | -------------------------------------------------------------------------------- /examples/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: install-antq install-kondo-configs install-zprint-config install-gitignore repl-enrich repl check-cljkondo check-tagref check-zprint-config check-zprint check test test-all test-coverage upgrade-libs build serve deploy clean-projects clean 2 | 3 | HOME := $(shell echo $$HOME) 4 | HERE := $(shell echo $$PWD) 5 | CLOJURE_SOURCES := $(shell find . -name '**.clj' -not -path './.clj-kondo/*') 6 | 7 | # Set bash instead of sh for the @if [[ conditions, 8 | # and use the usual safety flags: 9 | SHELL = /bin/bash -Eeu 10 | 11 | .DEFAULT_GOAL := repl 12 | 13 | help: ## A brief explanation of everything you can do 14 | @awk '/^[a-zA-Z0-9_-]+:.*##/ { \ 15 | printf "%-25s # %s\n", \ 16 | substr($$1, 1, length($$1)-1), \ 17 | substr($$0, index($$0,"##")+3) \ 18 | }' $(MAKEFILE_LIST) 19 | 20 | # The Clojure CLI aliases that will be selected for main options for `repl`. 21 | # Feel free to upgrade this, or to override it with an env var named DEPS_MAIN_OPTS. 22 | # Expected format: "-M:alias1:alias2" 23 | DEPS_MAIN_OPTS ?= "-M:dev:test:logs-dev:cider-storm" 24 | 25 | repl: ## Launch a REPL using the Clojure CLI 26 | clojure $(DEPS_MAIN_OPTS); 27 | 28 | # The enrich-classpath version to be injected. 29 | # Feel free to upgrade this. 30 | ENRICH_CLASSPATH_VERSION="1.19.3" 31 | 32 | # Create and cache a `clojure` command. deps.edn is mandatory; the others are optional but are taken into account for cache recomputation. 33 | # It's important not to silence with step with @ syntax, so that Enrich progress can be seen as it resolves dependencies. 34 | .enrich-classpath-repl: Makefile deps.edn $(wildcard $(HOME)/.clojure/deps.edn) $(wildcard $(XDG_CONFIG_HOME)/.clojure/deps.edn) 35 | cd $$(mktemp -d -t enrich-classpath.XXXXXX); clojure -Sforce -Srepro -J-XX:-OmitStackTraceInFastThrow -J-Dclojure.main.report=stderr -Sdeps '{:deps {mx.cider/tools.deps.enrich-classpath {:mvn/version $(ENRICH_CLASSPATH_VERSION)}}}' -M -m cider.enrich-classpath.clojure "clojure" "$(HERE)" "true" $(DEPS_MAIN_OPTS) | grep "^clojure" > $(HERE)/$@ 36 | 37 | # Launches a repl, falling back to vanilla Clojure repl if something went wrong during classpath calculation. 38 | repl-enrich: .enrich-classpath-repl ## Launch a repl enriched with Java source code paths 39 | @if grep --silent "^clojure" .enrich-classpath-repl; then \ 40 | echo "Executing: $$(cat .enrich-classpath-repl)" && \ 41 | eval $$(cat .enrich-classpath-repl); \ 42 | else \ 43 | echo "Falling back to Clojure repl... (you can avoid further falling back by removing .enrich-classpath-repl)"; \ 44 | clojure $(DEPS_MAIN_OPTS); \ 45 | fi 46 | 47 | .clj-kondo: 48 | mkdir .clj-kondo 49 | 50 | install-kondo-configs: .clj-kondo ## Install clj-kondo configs for all the currently installed deps 51 | clj-kondo --lint "$$(clojure -A:dev:test:cider:build -Spath)" --copy-configs --skip-lint 52 | 53 | check-zprint-config: 54 | @echo "Checking (HOME)/.zprint.edn..." 55 | @if [ ! -f "$(HOME)/.zprint.edn" ]; then \ 56 | echo "Error: ~/.zprint.edn not found"; \ 57 | echo "Please create ~/.zprint.edn with the content: {:search-config? true}"; \ 58 | exit 1; \ 59 | fi 60 | @if ! grep -q "search-config?" "$(HOME)/.zprint.edn"; then \ 61 | echo "Warning: ~/.zprint.edn might not contain required {:search-config? true} setting"; \ 62 | echo "Please ensure this setting is present for proper functionality"; \ 63 | exit 1; \ 64 | fi 65 | 66 | .zprint.edn: 67 | @echo "Creating .zprint.edn..." 68 | @echo '{:fn-map {"with-context" "with-meta"}, :map {:indent 0}}' > $@ 69 | 70 | .dir-locals.el: 71 | @echo "Creating .dir-locals.el..." 72 | @echo ';;; Directory Local Variables -*- no-byte-compile: t; -*-' > $@ 73 | @echo ';;; For more information see (info "(emacs) Directory Variables")' >> $@ 74 | @echo '((clojure-dart-ts-mode . ((apheleia-formatter . (zprint))))' >> $@ 75 | @echo ' (clojure-jank-ts-mode . ((apheleia-formatter . (zprint))))' >> $@ 76 | @echo ' (clojure-mode . ((apheleia-formatter . (zprint))))' >> $@ 77 | @echo ' (clojure-ts-mode . ((apheleia-formatter . (zprint))))' >> $@ 78 | @echo ' (clojurec-mode . ((apheleia-formatter . (zprint))))' >> $@ 79 | @echo ' (clojurec-ts-mode . ((apheleia-formatter . (zprint))))' >> $@ 80 | @echo ' (clojurescript-mode . ((apheleia-formatter . (zprint))))' >> $@ 81 | @echo ' (clojurescript-ts-mode . ((apheleia-formatter . (zprint)))))' >> $@ 82 | 83 | install-zprint-config: check-zprint-config .zprint.edn .dir-locals.el ## Install configuration for using the zprint formatter 84 | @echo "zprint configuration files created successfully." 85 | 86 | .gitignore: 87 | @echo "Creating a .gitignore file" 88 | @echo '# Artifacts' > $@ 89 | @echo '**/classes' >> $@ 90 | @echo '**/target' >> $@ 91 | @echo '**/.artifacts' >> $@ 92 | @echo '**/.cpcache' >> $@ 93 | @echo '**/.DS_Store' >> $@ 94 | @echo '**/.gradle' >> $@ 95 | @echo 'logs/' >> $@ 96 | @echo '' >> $@ 97 | @echo '# 12-factor App Configuration' >> $@ 98 | @echo '.envrc' >> $@ 99 | @echo '' >> $@ 100 | @echo '# User-specific stuff' >> $@ 101 | @echo '.idea/**/workspace.xml' >> $@ 102 | @echo '.idea/**/tasks.xml' >> $@ 103 | @echo '.idea/**/usage.statistics.xml' >> $@ 104 | @echo '.idea/**/shelf' >> $@ 105 | @echo '.idea/**/statistic.xml' >> $@ 106 | @echo '.idea/dictionaries/**' >> $@ 107 | @echo '.idea/libraries/**' >> $@ 108 | @echo '' >> $@ 109 | @echo '# File-based project format' >> $@ 110 | @echo '*.iws' >> $@ 111 | @echo '*.ipr' >> $@ 112 | @echo '' >> $@ 113 | @echo '# Cursive Clojure plugin' >> $@ 114 | @echo '.idea/replstate.xml' >> $@ 115 | @echo '*.iml' >> $@ 116 | @echo '' >> $@ 117 | @echo '/example/example/**' >> $@ 118 | @echo 'artifacts' >> $@ 119 | @echo 'projects/**/pom.xml' >> $@ 120 | @echo '' >> $@ 121 | @echo '# nrepl' >> $@ 122 | @echo '.nrepl-port' >> $@ 123 | @echo '' >> $@ 124 | @echo '# clojure-lsp' >> $@ 125 | @echo '.lsp/.cache' >> $@ 126 | @echo '' >> $@ 127 | @echo '# clj-kondo' >> $@ 128 | @echo '.clj-kondo/.cache' >> $@ 129 | @echo '' >> $@ 130 | @echo '# Calva VS Code Extension' >> $@ 131 | @echo '.calva/output-window/output.calva-repl' >> $@ 132 | @echo '' >> $@ 133 | @echo '# Metaclj tempfiles' >> $@ 134 | @echo '.antqtool.lastupdated' >> $@ 135 | @echo '.enrich-classpath-repl' >> $@ 136 | 137 | install-gitignore: .gitignore ## Install a meaningful .gitignore file 138 | @echo ".gitignore added/exists in the project" 139 | 140 | check-tagref: 141 | tagref 142 | 143 | check-cljkondo: 144 | clj-kondo --lint . 145 | 146 | check-zprint: 147 | zprint -c $(CLOJURE_SOURCES) 148 | 149 | check: check-tagref check-cljkondo check-zprint ## Check that the code is well linted and well formatted 150 | @echo "All checks passed!" 151 | 152 | format: 153 | zprint -lfw $(CLOJURE_SOURCES) 154 | 155 | test-coverage: 156 | clojure -X:dev:test:clofidence 157 | 158 | test: ## Run all the tests for the code 159 | clojure -T:build test 160 | 161 | install-antq: 162 | @if [ -f .antqtool.lastupdated ] && find .antqtool.lastupdated -mtime +15 -print | grep -q .; then \ 163 | echo "Updating antq tool to the latest version..."; \ 164 | clojure -Ttools install-latest :lib com.github.liquidz/antq :as antq; \ 165 | touch .antqtool.lastupdated; \ 166 | else \ 167 | echo "Skipping antq tool update..."; \ 168 | fi 169 | 170 | .antqtool.lastupdated: 171 | touch .antqtool.lastupdated 172 | 173 | upgrade-libs: .antqtool.lastupdated install-antq ## Install all the deps to their latest versions 174 | clojure -Tantq outdated :check-clojure-tools true :upgrade true 175 | 176 | build: ## Build the deployment artifact 177 | clojure -T:build ci 178 | 179 | install: build ## Install the artifact locally 180 | clojure -T:build install 181 | 182 | deploy: build ## Deploy the current code to production 183 | @echo "Run fly.io deployment commands here!" 184 | 185 | clean-projects: 186 | rm -rf target/ 187 | 188 | clean: clean-projects ## Delete any existing artifacts 189 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # examples 2 | 3 | 1. Calculator: `calculator_server` 4 | 2. Vega-lite: `vegalite_server` 5 | 3. Code Analysis: `code_analysis_server` 6 | 7 | ## Building the Jar 8 | 9 | $ make clean && make build 10 | 11 | ## Running the MCP Servers 12 | 13 | ### The Calculator MCP server 14 | Provides basic arithmetic tools: `add`, `subtract`, `multiply`, `divide`, `power`, `square-root`, `average`, `factorial` 15 | 16 | Some example commands you can try in Claude Desktop or Inspector: 17 | 18 | 1. What's the average of [1, 2, 3, 4, 5]? 19 | 2. What's the factorial of 15? 20 | 2. What's 2 to the power of 1000? 21 | 3. What's the square-root of 64? 22 | 23 | #### Before running the calculator MCP server: 24 | Remember: 25 | 1. Use the full-path to the examples JAR on your system 26 | 27 | #### In Claude Desktop 28 | 29 | ```json 30 | "calculator": { 31 | "command": "java", 32 | "args": [ 33 | "-Dclojure.tools.logging.factory=clojure.tools.logging.impl/log4j2-factory", 34 | "-Dorg.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.Slf4jLog", 35 | "-Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector", 36 | "-Dlog4j2.configurationFile=log4j2-mcp.xml", 37 | "-Dbabashka.json.provider=metosin/jsonista", 38 | "-Dlogging.level=INFO", 39 | "-cp", 40 | "/Users/vedang/mcp-clojure-sdk/examples/target/io.modelcontextprotocol.clojure-sdk/examples-1.2.0.jar", 41 | "calculator_server" 42 | ] 43 | } 44 | ``` 45 | 46 | #### In MCP Inspector 47 | 48 | ```shell 49 | npx @modelcontextprotocol/inspector java -Dclojure.tools.logging.factory=clojure.tools.logging.impl/log4j2-factory -Dorg.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.Slf4jLog -Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector -Dlog4j2.configurationFile=log4j2-mcp.xml -Dbabashka.json.provider=metosin/jsonista -Dlogging.level=INFO -cp examples/target/io.modelcontextprotocol.clojure-sdk/examples-1.2.0.jar calculator_server 50 | ``` 51 | 52 | ### The Vegalite MCP server 53 | Provides tools for generating Vega-lite charts: `save-data`, `visualize-data` 54 | 55 | PRE-REQUISITES: Needs [vl-convert CLI](https://github.com/vega/vl-convert) to be installed. 56 | 57 | Some example commands you can try in Claude Desktop or Inspector: 58 | 59 | Here is some example data for you: 60 | ```json 61 | [ 62 | { "year": 2011, "value": 14.6, "growth_type": "Market Cap Growth" }, 63 | { "year": 2011, "value": 11.4, "growth_type": "Revenue Growth" }, 64 | { "year": 2011, "value": 26.6, "growth_type": "Net Income Growth" }, 65 | { "year": 2012, "value": 40.1, "growth_type": "Market Cap Growth" }, 66 | { "year": 2012, "value": 42.7, "growth_type": "Revenue Growth" }, 67 | { "year": 2012, "value": 36.9, "growth_type": "Net Income Growth" }, 68 | { "year": 2013, "value": 16.9, "growth_type": "Market Cap Growth" }, 69 | { "year": 2013, "value": 14.6, "growth_type": "Revenue Growth" }, 70 | { "year": 2013, "value": 15.3, "growth_type": "Net Income Growth" }, 71 | { "year": 2014, "value": 9.6, "growth_type": "Market Cap Growth" }, 72 | { "year": 2014, "value": 7.9, "growth_type": "Revenue Growth" }, 73 | { "year": 2014, "value": 10.9, "growth_type": "Net Income Growth" }, 74 | { "year": 2015, "value": 5.8, "growth_type": "Market Cap Growth" }, 75 | { "year": 2015, "value": 6.7, "growth_type": "Revenue Growth" }, 76 | { "year": 2015, "value": 6.2, "growth_type": "Net Income Growth" }, 77 | { "year": 2016, "value": -12.4, "growth_type": "Market Cap Growth" }, 78 | { "year": 2016, "value": -3.9, "growth_type": "Revenue Growth" }, 79 | { "year": 2016, "value": -32.2, "growth_type": "Net Income Growth" }, 80 | { "year": 2017, "value": 25.3, "growth_type": "Market Cap Growth" }, 81 | { "year": 2017, "value": 5.9, "growth_type": "Revenue Growth" }, 82 | { "year": 2017, "value": 43.9, "growth_type": "Net Income Growth" } 83 | ] 84 | ``` 85 | Visualize this data for me using vega-lite. 86 | 87 | #### Before running the vegalite MCP server 88 | Remember: 89 | 1. Replace the full-path to the examples JAR with the correct path on your system 90 | 2. Specify the full-path to `vl-convert` on your system 91 | 92 | #### In Claude Desktop 93 | 94 | ```json 95 | "vegalite": { 96 | "command": "java", 97 | "args": [ 98 | "-Dclojure.tools.logging.factory=clojure.tools.logging.impl/log4j2-factory", 99 | "-Dorg.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.Slf4jLog", 100 | "-Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector", 101 | "-Dlog4j2.configurationFile=log4j2-mcp.xml", 102 | "-Dbabashka.json.provider=metosin/jsonista", 103 | "-Dlogging.level=INFO", 104 | "-Dmcp.vegalite.vl_convert_executable=/Users/vedang/.cargo/bin/vl-convert", 105 | "-cp", 106 | "/Users/vedang/mcp-clojure-sdk/examples/target/io.modelcontextprotocol.clojure-sdk/examples-1.2.0.jar", 107 | "vegalite_server" 108 | ] 109 | } 110 | ``` 111 | 112 | #### In MCP Inspector 113 | (Remember to use the full-path to the examples JAR on your system, or execute this command from the `mcp-clojure-sdk` repo) 114 | 115 | ```shell 116 | npx @modelcontextprotocol/inspector java -Dclojure.tools.logging.factory=clojure.tools.logging.impl/log4j2-factory -Dorg.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.Slf4jLog -Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector -Dlog4j2.configurationFile=log4j2-mcp.xml -Dbabashka.json.provider=metosin/jsonista -Dlogging.level=INFO -Dmcp.vegalite.vl_convert_executable=/Users/vedang/.cargo/bin/vl-convert -cp examples/target/io.modelcontextprotocol.clojure-sdk/examples-1.2.0.jar vegalite_server 117 | ``` 118 | 119 | ### The Code-Analysis MCP server 120 | This server is an example of a server which provides prompts and not tools. The following prompts are available: `analyse-code` and `poem-about-code`. 121 | 122 | You can try the prompts out in Claude Desktop or Inspector. While these prompts are very basic, this is a good way to see how you could expose powerful prompts through this technique. 123 | 124 | #### Before running the code-analysis MCP server 125 | Remember: 126 | 1. Replace the full-path to the examples JAR with the correct path on your system 127 | 128 | #### In Claude Desktop 129 | 130 | ```json 131 | "code-anaylsis": { 132 | "command": "java", 133 | "args": [ 134 | "-Dclojure.tools.logging.factory=clojure.tools.logging.impl/log4j2-factory", 135 | "-Dorg.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.Slf4jLog", 136 | "-Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector", 137 | "-Dlog4j2.configurationFile=log4j2-mcp.xml", 138 | "-Dbabashka.json.provider=metosin/jsonista", 139 | "-Dlogging.level=INFO", 140 | "-cp", 141 | "/Users/vedang/mcp-clojure-sdk/examples/target/io.modelcontextprotocol.clojure-sdk/examples-1.2.0.jar", 142 | "code_analysis_server" 143 | ] 144 | } 145 | ``` 146 | 147 | #### In MCP Inspector 148 | (Remember to use the full-path to the examples JAR on your system, or execute this command from the `mcp-clojure-sdk` repo) 149 | 150 | ```shell 151 | npx @modelcontextprotocol/inspector java -Dclojure.tools.logging.factory=clojure.tools.logging.impl/log4j2-factory -Dorg.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.Slf4jLog -Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector -Dlog4j2.configurationFile=log4j2-mcp.xml -Dbabashka.json.provider=metosin/jsonista -Dlogging.level=INFO -cp examples/target/io.modelcontextprotocol.clojure-sdk/examples-1.2.0.jar code_analysis_server 152 | ``` 153 | 154 | ## License 155 | 156 | Copyright © 2025 Unravel.tech 157 | 158 | Distributed under the MIT License. 159 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: install-antq install-kondo-configs install-zprint-config install-gitignore repl-enrich repl check-cljkondo check-tagref check-zprint-config check-zprint check test test-all test-coverage upgrade-libs build serve deploy clean-projects clean examples-jar 2 | 3 | HOME := $(shell echo $$HOME) 4 | HERE := $(shell echo $$PWD) 5 | CLOJURE_SOURCES := $(shell find . -name '**.clj' -not -path './.clj-kondo/*') 6 | 7 | # Set bash instead of sh for the @if [[ conditions, 8 | # and use the usual safety flags: 9 | SHELL = /bin/bash -Eeu 10 | 11 | .DEFAULT_GOAL := repl 12 | 13 | help: ## A brief explanation of everything you can do 14 | @awk '/^[a-zA-Z0-9_-]+:.*##/ { \ 15 | printf "%-25s # %s\n", \ 16 | substr($$1, 1, length($$1)-1), \ 17 | substr($$0, index($$0,"##")+3) \ 18 | }' $(MAKEFILE_LIST) 19 | 20 | # The Clojure CLI aliases that will be selected for main options for `repl`. 21 | # Feel free to upgrade this, or to override it with an env var named DEPS_MAIN_OPTS. 22 | # Expected format: "-M:alias1:alias2" 23 | DEPS_MAIN_OPTS ?= "-M:dev:test:logs-dev:cider-storm" 24 | 25 | repl: ## Launch a REPL using the Clojure CLI 26 | clojure $(DEPS_MAIN_OPTS); 27 | 28 | # The enrich-classpath version to be injected. 29 | # Feel free to upgrade this. 30 | ENRICH_CLASSPATH_VERSION="1.19.3" 31 | 32 | # Create and cache a `clojure` command. deps.edn is mandatory; the others are optional but are taken into account for cache recomputation. 33 | # It's important not to silence with step with @ syntax, so that Enrich progress can be seen as it resolves dependencies. 34 | .enrich-classpath-repl: Makefile deps.edn $(wildcard $(HOME)/.clojure/deps.edn) $(wildcard $(XDG_CONFIG_HOME)/.clojure/deps.edn) 35 | cd $$(mktemp -d -t enrich-classpath.XXXXXX); clojure -Sforce -Srepro -J-XX:-OmitStackTraceInFastThrow -J-Dclojure.main.report=stderr -Sdeps '{:deps {mx.cider/tools.deps.enrich-classpath {:mvn/version $(ENRICH_CLASSPATH_VERSION)}}}' -M -m cider.enrich-classpath.clojure "clojure" "$(HERE)" "true" $(DEPS_MAIN_OPTS) | grep "^clojure" > $(HERE)/$@ 36 | 37 | # Launches a repl, falling back to vanilla Clojure repl if something went wrong during classpath calculation. 38 | repl-enrich: .enrich-classpath-repl ## Launch a repl enriched with Java source code paths 39 | @if grep --silent "^clojure" .enrich-classpath-repl; then \ 40 | echo "Executing: $$(cat .enrich-classpath-repl)" && \ 41 | eval $$(cat .enrich-classpath-repl); \ 42 | else \ 43 | echo "Falling back to Clojure repl... (you can avoid further falling back by removing .enrich-classpath-repl)"; \ 44 | clojure $(DEPS_MAIN_OPTS); \ 45 | fi 46 | 47 | .clj-kondo: 48 | mkdir .clj-kondo 49 | 50 | install-kondo-configs: .clj-kondo ## Install clj-kondo configs for all the currently installed deps 51 | clj-kondo --lint "$$(clojure -A:dev:test:cider:build -Spath)" --copy-configs --skip-lint 52 | 53 | check-zprint-config: 54 | @echo "Checking (HOME)/.zprint.edn..." 55 | @if [ ! -f "$(HOME)/.zprint.edn" ]; then \ 56 | echo "Error: ~/.zprint.edn not found"; \ 57 | echo "Please create ~/.zprint.edn with the content: {:search-config? true}"; \ 58 | exit 1; \ 59 | fi 60 | @if ! grep -q "search-config?" "$(HOME)/.zprint.edn"; then \ 61 | echo "Warning: ~/.zprint.edn might not contain required {:search-config? true} setting"; \ 62 | echo "Please ensure this setting is present for proper functionality"; \ 63 | exit 1; \ 64 | fi 65 | 66 | .zprint.edn: 67 | @echo "Creating .zprint.edn..." 68 | @echo '{:fn-map {"with-context" "with-meta"}, :map {:indent 0}}' > $@ 69 | 70 | .dir-locals.el: 71 | @echo "Creating .dir-locals.el..." 72 | @echo ';;; Directory Local Variables -*- no-byte-compile: t; -*-' > $@ 73 | @echo ';;; For more information see (info "(emacs) Directory Variables")' >> $@ 74 | @echo '((clojure-dart-ts-mode . ((apheleia-formatter . (zprint))))' >> $@ 75 | @echo ' (clojure-jank-ts-mode . ((apheleia-formatter . (zprint))))' >> $@ 76 | @echo ' (clojure-mode . ((apheleia-formatter . (zprint))))' >> $@ 77 | @echo ' (clojure-ts-mode . ((apheleia-formatter . (zprint))))' >> $@ 78 | @echo ' (clojurec-mode . ((apheleia-formatter . (zprint))))' >> $@ 79 | @echo ' (clojurec-ts-mode . ((apheleia-formatter . (zprint))))' >> $@ 80 | @echo ' (clojurescript-mode . ((apheleia-formatter . (zprint))))' >> $@ 81 | @echo ' (clojurescript-ts-mode . ((apheleia-formatter . (zprint)))))' >> $@ 82 | 83 | install-zprint-config: check-zprint-config .zprint.edn .dir-locals.el ## Install configuration for using the zprint formatter 84 | @echo "zprint configuration files created successfully." 85 | 86 | .gitignore: 87 | @echo "Creating a .gitignore file" 88 | @echo '# Artifacts' > $@ 89 | @echo '**/classes' >> $@ 90 | @echo '**/target' >> $@ 91 | @echo '**/.artifacts' >> $@ 92 | @echo '**/.cpcache' >> $@ 93 | @echo '**/.DS_Store' >> $@ 94 | @echo '**/.gradle' >> $@ 95 | @echo 'logs/' >> $@ 96 | @echo '' >> $@ 97 | @echo '# 12-factor App Configuration' >> $@ 98 | @echo '.envrc' >> $@ 99 | @echo '' >> $@ 100 | @echo '# User-specific stuff' >> $@ 101 | @echo '.idea/**/workspace.xml' >> $@ 102 | @echo '.idea/**/tasks.xml' >> $@ 103 | @echo '.idea/**/usage.statistics.xml' >> $@ 104 | @echo '.idea/**/shelf' >> $@ 105 | @echo '.idea/**/statistic.xml' >> $@ 106 | @echo '.idea/dictionaries/**' >> $@ 107 | @echo '.idea/libraries/**' >> $@ 108 | @echo '' >> $@ 109 | @echo '# File-based project format' >> $@ 110 | @echo '*.iws' >> $@ 111 | @echo '*.ipr' >> $@ 112 | @echo '' >> $@ 113 | @echo '# Cursive Clojure plugin' >> $@ 114 | @echo '.idea/replstate.xml' >> $@ 115 | @echo '*.iml' >> $@ 116 | @echo '' >> $@ 117 | @echo '/example/example/**' >> $@ 118 | @echo 'artifacts' >> $@ 119 | @echo 'projects/**/pom.xml' >> $@ 120 | @echo '' >> $@ 121 | @echo '# nrepl' >> $@ 122 | @echo '.nrepl-port' >> $@ 123 | @echo '' >> $@ 124 | @echo '# clojure-lsp' >> $@ 125 | @echo '.lsp/.cache' >> $@ 126 | @echo '' >> $@ 127 | @echo '# clj-kondo' >> $@ 128 | @echo '.clj-kondo/.cache' >> $@ 129 | @echo '' >> $@ 130 | @echo '# Calva VS Code Extension' >> $@ 131 | @echo '.calva/output-window/output.calva-repl' >> $@ 132 | @echo '' >> $@ 133 | @echo '# Metaclj tempfiles' >> $@ 134 | @echo '.antqtool.lastupdated' >> $@ 135 | @echo '.enrich-classpath-repl' >> $@ 136 | 137 | install-gitignore: .gitignore ## Install a meaningful .gitignore file 138 | @echo ".gitignore added/exists in the project" 139 | 140 | check-tagref: 141 | tagref 142 | 143 | check-cljkondo: 144 | clj-kondo --lint . 145 | 146 | check-zprint: 147 | zprint -c $(CLOJURE_SOURCES) 148 | 149 | check: check-tagref check-cljkondo check-zprint ## Check that the code is well linted and well formatted 150 | @echo "All checks passed!" 151 | 152 | format: 153 | zprint -lfw $(CLOJURE_SOURCES) 154 | 155 | test-coverage: 156 | clojure -X:dev:test:clofidence 157 | 158 | test: ## Run all the tests for the code 159 | clojure -T:build test 160 | 161 | install-antq: 162 | @if [ -f .antqtool.lastupdated ] && find .antqtool.lastupdated -mtime +15 -print | grep -q .; then \ 163 | echo "Updating antq tool to the latest version..."; \ 164 | clojure -Ttools install-latest :lib com.github.liquidz/antq :as antq; \ 165 | touch .antqtool.lastupdated; \ 166 | else \ 167 | echo "Skipping antq tool update..."; \ 168 | fi 169 | 170 | .antqtool.lastupdated: 171 | touch .antqtool.lastupdated 172 | 173 | upgrade-libs: .antqtool.lastupdated install-antq ## Install all the deps to their latest versions 174 | clojure -Tantq outdated :check-clojure-tools true :upgrade true 175 | 176 | build: check ## Build the deployment artifact 177 | clojure -T:build ci 178 | 179 | install: build ## Install the artifact locally 180 | clojure -T:build install 181 | 182 | deploy: build ## Deploy to Clojars. needs `CLOJARS_USERNAME` and `CLOJARS_PASSWORD` env vars 183 | clojure -T:build deploy 184 | 185 | clean-examples: 186 | rm -rf examples/target 187 | 188 | clean-sdk: 189 | rm -rf target/ 190 | 191 | clean: clean-examples clean-sdk 192 | 193 | examples-jar: examples/Makefile 194 | $(MAKE) -C examples build 195 | -------------------------------------------------------------------------------- /examples/LICENSE: -------------------------------------------------------------------------------- 1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and 10 | documentation distributed under this Agreement, and 11 | 12 | b) in the case of each subsequent Contributor: 13 | 14 | i) changes to the Program, and 15 | 16 | ii) additions to the Program; 17 | 18 | where such changes and/or additions to the Program originate from and are 19 | distributed by that particular Contributor. A Contribution 'originates' from 20 | a Contributor if it was added to the Program by such Contributor itself or 21 | anyone acting on such Contributor's behalf. Contributions do not include 22 | additions to the Program which: (i) are separate modules of software 23 | distributed in conjunction with the Program under their own license 24 | agreement, and (ii) are not derivative works of the Program. 25 | 26 | "Contributor" means any person or entity that distributes the Program. 27 | 28 | "Licensed Patents" mean patent claims licensable by a Contributor which are 29 | necessarily infringed by the use or sale of its Contribution alone or when 30 | combined with the Program. 31 | 32 | "Program" means the Contributions distributed in accordance with this 33 | Agreement. 34 | 35 | "Recipient" means anyone who receives the Program under this Agreement, 36 | including all Contributors. 37 | 38 | 2. GRANT OF RIGHTS 39 | 40 | a) Subject to the terms of this Agreement, each Contributor hereby grants 41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 42 | reproduce, prepare derivative works of, publicly display, publicly perform, 43 | distribute and sublicense the Contribution of such Contributor, if any, and 44 | such derivative works, in source code and object code form. 45 | 46 | b) Subject to the terms of this Agreement, each Contributor hereby grants 47 | Recipient a non-exclusive, worldwide, royalty-free patent license under 48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 49 | transfer the Contribution of such Contributor, if any, in source code and 50 | object code form. This patent license shall apply to the combination of the 51 | Contribution and the Program if, at the time the Contribution is added by the 52 | Contributor, such addition of the Contribution causes such combination to be 53 | covered by the Licensed Patents. The patent license shall not apply to any 54 | other combinations which include the Contribution. No hardware per se is 55 | licensed hereunder. 56 | 57 | c) Recipient understands that although each Contributor grants the licenses 58 | to its Contributions set forth herein, no assurances are provided by any 59 | Contributor that the Program does not infringe the patent or other 60 | intellectual property rights of any other entity. Each Contributor disclaims 61 | any liability to Recipient for claims brought by any other entity based on 62 | infringement of intellectual property rights or otherwise. As a condition to 63 | exercising the rights and licenses granted hereunder, each Recipient hereby 64 | assumes sole responsibility to secure any other intellectual property rights 65 | needed, if any. For example, if a third party patent license is required to 66 | allow Recipient to distribute the Program, it is Recipient's responsibility 67 | to acquire that license before distributing the Program. 68 | 69 | d) Each Contributor represents that to its knowledge it has sufficient 70 | copyright rights in its Contribution, if any, to grant the copyright license 71 | set forth in this Agreement. 72 | 73 | 3. REQUIREMENTS 74 | 75 | A Contributor may choose to distribute the Program in object code form under 76 | its own license agreement, provided that: 77 | 78 | a) it complies with the terms and conditions of this Agreement; and 79 | 80 | b) its license agreement: 81 | 82 | i) effectively disclaims on behalf of all Contributors all warranties and 83 | conditions, express and implied, including warranties or conditions of title 84 | and non-infringement, and implied warranties or conditions of merchantability 85 | and fitness for a particular purpose; 86 | 87 | ii) effectively excludes on behalf of all Contributors all liability for 88 | damages, including direct, indirect, special, incidental and consequential 89 | damages, such as lost profits; 90 | 91 | iii) states that any provisions which differ from this Agreement are offered 92 | by that Contributor alone and not by any other party; and 93 | 94 | iv) states that source code for the Program is available from such 95 | Contributor, and informs licensees how to obtain it in a reasonable manner on 96 | or through a medium customarily used for software exchange. 97 | 98 | When the Program is made available in source code form: 99 | 100 | a) it must be made available under this Agreement; and 101 | 102 | b) a copy of this Agreement must be included with each copy of the Program. 103 | 104 | Contributors may not remove or alter any copyright notices contained within 105 | the Program. 106 | 107 | Each Contributor must identify itself as the originator of its Contribution, 108 | if any, in a manner that reasonably allows subsequent Recipients to identify 109 | the originator of the Contribution. 110 | 111 | 4. COMMERCIAL DISTRIBUTION 112 | 113 | Commercial distributors of software may accept certain responsibilities with 114 | respect to end users, business partners and the like. While this license is 115 | intended to facilitate the commercial use of the Program, the Contributor who 116 | includes the Program in a commercial product offering should do so in a 117 | manner which does not create potential liability for other Contributors. 118 | Therefore, if a Contributor includes the Program in a commercial product 119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend 120 | and indemnify every other Contributor ("Indemnified Contributor") against any 121 | losses, damages and costs (collectively "Losses") arising from claims, 122 | lawsuits and other legal actions brought by a third party against the 123 | Indemnified Contributor to the extent caused by the acts or omissions of such 124 | Commercial Contributor in connection with its distribution of the Program in 125 | a commercial product offering. The obligations in this section do not apply 126 | to any claims or Losses relating to any actual or alleged intellectual 127 | property infringement. In order to qualify, an Indemnified Contributor must: 128 | a) promptly notify the Commercial Contributor in writing of such claim, and 129 | b) allow the Commercial Contributor to control, and cooperate with the 130 | Commercial Contributor in, the defense and any related settlement 131 | negotiations. The Indemnified Contributor may participate in any such claim 132 | at its own expense. 133 | 134 | For example, a Contributor might include the Program in a commercial product 135 | offering, Product X. That Contributor is then a Commercial Contributor. If 136 | that Commercial Contributor then makes performance claims, or offers 137 | warranties related to Product X, those performance claims and warranties are 138 | such Commercial Contributor's responsibility alone. Under this section, the 139 | Commercial Contributor would have to defend claims against the other 140 | Contributors related to those performance claims and warranties, and if a 141 | court requires any other Contributor to pay any damages as a result, the 142 | Commercial Contributor must pay those damages. 143 | 144 | 5. NO WARRANTY 145 | 146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON 147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR 149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A 150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the 151 | appropriateness of using and distributing the Program and assumes all risks 152 | associated with its exercise of rights under this Agreement , including but 153 | not limited to the risks and costs of program errors, compliance with 154 | applicable laws, damage to or loss of data, programs or equipment, and 155 | unavailability or interruption of operations. 156 | 157 | 6. DISCLAIMER OF LIABILITY 158 | 159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 166 | OF SUCH DAMAGES. 167 | 168 | 7. GENERAL 169 | 170 | If any provision of this Agreement is invalid or unenforceable under 171 | applicable law, it shall not affect the validity or enforceability of the 172 | remainder of the terms of this Agreement, and without further action by the 173 | parties hereto, such provision shall be reformed to the minimum extent 174 | necessary to make such provision valid and enforceable. 175 | 176 | If Recipient institutes patent litigation against any entity (including a 177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 178 | (excluding combinations of the Program with other software or hardware) 179 | infringes such Recipient's patent(s), then such Recipient's rights granted 180 | under Section 2(b) shall terminate as of the date such litigation is filed. 181 | 182 | All Recipient's rights under this Agreement shall terminate if it fails to 183 | comply with any of the material terms or conditions of this Agreement and 184 | does not cure such failure in a reasonable period of time after becoming 185 | aware of such noncompliance. If all Recipient's rights under this Agreement 186 | terminate, Recipient agrees to cease use and distribution of the Program as 187 | soon as reasonably practicable. However, Recipient's obligations under this 188 | Agreement and any licenses granted by Recipient relating to the Program shall 189 | continue and survive. 190 | 191 | Everyone is permitted to copy and distribute copies of this Agreement, but in 192 | order to avoid inconsistency the Agreement is copyrighted and may only be 193 | modified in the following manner. The Agreement Steward reserves the right to 194 | publish new versions (including revisions) of this Agreement from time to 195 | time. No one other than the Agreement Steward has the right to modify this 196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 197 | Eclipse Foundation may assign the responsibility to serve as the Agreement 198 | Steward to a suitable separate entity. Each new version of the Agreement will 199 | be given a distinguishing version number. The Program (including 200 | Contributions) may always be distributed subject to the version of the 201 | Agreement under which it was received. In addition, after a new version of 202 | the Agreement is published, Contributor may elect to distribute the Program 203 | (including its Contributions) under the new version. Except as expressly 204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 205 | licenses to the intellectual property of any Contributor under this 206 | Agreement, whether expressly, by implication, estoppel or otherwise. All 207 | rights in the Program not expressly granted under this Agreement are 208 | reserved. 209 | 210 | This Agreement is governed by the laws of the State of New York and the 211 | intellectual property laws of the United States of America. No party to this 212 | Agreement will bring a legal action under this Agreement more than one year 213 | after the cause of action arose. Each party waives its rights to a jury trial 214 | in any resulting litigation. 215 | -------------------------------------------------------------------------------- /doc/lsp4clj.md: -------------------------------------------------------------------------------- 1 | # lsp4clj 2 | 3 | A [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) base for developing any LSP implementation in Clojure. 4 | 5 | [![Clojars Project](https://img.shields.io/clojars/v/com.github.clojure-lsp/lsp4clj.svg)](https://clojars.org/com.github.clojure-lsp/lsp4clj) 6 | 7 | lsp4clj reads and writes from io streams, parsing JSON-RPC according to the LSP spec. It provides tools to allow server implementors to receive, process, and respond to any of the methods defined in the LSP spec, and to send their own requests and notifications to clients. 8 | 9 | ## Usage 10 | 11 | ### Create a server 12 | 13 | To initialize a server that will read from stdin and write to stdout: 14 | 15 | ```clojure 16 | (lsp4clj.io-server/stdio-server) 17 | ``` 18 | 19 | The returned server will have a core.async `:log-ch`, from which you can read server logs (vectors beginning with a log level). 20 | 21 | ```clojure 22 | (async/go-loop [] 23 | (when-let [[level & args] (async/> params 46 | (handler/definition context) 47 | (conform-or-log ::coercer/location))) 48 | ``` 49 | 50 | The return value of requests will be converted to camelCase json and returned to the client. If the return value looks like `{:error ...}`, it is assumed to indicate an error response, and the `...` part will be set as the `error` of a [JSON-RPC error object](https://www.jsonrpc.org/specification#error_object). It is up to you to conform the `...` object (by giving it a `code`, `message`, and `data`.) Otherwise, the entire return value will be set as the `result` of a [JSON-RPC response object](https://www.jsonrpc.org/specification#response_object). (Message ids are handled internally by lsp4clj.) 51 | 52 | ### Async requests 53 | 54 | lsp4clj passes the language server the client's messages one at a time. It won't provide another message until it receives a result from the multimethods. Therefore, by default, requests and notifications are processed in series. 55 | 56 | However, it's possible to calculate requests in parallel (though not notifications). If the language server wants a request to be calculated in parallel with others, it should return a `java.util.concurrent.CompletableFuture`, possibly created with `promesa.core/future`, from `lsp4clj.server/receive-request`. lsp4clj will arrange for the result of this future to be returned to the client when it resolves. In the meantime, lsp4clj will continue passing the client's messages to the language server. The language server can control the number of simultaneous messages by setting the parallelism of the CompletableFutures' executor. 57 | 58 | ### Cancelled inbound requests 59 | 60 | Clients sometimes send `$/cancelRequest` notifications to indicate they're no longer interested in a request. If the request is being calculated in series, lsp4clj won't see the cancellation notification until after the response is already generated, so it's not possible to cancel requests that are processed in series. 61 | 62 | But clients can cancel requests that are processed in parallel. In these cases lsp4clj will cancel the future and return a message to the client acknowledging the cancellation. Because of the design of CompletableFuture, cancellation can mean one of two things. If the executor hasn't started the thread that is calculating the value of the future (perhaps because the executor's thread pool is full), it won't be started. But if there is already a thread calculating the value, the thread won't be interupted. See the documentation for CompletableFuture for an explanation of why this is so. 63 | 64 | Nevertheless, lsp4clj gives language servers a tool to abort cancelled requests. In the request's `context`, there will be a key `:lsp4clj.server/req-cancelled?` that can be dereffed to check if the request has been cancelled. If it has, then the language server can abort whatever it is doing. If it fails to abort, there are no consequences except that it will do more work than necessary. 65 | 66 | ```clojure 67 | (defmethod lsp4clj.server/receive-request "textDocument/semanticTokens/full" 68 | [_ {:keys [:lsp4clj.server/req-cancelled?] :as context} params] 69 | (promesa.core/future 70 | ;; client may cancel request while we are waiting for analysis 71 | (wait-for-analysis context) 72 | (when-not @req-cancelled? 73 | (handler/semantic-tokens-full context params)))) 74 | ``` 75 | 76 | ### Send messages 77 | 78 | Servers also send their own requests and notifications to a client. To send a notification, call `lsp4clj.server/send-notification`. 79 | 80 | ```clojure 81 | (->> {:message message 82 | :type type 83 | :extra extra} 84 | (conform-or-log ::coercer/show-message) 85 | (lsp4clj.server/send-notification server "window/showMessage")) 86 | ``` 87 | 88 | Sending a request is similar, with `lsp4clj.server/send-request`. This method returns a request object which may be dereffed to get the client's response. Most of the time you will want to call `lsp4clj.server/deref-or-cancel`, which will send a `$/cancelRequest` to the client if a timeout is reached before the client responds. 89 | 90 | ```clojure 91 | (let [request (->> {:edit edit} 92 | (conform-or-log ::coercer/workspace-edit-params) 93 | (lsp4clj.server/send-request server "workspace/applyEdit")) 94 | response (lsp4clj.server/deref-or-cancel request 10e3 ::timeout)] 95 | (if (= ::timeout response) 96 | (logger/error "No reponse from client after 10 seconds.") 97 | response)) 98 | ``` 99 | 100 | The request object presents the same interface as `future`. It responds to `realized?`, `future?`, `future-done?` and `future-cancelled?`. 101 | 102 | If you call `future-cancel` on the request object, the server will send the client a `$/cancelRequest`. `$/cancelRequest` is sent only once, although `lsp4clj.server/deref-or-cancel` or `future-cancel` can be called multiple times. After a request is cancelled, later invocations of `deref` will return `:lsp4clj.server/cancelled`. 103 | 104 | Alternatively, you can convert the request to a promesa promise, and handle it using that library's tools: 105 | 106 | ```clojure 107 | (let [request (lsp4clj.server/send-request server "..." params)] 108 | (-> request 109 | (promesa/promise) 110 | (promesa/then (fn [response] {:result :client-success 111 | :value 1 112 | :resp response})) 113 | (promesa/catch (fn [ex-response] {:result :client-error 114 | :value 10 115 | :resp (ex-data ex-response)})) 116 | (promesa/timeout 10000 {:result :timeout 117 | :value 100}) 118 | (promesa/then #(update % :value inc)))) 119 | ``` 120 | 121 | In this case `(promesa/cancel! request)` will send `$/cancelRequest`. 122 | 123 | Response promises are completed on Promesa's `:default` executor. You 124 | can specify your own executor by passing the `:response-executor` option 125 | when creating your server instance. 126 | 127 | ### Start and stop a server 128 | 129 | The last step is to start the server you created earlier. Use `lsp4clj.server/start`. This method accepts two arguments, the server and a "context". 130 | 131 | The context should be `associative?`. Whatever you provide in the context will be passed as the second argument to the notification and request `defmethod`s you defined earlier. This is a convenient way to make components of your system available to those methods without definining global constants. Often the context will include the server itself so that you can initiate outbound requests and notifications in reaction to inbound messages. lsp4clj reserves the right to add its own data to the context, using keys namespaced with `:lsp4clj.server/...`. 132 | 133 | ```clojure 134 | (lsp4clj.server/start server {:custom-settings custom-settings, :logger logger}) 135 | ``` 136 | 137 | The return of `start` is a promise that will resolve to `:done` when the server shuts down, which can happen in a few ways. 138 | 139 | First, if the server's input is closed, it will shut down too. Second, if you call `lsp4clj.server/shutdown` on it, it will shut down. 140 | 141 | When a server shuts down it stops reading input, finishes processing the messages it has in flight, and then closes is output. Finally it closes its `:log-ch` and `:trace-ch`. As such, it should probably not be shut down until the LSP `exit` notification (as opposed to the `shutdown` request) to ensure all messages are received. `lsp4clj.server/shutdown` will not return until all messages have been processed, or until 10 seconds have passed, whichever happens sooner. It will return `:done` in the first case and `:timeout` in the second. 142 | 143 | ## Other types of servers 144 | 145 | So far the examples have focused on `lsp4clj.io-server/stdio-server`, because many clients communicate over stdio by default. The client opens a subprocess for the LSP server, then starts sending messages to the process via the process's stdin and reading messages from it on its stdout. 146 | 147 | Many clients can also communicate over a socket. Typically the client starts a socket server, then passes a command-line argument to the LSP subprocess, telling it what port to connect to. The server is expected to connect to that port and use it to send and receive messages. In lsp4clj, that can be accomplished with `lsp4clj.io-server/server`: 148 | 149 | ```clojure 150 | (defn socket-server [{:keys [host port]}] 151 | {:pre [(or (nil? host) (string? host)) 152 | (and (int? port) (<= 1024 port 65535))]} 153 | (let [addr (java.net.InetAddress/getByName host) ;; nil host == loopback 154 | sock (java.net.Socket. ^java.net.InetAddress addr ^int port)] 155 | (lsp4clj.io-server/server {:in sock 156 | :out sock}))) 157 | ``` 158 | 159 | `lsp4clj.io-server/server` accepts a pair of options `:in` and `:out`. These will be coerced to a `java.io.InputStream` and `java.io.OutputStream` via `clojure.java.io/input-stream` and `clojure.java.io/output-stream`, respectively. The example above works because a `java.net.Socket` can be coerced to both an input and output stream via this mechanism. 160 | 161 | A similar approach can be used to connect over pipes. 162 | 163 | ## Development details 164 | 165 | ### Tracing 166 | 167 | As you are implementing, you may want to trace incoming and outgoing messages. Initialize the server with `:trace-level "verbose"` and then read traces (two element vectors, beginning with the log level `:debug` and ending with a string, the trace itself) off its `:trace-ch`. 168 | 169 | ```clojure 170 | (let [server (lsp4clj.io-server/stdio-server {:trace-level "verbose"})] 171 | (async/go-loop [] 172 | (when-let [[level trace] (async/>+MCPServer: initialize 270 | MCPServer-->>-Client: initialize response (capabilities) 271 | Client->>MCPServer: notifications/initialized 272 | 273 | Note over Client,MCPServer: Discovery Phase 274 | Client->>+MCPServer: tools/list 275 | MCPServer-->>-Client: List of available tools 276 | 277 | Client->>+MCPServer: resources/list 278 | MCPServer-->>-Client: List of available resources 279 | 280 | Client->>+MCPServer: prompts/list 281 | MCPServer-->>-Client: List of available prompts 282 | 283 | Note over Client,MCPServer: Tool Interaction 284 | Client->>+MCPServer: tools/call (name, arguments) 285 | MCPServer->>+Tool: handler(arguments) 286 | Tool-->>-MCPServer: result 287 | MCPServer-->>-Client: Tool response 288 | 289 | Note over Client,MCPServer: Resource Interaction 290 | Client->>+MCPServer: resources/read (uri) 291 | MCPServer->>+Resource: handler(uri) 292 | Resource-->>-MCPServer: contents 293 | MCPServer-->>-Client: Resource contents 294 | 295 | Note over Client,MCPServer: Prompt Interaction 296 | Client->>+MCPServer: prompts/get (name, arguments) 297 | MCPServer->>+Prompt: handler(arguments) 298 | Prompt-->>-MCPServer: messages 299 | MCPServer-->>-Client: Prompt messages 300 | 301 | Note over Client,MCPServer: Optional Subscription 302 | Client->>+MCPServer: resources/subscribe (uri) 303 | MCPServer-->>-Client: Empty response 304 | MCPServer-->>Client: notifications/resources/updated 305 | 306 | Note over Client,MCPServer: Health Check 307 | Client->>+MCPServer: ping 308 | MCPServer-->>-Client: pong 309 | ``` 310 | ## Pending Work 311 | 312 | You can help dear reader! Head over to the [todo.org file](todo.org) 313 | to see the list of pending changes, arranged roughly in the order I 314 | plan to tackle them. 315 | 316 | ## Development of the SDK 317 | 318 | The `clojure-sdk` is a standard `deps-new` project, so you should 319 | expect all the `deps-new` commands to work as expected. Even so: 320 | 321 | Run the project's tests: 322 | 323 | $ make test ## or clojure -T:build test 324 | 325 | Run the project's CI pipeline and build a JAR: 326 | 327 | $ make build ## or clojure -T:build ci 328 | 329 | This will produce an updated `pom.xml` file with synchronized 330 | dependencies inside the `META-INF` directory inside `target/classes` 331 | and the JAR in `target`. You can update the version (and SCM tag) 332 | information in generated `pom.xml` by updating `build.clj`. 333 | 334 | Install it locally: 335 | 336 | $ make install ## or clojure -T:build install 337 | 338 | Deploy it to Clojars -- needs `CLOJARS_USERNAME` and 339 | `CLOJARS_PASSWORD` environment variables (requires the `ci` task be 340 | run first): 341 | 342 | $ make deploy ## or clojure -T:build deploy 343 | 344 | Your library will be deployed to io.modelcontext/clojure-sdk on 345 | clojars.org by default. 346 | 347 | ## Inspiration 348 | 349 | This SDK is built on top of 350 | [lsp4clj](https://github.com/clojure-lsp/lsp4clj), which solves the 351 | hard part of handling all the edge-cases of a JSON-RPC based server. I 352 | built this layer by hand and discovered all the edge-cases before 353 | realising that `lsp4clj` was the smarter approach. The code is super 354 | well written and easy to modify for my requirements. 355 | 356 | ## License 357 | 358 | Copyright © 2025 Unravel.tech 359 | 360 | Distributed under the MIT License 361 | -------------------------------------------------------------------------------- /src/io/modelcontext/clojure_sdk/server.clj: -------------------------------------------------------------------------------- 1 | (ns io.modelcontext.clojure-sdk.server 2 | (:require [clojure.core.async :as async] 3 | [io.modelcontext.clojure-sdk.mcp.errors :as mcp.errors] 4 | [io.modelcontext.clojure-sdk.specs :as specs] 5 | [lsp4clj.coercer :as coercer] 6 | [lsp4clj.server :as lsp.server] 7 | [me.vedang.logger.interface :as log])) 8 | 9 | ;;; Helpers 10 | ;; Logging and Spec Checking 11 | (defmacro conform-or-log 12 | "Provides log function for conformation, while preserving line numbers." 13 | [spec value] 14 | (let [fmeta (assoc (meta &form) 15 | :file *file* 16 | :ns-str (str *ns*))] 17 | `(coercer/conform-or-log 18 | (fn [& args#] 19 | (cond (= 2 (count args#)) (log/error :msg (first args#) 20 | :explain (second args#) 21 | :meta ~fmeta) 22 | (= 4 (count args#)) (log/error :ex (first args#) 23 | :msg (second args#) 24 | :spec ~spec 25 | :value ~value 26 | :meta ~fmeta) 27 | :else (throw (ex-info "Unknown Conform Error" :args args#)))) 28 | ~spec 29 | ~value))) 30 | 31 | ;;; Helper functions for handling various requests 32 | 33 | (defn store-client-info! 34 | [context client-info client-capabilities] 35 | (let [client-id (random-uuid)] 36 | (swap! (:connected-clients context) assoc 37 | client-id 38 | {:client-info client-info, :capabilities client-capabilities}) 39 | client-id)) 40 | 41 | (defn- supported-protocol-version 42 | "Return the version of MCP protocol as part of connection initialization." 43 | [version] 44 | ;; [ref: version_negotiation] 45 | (if ((set specs/supported-protocol-versions) version) 46 | version 47 | (first specs/supported-protocol-versions))) 48 | 49 | (defn- handle-initialize 50 | [context params] 51 | (let [client-info (:clientInfo params) 52 | client-capabilities (:capabilities params) 53 | server-info (:server-info context) 54 | server-capabilities @(:capabilities context) 55 | client-id (store-client-info! context client-info client-capabilities)] 56 | (log/trace :fn :handle-initialize 57 | :msg "[Initialize] Client connected!" 58 | :client-info client-info 59 | :client-id client-id) 60 | {:protocolVersion (supported-protocol-version (:protocolVersion params)), 61 | :capabilities server-capabilities, 62 | :serverInfo server-info})) 63 | 64 | (defn- handle-ping [_context _params] (log/trace :fn :handle-ping) "pong") 65 | 66 | (defn- handle-list-tools 67 | [context _params] 68 | (log/trace :fn :handle-list-tools) 69 | {:tools (mapv :tool (vals @(:tools context)))}) 70 | 71 | (defn coerce-tool-response 72 | "Coerces a tool response into the expected format. 73 | If the response is not sequential, wraps it in a vector. 74 | If the tool has an outputSchema, adds structuredContent." 75 | [tool response] 76 | (let [response (if (sequential? response) (vec response) [response]) 77 | base-map {:content response}] 78 | ;; @TODO: [ref: structured-content-should-match-output-schema-exactly] 79 | (cond-> base-map (:outputSchema tool) (assoc :structuredContent response)))) 80 | 81 | (defn- handle-call-tool 82 | [context params] 83 | (log/trace :fn :handle-call-tool 84 | :tool (:name params) 85 | :args (:arguments params)) 86 | (let [tools @(:tools context) 87 | tool-name (:name params) 88 | arguments (:arguments params)] 89 | (if-let [{:keys [tool handler]} (get tools tool-name)] 90 | (try (coerce-tool-response tool (handler arguments)) 91 | (catch Exception e 92 | {:content [{:type "text", :text (str "Error: " (.getMessage e))}], 93 | :isError true})) 94 | (do 95 | (log/debug :fn :handle-call-tool :tool tool-name :error :tool-not-found) 96 | {:error (mcp.errors/body :tool-not-found {:tool-name tool-name})})))) 97 | 98 | (defn- handle-list-resources 99 | [context _params] 100 | (log/trace :fn :handle-list-resources) 101 | {:resources (mapv :resource (vals @(:resources context)))}) 102 | 103 | (defn- handle-read-resource 104 | [context params] 105 | (log/trace :fn :handle-read-resource :resource (:uri params)) 106 | (let [resources @(:resources context) 107 | uri (:uri params)] 108 | (if-let [{:keys [handler]} (get resources uri)] 109 | {:contents [(handler uri)]} 110 | (do (log/debug :fn :handle-read-resource 111 | :resource uri 112 | :error :resource-not-found) 113 | {:error (mcp.errors/body :resource-not-found {:uri uri})})))) 114 | 115 | (defn- handle-list-prompts 116 | [context _params] 117 | (log/trace :fn :handle-list-prompts) 118 | {:prompts (mapv :prompt (vals @(:prompts context)))}) 119 | 120 | (defn- handle-get-prompt 121 | [context params] 122 | (log/trace :fn :handle-get-prompt 123 | :prompt (:name params) 124 | :args (:arguments params)) 125 | (let [prompts @(:prompts context) 126 | prompt-name (:name params) 127 | arguments (:arguments params)] 128 | (if-let [{:keys [handler]} (get prompts prompt-name)] 129 | (handler arguments) 130 | (do (log/debug :fn :handle-get-prompt 131 | :prompt prompt-name 132 | :error :prompt-not-found) 133 | {:error (mcp.errors/body :prompt-not-found 134 | {:prompt-name prompt-name})})))) 135 | 136 | ;;; Requests and Notifications 137 | 138 | ;; [ref: initialize_request] 139 | (defmethod lsp.server/receive-request "initialize" 140 | [_ context params] 141 | (log/trace :fn :receive-request :method "initialize" :params params) 142 | ;; [tag: log_bad_input_params] 143 | ;; 144 | ;; If the input is non-conformant, we should log it. But we shouldn't 145 | ;; take any other action. The principle we want to follow is Postel's 146 | ;; law: https://en.wikipedia.org/wiki/Robustness_principle 147 | (conform-or-log ::specs/initialize-request params) 148 | (->> params 149 | (handle-initialize context) 150 | (conform-or-log ::specs/initialize-response))) 151 | 152 | ;; [ref: initialized_notification] 153 | (defmethod lsp.server/receive-notification "notifications/initialized" 154 | [_ _ params] 155 | (conform-or-log ::specs/initialized-notification params)) 156 | 157 | ;; [ref: ping_request] 158 | (defmethod lsp.server/receive-request "ping" 159 | [_ context params] 160 | (log/trace :fn :receive-request :method "ping" :params params) 161 | ;; [ref: log_bad_input_params] 162 | (conform-or-log ::specs/ping-request params) 163 | (->> params 164 | (handle-ping context))) 165 | 166 | ;; [ref: list_tools_request] 167 | (defmethod lsp.server/receive-request "tools/list" 168 | [_ context params] 169 | (log/trace :fn :receive-request :method "tools/list" :params params) 170 | ;; [ref: log_bad_input_params] 171 | (conform-or-log ::specs/list-tools-request params) 172 | (->> params 173 | (handle-list-tools context) 174 | (conform-or-log ::specs/list-tools-response))) 175 | 176 | ;; [ref: call_tool_request] 177 | (defmethod lsp.server/receive-request "tools/call" 178 | [_ context params] 179 | (log/trace :fn :receive-request :method "tools/call" :params params) 180 | ;; [ref: log_bad_input_params] 181 | (conform-or-log ::specs/call-tool-request params) 182 | (->> params 183 | (handle-call-tool context) 184 | (conform-or-log ::specs/call-tool-response))) 185 | 186 | ;; [ref: list_resources_request] 187 | (defmethod lsp.server/receive-request "resources/list" 188 | [_ context params] 189 | (log/trace :fn :receive-request :method "resources/list" :params params) 190 | ;; [ref: log_bad_input_params] 191 | (conform-or-log ::specs/list-resources-request params) 192 | (->> params 193 | (handle-list-resources context) 194 | (conform-or-log ::specs/list-resources-response))) 195 | 196 | ;; [ref: read_resource_request] 197 | (defmethod lsp.server/receive-request "resources/read" 198 | [_ context params] 199 | (log/trace :fn :receive-request :method "resources/read" :params params) 200 | ;; [ref: log_bad_input_params] 201 | (conform-or-log ::specs/read-resource-request params) 202 | (->> params 203 | (handle-read-resource context) 204 | (conform-or-log ::specs/read-resource-response))) 205 | 206 | ;; [ref: list_prompts_request] 207 | (defmethod lsp.server/receive-request "prompts/list" 208 | [_ context params] 209 | (log/trace :fn :receive-request :method "prompts/list" :params params) 210 | ;; [ref: log_bad_input_params] 211 | (conform-or-log ::specs/list-prompts-request params) 212 | (->> params 213 | (handle-list-prompts context) 214 | (conform-or-log ::specs/list-prompts-response))) 215 | 216 | ;; [ref: get_prompt_request] 217 | (defmethod lsp.server/receive-request "prompts/get" 218 | [_ context params] 219 | (log/trace :fn :receive-request :method "prompts/get" :params params) 220 | ;; [ref: log_bad_input_params] 221 | (conform-or-log ::specs/get-prompt-request params) 222 | (->> params 223 | (handle-get-prompt context) 224 | (conform-or-log ::specs/get-prompt-response))) 225 | 226 | ;;; @TODO: Requests to Implement 227 | 228 | ;; [ref: list_resource_templates_request] 229 | (defmethod lsp.server/receive-request "resources/templates/list" 230 | [_ _context params] 231 | (log/trace :fn :receive-request 232 | :method "resources/templates/list" 233 | :params params) 234 | ;; [ref: log_bad_input_params] 235 | (conform-or-log ::specs/list-resource-templates-request params) 236 | (identity ::specs/list-resource-templates-response) 237 | ::lsp.server/method-not-found) 238 | 239 | ;; [ref: resource_subscribe_unsubscribe_request] 240 | (defmethod lsp.server/receive-request "resources/subscribe" 241 | [_ _context params] 242 | (log/trace :fn :receive-request :method "resources/subscribe" :params params) 243 | ;; [ref: log_bad_input_params] 244 | (conform-or-log ::specs/resource-subscribe-unsubscribe-request params) 245 | ::lsp.server/method-not-found) 246 | 247 | ;; [ref: resource_subscribe_unsubscribe_request] 248 | (defmethod lsp.server/receive-request "resources/unsubscribe" 249 | [_ _context params] 250 | (log/trace :fn :receive-request 251 | :method "resources/unsubscribe" 252 | :params params) 253 | ;; [ref: log_bad_input_params] 254 | (conform-or-log ::specs/resource-subscribe-unsubscribe-request params) 255 | ::lsp.server/method-not-found) 256 | 257 | ;; [ref: set_logging_level_request] 258 | (defmethod lsp.server/receive-request "logging/setLevel" 259 | [_ _context params] 260 | (log/trace :fn :receive-request :method "logging/setLevel" :params params) 261 | ;; [ref: log_bad_input_params] 262 | (conform-or-log ::specs/set-logging-level-request params) 263 | ::lsp.server/method-not-found) 264 | 265 | ;; [ref: complete_request] 266 | (defmethod lsp.server/receive-request "completion/complete" 267 | [_ _context params] 268 | (log/trace :fn :receive-request :method "completion/complete" :params params) 269 | ;; [ref: log_bad_input_params] 270 | (conform-or-log ::specs/complete-request params) 271 | (identity ::specs/complete-response) 272 | ::lsp.server/method-not-found) 273 | 274 | ;;; @TODO: Notifications to Implement 275 | 276 | ;; [ref: cancelled_notification] 277 | (defmethod lsp.server/receive-notification "notifications/cancelled" 278 | [_method _context _params] 279 | (identity ::specs/cancelled-notification) 280 | ::lsp.server/method-not-found) 281 | 282 | ;; @TODO: Implement send-notification "notifications/cancelled" when request is 283 | ;; cancelled 284 | 285 | ;; [ref: progress_notification] 286 | (defmethod lsp.server/receive-notification "notifications/progress" 287 | [_method _context _params] 288 | (identity ::specs/progress-notification) 289 | ::lsp.server/method-not-found) 290 | 291 | ;; @TODO: Implement send-notification "notifications/progress" for long-lived 292 | ;; requests 293 | 294 | ;; @TODO: Implement [ref: resource_list_changed_notification] for when list of 295 | ;; resources available to the client changes. 296 | 297 | ;; @TODO: Implement [ref: resource_updated_notification] for when a resource is 298 | ;; updated at the server 299 | 300 | ;; @TODO: Implement [ref: prompt_list_changed_notification] for when list of 301 | ;; prompts available to the client changes. 302 | 303 | ;; @TODO: Implement [ref: tool_list_changed_notification] for when list of 304 | ;; tools available to the client changes. 305 | 306 | ;; @TODO: Implement [ref: logging_message_notification] for when server wants 307 | ;; to send a logging message to the client. 308 | 309 | ;;; Server Spec 310 | 311 | (defn validate-spec! 312 | [server-spec] 313 | (when-not (specs/valid-server-spec? server-spec) 314 | (let [msg "Invalid server-spec definition"] 315 | (log/debug :msg msg :spec server-spec) 316 | (throw (ex-info msg (specs/explain-server-spec server-spec))))) 317 | server-spec) 318 | 319 | (defn register-tool! 320 | [context tool handler] 321 | (swap! (:tools context) assoc (:name tool) {:tool tool, :handler handler})) 322 | 323 | (defn register-resource! 324 | [context resource handler] 325 | (swap! (:resources context) assoc 326 | (:uri resource) 327 | {:resource resource, :handler handler})) 328 | 329 | (defn register-prompt! 330 | [context prompt handler] 331 | (swap! (:prompts context) assoc 332 | (:name prompt) 333 | {:prompt prompt, :handler handler})) 334 | 335 | (defn- create-empty-context 336 | [name version] 337 | (log/trace :fn :create-empty-context) 338 | ;; [tag: context_must_be_a_map] 339 | ;; 340 | ;; Since so much of the state is "global" in nature, it's tempting to 341 | ;; just make the entire context global instead of defining atoms at each 342 | ;; key. However, do not do this! 343 | ;; 344 | ;; This context is passed to lsp4j, which expects the data-structure to 345 | ;; be `associative?` in nature and uses it further for it's own temporary 346 | ;; state. 347 | {:server-info {:name name, :version version}, 348 | :tools (atom {}), 349 | :resources (atom {}), 350 | :prompts (atom {}), 351 | :protocol (atom nil), 352 | :capabilities (atom {:tools {}, :resources {}, :prompts {}}), 353 | :connected-clients (atom {})}) 354 | 355 | (defn create-context! 356 | "Create and configure an MCP server from a configuration map. 357 | Config map should have the shape: 358 | {:name \"server-name\" 359 | :version \"1.0.0\" 360 | :tools [{:name \"tool-name\" 361 | :description \"Tool description\" 362 | :inputSchema {...} 363 | :handler (fn [args] ...)}] 364 | :prompts [{:name \"prompt-name\" 365 | :description \"Prompt description\" 366 | :handler (fn [args] ...)}] 367 | :resources [{:uri \"resource-uri\" 368 | :type \"text\" 369 | :handler (fn [uri] ...)}]}" 370 | [{:keys [name version tools prompts resources], :as spec}] 371 | (validate-spec! spec) 372 | (log/with-context {:action :create-context!} 373 | (let [context (create-empty-context name version)] 374 | (when (> (count tools) 0) 375 | (log/debug :num-tools (count tools) 376 | :msg "Registering tools" 377 | :server-info {:name name, :version version})) 378 | (doseq [tool tools] 379 | (register-tool! context (dissoc tool :handler) (:handler tool))) 380 | (when (> (count resources) 0) 381 | (log/debug :num-resources (count resources) 382 | :msg "Registering resources" 383 | :server-info {:name name, :version version})) 384 | (doseq [resource resources] 385 | (register-resource! context 386 | (dissoc resource :handler) 387 | (:handler resource))) 388 | (when (> (count prompts) 0) 389 | (log/debug :num-prompts (count prompts) 390 | :msg "Registering prompts" 391 | :server-info {:name name, :version version})) 392 | (doseq [prompt prompts] 393 | (register-prompt! context (dissoc prompt :handler) (:handler prompt))) 394 | context))) 395 | 396 | (defn start! 397 | [server context] 398 | (log/info :msg "[SERVER] Starting server...") 399 | (lsp.server/start server context)) 400 | 401 | (defn chan-server 402 | [] 403 | (let [input-ch (async/chan 3) 404 | output-ch (async/chan 3)] 405 | (lsp.server/chan-server {:output-ch output-ch, :input-ch input-ch}))) 406 | -------------------------------------------------------------------------------- /test/io/modelcontext/clojure_sdk/server_test.clj: -------------------------------------------------------------------------------- 1 | (ns io.modelcontext.clojure-sdk.server-test 2 | (:require [clojure.core.async :as async] 3 | [clojure.test :refer [deftest is testing]] 4 | [io.modelcontext.clojure-sdk.mcp.errors :as mcp.errors] 5 | [io.modelcontext.clojure-sdk.server :as server] 6 | [io.modelcontext.clojure-sdk.specs :as specs] 7 | [io.modelcontext.clojure-sdk.test-helper :as h] 8 | [lsp4clj.lsp.requests :as lsp.requests] 9 | [lsp4clj.lsp.responses :as lsp.responses] 10 | [lsp4clj.server :as lsp.server])) 11 | 12 | ;;; Tools 13 | (def tool-greet 14 | {:name "greet", 15 | :description "Greet someone", 16 | :inputSchema {:type "object", :properties {"name" {:type "string"}}}, 17 | :handler (fn [{:keys [name]}] 18 | {:type "text", :text (str "Hello, " name "!")})}) 19 | 20 | (def tool-echo 21 | {:name "echo", 22 | :description "Echo input", 23 | :inputSchema {:type "object", 24 | :properties {"message" {:type "string"}}, 25 | :required ["message"]}, 26 | :handler (fn [{:keys [message]}] {:type "text", :text message})}) 27 | 28 | 29 | ;;; Prompts 30 | (def prompt-analyze-code 31 | {:name "analyze-code", 32 | :description "Analyze code for potential improvements", 33 | :arguments 34 | [{:name "language", :description "Programming language", :required true} 35 | {:name "code", :description "The code to analyze", :required true}], 36 | :handler (fn analyze-code [args] 37 | {:messages [{:role "assistant", 38 | :content 39 | {:type "text", 40 | :text (str "Analysis of " 41 | (:language args) 42 | " code:\n" 43 | "Here are potential improvements for:\n" 44 | (:code args))}}]})}) 45 | 46 | (def prompt-poem-about-code 47 | {:name "poem-about-code", 48 | :description "Write a poem describing what this code does", 49 | :arguments 50 | [{:name "poetry_type", 51 | :description 52 | "The style in which to write the poetry: sonnet, limerick, haiku", 53 | :required true} 54 | {:name "code", 55 | :description "The code to write poetry about", 56 | :required true}], 57 | :handler (fn [args] 58 | {:messages [{:role "assistant", 59 | :content {:type "text", 60 | :text (str "Write a " (:poetry_type args) 61 | " Poem about:\n" (:code 62 | args))}}]})}) 63 | 64 | ;;; Resources 65 | (def resource-test-json 66 | {:description "Test JSON data", 67 | :mimeType "application/json", 68 | :name "Test Data", 69 | :uri "file:///data.json", 70 | :handler 71 | (fn read-resource [uri] 72 | {:uri uri, :mimeType "application/json", :blob "Hello from Test Data"})}) 73 | 74 | (def resource-test-file 75 | {:description "A test file", 76 | :mimeType "text/plain", 77 | :name "Test File", 78 | :uri "file:///test.txt", 79 | :handler 80 | (fn read-resource [uri] 81 | {:uri uri, :mimeType "text/plain", :text "Hello from Test File"})}) 82 | 83 | ;;; Example Server Spec 84 | (def example-server-spec 85 | {:name "test-server", 86 | :version "1.0.0", 87 | :tools [tool-greet], 88 | :prompts [], 89 | :resources [resource-test-file]}) 90 | 91 | ;;; Tests 92 | 93 | (deftest server-basic-functionality 94 | (testing "Server creation and tool registration" 95 | (let [context (server/create-context! {:name "test-server", 96 | :version "1.0.0", 97 | :tools [tool-greet], 98 | :prompts [], 99 | :resources []})] 100 | (testing "Tool listing" 101 | (let [tools (-> @(:tools context) 102 | vals 103 | first 104 | :tool)] 105 | (is (= "greet" (:name tools))) 106 | (is (= "Greet someone" (:description tools))))) 107 | (testing "Tool execution" 108 | (let [handler (-> @(:tools context) 109 | (get "greet") 110 | :handler) 111 | result (handler {:name "World"})] 112 | (is (= {:type "text", :text "Hello, World!"} result)))))) 113 | (testing "Server creation with basic configuration" 114 | (let [context (server/create-context! {:name "test-server", 115 | :version "1.0.0", 116 | :tools [], 117 | :prompts [], 118 | :resources []})] 119 | (is (= "test-server" (get-in context [:server-info :name]))) 120 | (is (= "1.0.0" (get-in context [:server-info :version]))) 121 | (is (= {} @(:tools context))) 122 | (is (= {} @(:resources context))) 123 | (is (= {} @(:prompts context)))))) 124 | 125 | (deftest tool-registration 126 | (testing "Tool registration and validation" 127 | (let [context (server/create-context! 128 | {:name "test-server", :version "1.0.0", :tools []})] 129 | (server/register-tool! 130 | context 131 | {:name "test-tool", 132 | :description "A test tool", 133 | :inputSchema {:type "object", :properties {"arg" {:type "string"}}}} 134 | (fn [_] {:type "text", :text "success"})) 135 | (is (= 1 (count @(:tools context)))) 136 | (is (get @(:tools context) "test-tool"))))) 137 | 138 | (deftest initialization 139 | (testing "Connection initialization through initialize, 2024-11-05 version" 140 | (let [context (server/create-context! 141 | {:name "test-server", :version "1.0.0", :tools [tool-echo]}) 142 | server (server/chan-server) 143 | _join (server/start! server context)] 144 | (testing "Client initialization" 145 | (async/put! (:input-ch server) 146 | (lsp.requests/request 147 | 1 148 | "initialize" 149 | {:protocolVersion "2024-11-05", 150 | :capabilities {:roots {:listChanged true}, :sampling {}}, 151 | :clientInfo {:name "ExampleClient", :version "1.0.0"}})) 152 | (is (= (lsp.responses/response 153 | 1 154 | {:protocolVersion "2024-11-05", 155 | :capabilities {:tools {}, :resources {}, :prompts {}}, 156 | :serverInfo {:name "test-server", :version "1.0.0"}}) 157 | (h/take-or-timeout (:output-ch server) 200)))) 158 | (lsp.server/shutdown server))) 159 | (testing "Connection initialization through initialize, 2025-03-26 version" 160 | (let [context (server/create-context! 161 | {:name "test-server", :version "1.0.0", :tools [tool-echo]}) 162 | server (server/chan-server) 163 | _join (server/start! server context)] 164 | (testing "Client initialization" 165 | (async/put! (:input-ch server) 166 | (lsp.requests/request 167 | 1 168 | "initialize" 169 | {:protocolVersion "2025-03-26", 170 | :capabilities {:roots {:listChanged true}, :sampling {}}, 171 | :clientInfo {:name "ExampleClient", :version "1.0.0"}})) 172 | (is (= (lsp.responses/response 173 | 1 174 | {:protocolVersion "2025-03-26", 175 | :capabilities {:tools {}, :resources {}, :prompts {}}, 176 | :serverInfo {:name "test-server", :version "1.0.0"}}) 177 | (h/take-or-timeout (:output-ch server) 200)))) 178 | (lsp.server/shutdown server))) 179 | (testing "Connection initialization through initialize, unknown version" 180 | (let [context (server/create-context! 181 | {:name "test-server", :version "1.0.0", :tools [tool-echo]}) 182 | server (server/chan-server) 183 | _join (server/start! server context)] 184 | (testing "Client initialization" 185 | (async/put! (:input-ch server) 186 | (lsp.requests/request 187 | 1 188 | "initialize" 189 | {:protocolVersion "DRAFT-2025-v2", 190 | :capabilities {:roots {:listChanged true}, :sampling {}}, 191 | :clientInfo {:name "ExampleClient", :version "1.0.0"}})) 192 | (is (= (lsp.responses/response 193 | 1 194 | {:protocolVersion "2025-03-26", 195 | :capabilities {:tools {}, :resources {}, :prompts {}}, 196 | :serverInfo {:name "test-server", :version "1.0.0"}}) 197 | (h/take-or-timeout (:output-ch server) 200)))) 198 | (lsp.server/shutdown server)))) 199 | 200 | (deftest tool-execution 201 | (testing "Tool execution through protocol" 202 | (let [context (server/create-context! 203 | {:name "test-server", :version "1.0.0", :tools [tool-echo]}) 204 | server (server/chan-server) 205 | _join (server/start! server context)] 206 | (testing "Tool list request" 207 | (async/put! (:input-ch server) (lsp.requests/request 1 "tools/list" {})) 208 | (is (= (lsp.responses/response 1 209 | {:tools [{:name "echo", 210 | :description "Echo input", 211 | :inputSchema 212 | {:type "object", 213 | :properties 214 | {"message" {:type "string"}}, 215 | :required ["message"]}}]}) 216 | (h/assert-take (:output-ch server))))) 217 | (testing "Tool execution request" 218 | (async/put! (:input-ch server) 219 | (lsp.requests/request 2 220 | "tools/call" 221 | {:name "echo", 222 | :arguments {:message "test"}})) 223 | (is (= (lsp.responses/response 2 224 | {:content [{:type "text", 225 | :text "test"}]}) 226 | (h/assert-take (:output-ch server))))) 227 | (testing "Invalid tool request" 228 | (async/put! (:input-ch server) 229 | (lsp.requests/request 3 "tools/call" {:name "invalid"})) 230 | (is (= (lsp.responses/error (lsp.responses/response 3) 231 | (mcp.errors/body :tool-not-found 232 | {:tool-name "invalid"})) 233 | (h/assert-take (:output-ch server))))) 234 | (lsp.server/shutdown server)))) 235 | 236 | (deftest prompt-listing 237 | (testing "Listing available prompts" 238 | (let [context (server/create-context! {:name "test-server", 239 | :version "1.0.0", 240 | :tools [], 241 | :prompts [prompt-analyze-code 242 | prompt-poem-about-code]}) 243 | server (server/chan-server) 244 | _join (server/start! server context)] 245 | (testing "Prompts list request" 246 | (async/put! (:input-ch server) 247 | (lsp.requests/request 1 "prompts/list" {})) 248 | (let [response (h/assert-take (:output-ch server))] 249 | (is (= 2 (count (:prompts (:result response))))) 250 | (let [analyze (first (:prompts (:result response))) 251 | poem (second (:prompts (:result response)))] 252 | (is (= "analyze-code" (:name analyze))) 253 | (is (= "Analyze code for potential improvements" 254 | (:description analyze))) 255 | (is (= [{:name "language", 256 | :description "Programming language", 257 | :required true} 258 | {:name "code", 259 | :description "The code to analyze", 260 | :required true}] 261 | (:arguments analyze))) 262 | (is (= "poem-about-code" (:name poem))) 263 | (is (= "Write a poem describing what this code does" 264 | (:description poem))) 265 | (is 266 | (= 267 | [{:name "poetry_type", 268 | :description 269 | "The style in which to write the poetry: sonnet, limerick, haiku", 270 | :required true} 271 | {:name "code", 272 | :description "The code to write poetry about", 273 | :required true}] 274 | (:arguments poem)))))) 275 | (lsp.server/shutdown server)))) 276 | 277 | (deftest prompt-getting 278 | (testing "Getting specific prompts" 279 | (let [server (server/chan-server) 280 | context (server/create-context! {:name "test-server", 281 | :version "1.0.0", 282 | :tools [], 283 | :prompts [prompt-analyze-code 284 | prompt-poem-about-code]}) 285 | _join (server/start! server context)] 286 | (testing "Get analyze-code prompt" 287 | (async/put! (:input-ch server) 288 | (lsp.requests/request 1 289 | "prompts/get" 290 | {:name "analyze-code", 291 | :arguments {:language "Clojure", 292 | :code "(defn foo [])"}})) 293 | (let [response (h/assert-take (:output-ch server)) 294 | result (:result response)] 295 | (is (= 1 (count (:messages result)))) 296 | (is 297 | (= 298 | "Analysis of Clojure code:\nHere are potential improvements for:\n(defn foo [])" 299 | (-> result 300 | :messages 301 | first 302 | :content 303 | :text))))) 304 | (testing "Get poem-about-code prompt" 305 | (async/put! (:input-ch server) 306 | (lsp.requests/request 2 307 | "prompts/get" 308 | {:name "poem-about-code", 309 | :arguments {:poetry_type "haiku", 310 | :code "(defn foo [])"}})) 311 | (let [response (h/assert-take (:output-ch server)) 312 | result (:result response)] 313 | (is (= 1 (count (:messages result)))) 314 | (is (= "Write a haiku Poem about:\n(defn foo [])" 315 | (-> result 316 | :messages 317 | first 318 | :content 319 | :text))))) 320 | (testing "Invalid prompt request" 321 | (async/put! 322 | (:input-ch server) 323 | (lsp.requests/request 3 "prompts/get" {:name "invalid-prompt"})) 324 | (is (= (lsp.responses/error (lsp.responses/response 3) 325 | (mcp.errors/body :prompt-not-found 326 | {:prompt-name 327 | "invalid-prompt"})) 328 | (h/assert-take (:output-ch server))))) 329 | (lsp.server/shutdown server)))) 330 | 331 | (deftest resource-listing 332 | (testing "Listing available resources" 333 | (let [server (server/chan-server) 334 | context (server/create-context! {:name "test-server", 335 | :version "1.0.0", 336 | :tools [], 337 | :prompts [], 338 | :resources [resource-test-file 339 | resource-test-json]}) 340 | _join (server/start! server context)] 341 | (testing "Resources list request" 342 | (async/put! (:input-ch server) 343 | (lsp.requests/request 1 "resources/list" {})) 344 | (let [response (h/assert-take (:output-ch server)) 345 | result (:result response)] 346 | (is (= 2 (count (:resources result)))) 347 | (let [file-resource (first (:resources result)) 348 | json-resource (second (:resources result))] 349 | (is (= "file:///test.txt" (:uri file-resource))) 350 | (is (= "Test File" (:name file-resource))) 351 | (is (= "A test file" (:description file-resource))) 352 | (is (= "text/plain" (:mimeType file-resource))) 353 | (is (= "file:///data.json" (:uri json-resource))) 354 | (is (= "Test Data" (:name json-resource))) 355 | (is (= "Test JSON data" (:description json-resource))) 356 | (is (= "application/json" (:mimeType json-resource))))) 357 | (lsp.server/shutdown server))))) 358 | 359 | (deftest resource-reading 360 | (testing "Reading resources" 361 | (let [server (server/chan-server) 362 | context (server/create-context! {:name "test-server", 363 | :version "1.0.0", 364 | :tools [], 365 | :prompts [], 366 | :resources [resource-test-file 367 | resource-test-json]}) 368 | _join (server/start! server context)] 369 | (testing "Read text file resource" 370 | (async/put! 371 | (:input-ch server) 372 | (lsp.requests/request 2 "resources/read" {:uri "file:///test.txt"})) 373 | (let [response (h/assert-take (:output-ch server)) 374 | result (:result response)] 375 | (is (= 1 (count (:contents result)))) 376 | (let [content (first (:contents result))] 377 | (is (= "file:///test.txt" (:uri content))) 378 | (is (= "text/plain" (:mimeType content))) 379 | (is (contains? content :text))))) 380 | (testing "Read JSON resource" 381 | (async/put! 382 | (:input-ch server) 383 | (lsp.requests/request 3 "resources/read" {:uri "file:///data.json"})) 384 | (let [response (h/assert-take (:output-ch server)) 385 | result (:result response)] 386 | (is (= 1 (count (:contents result)))) 387 | (let [content (first (:contents result))] 388 | (is (= "file:///data.json" (:uri content))) 389 | (is (= "application/json" (:mimeType content))) 390 | (is (contains? content :blob))))) 391 | (testing "Invalid resource request" 392 | (async/put! (:input-ch server) 393 | (lsp.requests/request 4 394 | "resources/read" 395 | {:uri "file:///invalid.txt"})) 396 | (is (= (lsp.responses/error (lsp.responses/response 4) 397 | (mcp.errors/body :resource-not-found 398 | {:uri 399 | "file:///invalid.txt"})) 400 | (h/assert-take (:output-ch server))))) 401 | (lsp.server/shutdown server)))) 402 | 403 | (deftest coerce-tool-response-test 404 | (testing "Coercing tool responses" 405 | (testing "Tool with sequential response" 406 | (let [tool {:name "list-maker", 407 | :description "Creates a list of items", 408 | :inputSchema {:type "object", 409 | :properties {"count" {:type "number"}}}} 410 | handler-response [{:type "text", :text "item 1"} 411 | {:type "text", :text "item 2"}] 412 | coerced (server/coerce-tool-response tool handler-response)] 413 | (is (= {:content [{:type "text", :text "item 1"} 414 | {:type "text", :text "item 2"}]} 415 | coerced)) 416 | (is 417 | (not (contains? coerced :structuredContent)) 418 | "Should not include structuredContent when tool has no outputSchema"))) 419 | (testing "Tool with non-sequential response" 420 | (let [tool {:name "echo", 421 | :description "Echoes input", 422 | :inputSchema {:type "object", 423 | :properties {"message" {:type "string"}}}} 424 | handler-response {:type "text", :text "single item"} 425 | coerced (server/coerce-tool-response tool handler-response)] 426 | (is (= {:content [{:type "text", :text "single item"}]} coerced)) 427 | (is (vector? (:content coerced)) 428 | "Response should be wrapped in a vector"))) 429 | (testing "Tool with outputSchema" 430 | (let [tool {:name "calculator", 431 | :description "Performs calculations", 432 | :inputSchema {:type "object", 433 | :properties {"expression" {:type "string"}}}, 434 | :outputSchema {:type "object", 435 | :properties {"result" {:type "number"}}}} 436 | handler-response {:result 42} 437 | coerced (server/coerce-tool-response tool handler-response)] 438 | (is (= {:content [{:result 42}], :structuredContent [{:result 42}]} 439 | coerced)) 440 | (is (contains? coerced :structuredContent) 441 | "Should include structuredContent when tool has outputSchema"))))) 442 | 443 | (deftest validate-spec-test 444 | (testing "Validating server specifications" 445 | (let [valid-tool {:name "valid-tool", 446 | :description "A valid tool", 447 | :inputSchema {:type "object"}, 448 | :handler identity} 449 | valid-resource {:uri "file:///valid.txt", 450 | :name "Valid Resource", 451 | :handler (fn [_] 452 | {:uri "file:///valid.txt", :text "valid"})} 453 | valid-prompt {:name "valid-prompt", :handler (fn [_] {:messages []})} 454 | valid-server-info {:name "test-server", :version "1.0.0"} 455 | invalid-tool-schema {:name "invalid-tool", 456 | :description "Bad schema", 457 | :inputSchema {:invalid "schema"}, ; Invalid key 458 | :handler identity} 459 | invalid-resource-missing-name 460 | {:uri "file:///invalid.txt", 461 | ;; Missing :name 462 | :handler (fn [_] {:uri "file:///invalid.txt", :text "invalid"})} 463 | invalid-prompt-missing-name {;; Missing :name 464 | :handler (fn [_] {:messages []})} 465 | invalid-handler-tool {:name "invalid-handler-tool", 466 | :description "Non-fn handler", 467 | :inputSchema {:type "object"}, 468 | :handler "not-a-function"} 469 | invalid-server-info-missing-name {:version "1.0.0"} 470 | invalid-server-info-missing-version {:name "test-server"} 471 | invalid-server-info-bad-type {:name 123, :version "1.0.0"}] 472 | (testing "Valid specification" 473 | (is (server/validate-spec! (merge valid-server-info 474 | {:tools [valid-tool], 475 | :prompts [valid-prompt], 476 | :resources [valid-resource]})) 477 | "A completely valid spec should not throw")) 478 | (testing "Empty specification (requires server info)" 479 | (is (server/validate-spec! valid-server-info) 480 | "A spec with only server info should be valid") 481 | (is (server/validate-spec! 482 | (merge valid-server-info {:tools [], :prompts [], :resources []})) 483 | "A spec with server info and empty lists should be valid")) 484 | (testing "Partially valid specification (requires server info)" 485 | (is (server/validate-spec! (merge valid-server-info 486 | {:tools [valid-tool]})) 487 | "A spec with only valid tools should be valid") 488 | (is (server/validate-spec! (merge valid-server-info 489 | {:prompts [valid-prompt]})) 490 | "A spec with only valid prompts should be valid") 491 | (is (server/validate-spec! (merge valid-server-info 492 | {:resources [valid-resource]})) 493 | "A spec with only valid resources should be valid")) 494 | (testing "Invalid server info - missing name" 495 | (is (thrown? Exception 496 | (server/validate-spec! invalid-server-info-missing-name)) 497 | "Spec with missing server name should throw")) 498 | (testing "Invalid server info - missing version" 499 | (is (thrown? Exception 500 | (server/validate-spec! 501 | invalid-server-info-missing-version)) 502 | "Spec with missing server version should throw")) 503 | (testing "Invalid server info - bad type" 504 | (is (thrown? Exception 505 | (server/validate-spec! invalid-server-info-bad-type)) 506 | "Spec with invalid server info type should throw")) 507 | (testing "Invalid tool schema" 508 | (is (thrown? Exception 509 | (server/validate-spec! (merge valid-server-info 510 | {:tools 511 | [invalid-tool-schema]}))) 512 | "Spec with invalid tool schema should throw")) 513 | (testing "Invalid resource definition" 514 | (is (thrown? Exception 515 | (server/validate-spec! 516 | (merge valid-server-info 517 | {:resources [invalid-resource-missing-name]}))) 518 | "Spec with invalid resource definition should throw")) 519 | (testing "Invalid prompt definition" 520 | (is (thrown? Exception 521 | (server/validate-spec! 522 | (merge valid-server-info 523 | {:prompts [invalid-prompt-missing-name]}))) 524 | "Spec with invalid prompt definition should throw")) 525 | (testing "Invalid handler type" 526 | (is (thrown? Exception 527 | (server/validate-spec! (merge valid-server-info 528 | {:tools 529 | [invalid-handler-tool]}))) 530 | "Spec with a non-function handler should throw"))))) 531 | -------------------------------------------------------------------------------- /src/io/modelcontext/clojure_sdk/specs.clj: -------------------------------------------------------------------------------- 1 | (ns io.modelcontext.clojure-sdk.specs 2 | (:require [clojure.spec.alpha :as s] 3 | [lsp4clj.coercer :as coercer])) 4 | 5 | ;; [tag: reuse_lsp4clj_coercer] 6 | ;; 7 | ;; This file heavily reuses specs defined in the `lsp4clj.coercer` namespace. 8 | ;; If you don't find the definition of a spec in this ns, check the coercer ns. 9 | 10 | ;; JSON-RPC types 11 | ;; Refer to `::coercer/json-rpc.input` to see all the possible inputs as 12 | ;; defined 13 | ;; in the JSON-RPC spec. 14 | 15 | ;; Protocol constants 16 | (def supported-protocol-versions ["2025-03-26" "2024-11-05"]) 17 | ;; [tag: version_negotiation] 18 | ;; 19 | ;; (From [[/specification/draft/basic/lifecycle.mdx::Version Negotiation]]) 20 | ;; 21 | ;; In the `initialize` request, the client **MUST** send a protocol 22 | ;; version it supports. This **SHOULD** be the _latest_ version supported 23 | ;; by the client. 24 | ;; 25 | ;; If the server supports the requested protocol version, it **MUST** 26 | ;; respond with the same version. Otherwise, the server **MUST** respond 27 | ;; with another protocol version it supports. This **SHOULD** be the 28 | ;; _latest_ version supported by the server. 29 | ;; 30 | ;; If the client does not support the version in the server's response, 31 | ;; it **SHOULD** disconnect. 32 | 33 | ;;; Base Interface: Request 34 | ;; A progress token, used to associate progress notifications with the original 35 | ;; request. 36 | (s/def ::progressToken 37 | (s/or :str string? 38 | :num number?)) 39 | ;; An opaque token used to represent a cursor for pagination. 40 | (s/def ::cursor string?) 41 | 42 | ;; _meta: If specified, the caller is requesting out-of-band progress 43 | ;; notifications for this request (as represented by 44 | ;; notifications/progress). The value of this parameter is an opaque 45 | ;; token that will be attached to any subsequent notifications. The 46 | ;; receiver is not obligated to provide these notifications. 47 | (s/def :json-rpc.message/_meta (s/keys :opt-un [::progressToken])) 48 | ;; Parameters 49 | (s/def :json-rpc.message/params (s/keys :opt-un [:json-rpc.message/_meta])) 50 | 51 | ;;; Base interface: Notification 52 | ;; [ref: reuse_lsp4clj_coercer] 53 | 54 | ;;; Base interface: Result 55 | ;; [ref: reuse_lsp4clj_coercer] 56 | ;; _meta: This result property is reserved by the protocol to allow clients and 57 | ;; servers to attach additional metadata to their responses. 58 | (s/def :json-rpc.message/result (s/keys :opt-un [:json-rpc.message/_meta])) 59 | 60 | ;; Standard error codes 61 | ;; [tag: reuse_lsp4clj_errors] 62 | ;; 63 | ;; These error codes are defined in the namespace `lsp4clj.lsp.errors` and can 64 | ;; be reused using the helpers provided there 65 | 66 | ;; Cancellation 67 | ;; [tag: cancelled_notification] 68 | ;; This notification can be sent by either side to indicate that it is 69 | ;; cancelling a previously-issued request. 70 | ;; 71 | ;; The request SHOULD still be in-flight, but due to communication latency, it 72 | ;; is always possible that this notification MAY arrive after the request has 73 | ;; already finished. 74 | ;; 75 | ;; This notification indicates that the result will be unused, so any 76 | ;; associated processing SHOULD cease. 77 | ;; 78 | ;; A client MUST NOT attempt to cancel its `initialize` request. 79 | ;; method: "notifications/cancelled" 80 | (s/def :cancelled-notification/requestId :json-rpc.message/id) 81 | (s/def :cancelled-notification/reason string?) 82 | (s/def ::cancelled-notification 83 | (s/keys :req-un [:cancelled-notification/requestId] 84 | :opt-un [:cancelled-notification/reason])) 85 | 86 | ;;; Initialization 87 | ;; [tag: initialize_request] 88 | ;; This request is sent from the client to the server when it first connects, 89 | ;; asking it to begin initialization 90 | (s/def :initialize/protocolVersion string?) 91 | ;; The latest version of the Model Context Protocol that the client 92 | ;; supports. The client MAY decide to support older versions as 93 | ;; well. 94 | ;; [ref: client_capabilities] for :initialize-request/capabilities and 95 | ;; [ref: implementation_server_client_info] for :initialize-request/clientInfo 96 | (s/def ::initialize-request 97 | (s/keys :req-un [:initialize/protocolVersion :initialize-request/capabilities 98 | :initialize-request/clientInfo])) 99 | 100 | ;; After receiving an initialize request from the client, the server sends this 101 | ;; response. 102 | (s/def :initialize-response/instructions string?) 103 | (s/def :initialize-response/result 104 | ;; The version of the Model Context Protocol that the server wants to 105 | ;; use. This may not match the version that the client requested. If the 106 | ;; client cannot support this version, it MUST disconnect. 107 | ;; [ref: server_capabilities] for :initialize-response/capabilities and 108 | ;; [ref: implementation_server_client_info] for 109 | ;; :initialize-response/serverInfo 110 | (s/keys :req-un [:initialize/protocolVersion :initialize-response/capabilities 111 | :initialize-response/serverInfo] 112 | ;; Instructions describing how to use the 113 | ;; server and its features. This can be used by 114 | ;; clients to improve the LLM's understanding 115 | ;; of available tools, resources, etc. It can 116 | ;; be thought of like a "hint" to the model. 117 | ;; For example, this information MAY be added 118 | ;; to the system prompt. 119 | :opt-un [:initialize-response/instructions])) 120 | (s/def ::initialize-response 121 | (s/and (s/or :error ::coercer/response-error 122 | :initialize :initialize-response/result) 123 | (s/conformer second))) 124 | 125 | ;; [tag: initialized_notification] 126 | ;; This notification is sent from the client to the server after initialization 127 | ;; has finished. The server should start processing requests after this 128 | ;; notification. 129 | (s/def ::initialized-notification (s/keys :opt-un [:json-rpc.message/_meta])) 130 | 131 | ;;; Capabilities 132 | ;; Experimental, non-standard capabilities that the server/client supports. 133 | (s/def :capabilities/experimental (s/map-of string? any?)) 134 | ;; Whether the server/client supports notifications for changes to the 135 | ;; prompts/roots list. 136 | (s/def :capabilities/listChanged boolean?) 137 | ;; Present if the client supports listing roots. 138 | (s/def :capabilities/roots (s/keys :opt-un [:capabilities/listChanged])) 139 | ;; Present if the client supports sampling from an LLM. 140 | (s/def :capabilities/sampling any?) 141 | 142 | ;; [tag: client_capabilities] 143 | ;; 144 | ;; Capabilities a client may support. Known capabilities are defined here, in 145 | ;; this schema, but this is not a closed set: any client can define its own, 146 | ;; additional capabilities. 147 | (s/def ::client-capabilities 148 | (s/keys :opt-un [:capabilities/experimental :capabilities/roots 149 | :capabilities/sampling])) 150 | (s/def :initialize-request/capabilities ::client-capabilities) 151 | 152 | ;; Present if the server supports sending log messages to the client. 153 | (s/def :capabilities/logging any?) 154 | (s/def :capabilities/subscribe boolean?) 155 | ;; Present if the server offers any prompt templates. 156 | (s/def :capabilities/prompts (s/keys :opt-un [:capabilities/listChanged])) 157 | ;; Present if the server offers any resources to read. 158 | (s/def :capabilities/resources 159 | (s/keys :opt-un [:capabilities/subscribe :capabilities/listChanged])) 160 | ;; Present if the server offers any tools to call. 161 | (s/def :capabilities/tools (s/keys :opt-un [:capabilities/listChanged])) 162 | 163 | ;; [tag: server_capabilities] 164 | ;; 165 | ;; Capabilities that a server may support. Known capabilities are defined here, 166 | ;; in this schema, but this is not a closed set: any server can define its own, 167 | ;; additional capabilities. 168 | (s/def ::server-capabilities 169 | (s/keys :opt-un [:capabilities/experimental :capabilities/logging 170 | :capabilities/prompts :capabilities/resources 171 | :capabilities/tools])) 172 | (s/def :initialize-response/capabilities ::server-capabilities) 173 | 174 | ;;; [tag: implementation_server_client_info] 175 | ;; Describes the name and version of an MCP implementation. 176 | (s/def :implementation/name string?) 177 | (s/def :implementation/version string?) 178 | (s/def ::implementation 179 | (s/keys :req-un [:implementation/name :implementation/version])) 180 | (s/def :initialize-request/clientInfo ::implementation) 181 | (s/def :initialize-response/serverInfo ::implementation) 182 | 183 | ;;; [tag: ping_request] 184 | ;; A ping, issued by either the server or the client, to check that the other 185 | ;; party is still alive. The receiver must promptly respond, or else may be 186 | ;; disconnected. 187 | (s/def ::ping-request any?) 188 | 189 | ;;; [tag: progress_notification] 190 | ;; An out-of-band notification used to inform the receiver of a progress update 191 | ;; for a long-running request. 192 | ;; The progress thus far. This should increase every time progress is made, 193 | ;; even if the total is unknown. 194 | (s/def :progress-notification/progress number?) 195 | ;; Total number of items to process (or total progress required), if known. 196 | (s/def :progress-notification/total number?) 197 | (s/def ::progress-notification 198 | (s/keys :req-un [::progressToken :progress-notification/progress] 199 | :opt-un [:progress-notification/total])) 200 | 201 | ;;; Pagination 202 | (s/def ::paginated-request (s/keys :opt-un [::cursor])) 203 | (s/def :paginated/nextCursor ::cursor) 204 | (s/def ::paginated-response (s/keys :opt-un [:paginated/nextCursor])) 205 | 206 | ;;; Resources 207 | ;; [tag: list_resources_request] 208 | ;; Sent from the client to request a list of resources the server has. 209 | (s/def ::list-resources-request ::paginated-request) 210 | 211 | ;; The server's response to a resources/list request from the client. 212 | (s/def :list-resources-response/resources (s/coll-of ::resource)) 213 | (s/def :list-resources-response/result 214 | (s/merge ::paginated-response 215 | (s/keys :req-un [:list-resources-response/resources]))) 216 | (s/def ::list-resources-response 217 | (s/and (s/or :error ::coercer/response-error 218 | :list-resources :list-resources-response/result) 219 | (s/conformer second))) 220 | 221 | ;; [tag: list_resource_templates_request] 222 | ;; Sent from the client to request a list of resource templates the server has. 223 | (s/def ::list-resource-templates-request ::paginated-request) 224 | 225 | ;; The server's response to a resources/templates/list request from the client. 226 | (s/def :list-resource-templates-response/resourceTemplates 227 | (s/coll-of ::resource-template)) 228 | (s/def :list-resource-templates-response/result 229 | (s/merge ::paginated-response 230 | (s/keys :req-un 231 | [:list-resource-templates-response/resourceTemplates]))) 232 | (s/def ::list-resource-templates-response 233 | (s/and (s/or :error ::coercer/response-error 234 | :list-resource-templates 235 | :list-resource-templates-response/result) 236 | (s/conformer second))) 237 | 238 | ;; [tag: read_resource_request] 239 | ;; Sent from the client to the server, to read a specific resource URI. 240 | (s/def ::read-resource-request (s/keys :req-un [:resource/uri])) 241 | 242 | ;; The server's response to a resources/read request from the client. 243 | (s/def :read-resource-response/content 244 | (s/and (s/or :text-resource :contents/text-resource 245 | :blob-resource :contents/blob-resource) 246 | (s/conformer second))) 247 | (s/def :read-resource-response/contents 248 | (s/coll-of :read-resource-response/content)) 249 | (s/def :read-resource-response/result 250 | (s/keys :req-un [:read-resource-response/contents])) 251 | (s/def ::read-resource-response 252 | (s/and (s/or :error ::coercer/response-error 253 | :read-resource :read-resource-response/result) 254 | (s/conformer second))) 255 | 256 | ;;; Resource List Changed Notification 257 | ;; [tag: resource_list_changed_notification] 258 | ;; An optional notification from the server to the client, informing 259 | ;; it that the list of resources it can read from has changed. This 260 | ;; may be issued by servers without any previous subscription from the 261 | ;; client. 262 | ;; method: "notifications/resources/list_changed" 263 | (s/def ::resource-list-changed-notification 264 | (s/keys :opt-un [:json-rpc.message/_meta])) 265 | 266 | ;;; Resource Subscribe/Unsubscribe 267 | ;; [tag: resource_subscribe_unsubscribe_request] 268 | ;; Subscribe: Sent from the client to request resources/updated notifications 269 | ;; from the 270 | ;; server whenever a particular resource changes. 271 | ;; Unsubscribe: Sent from the client to request cancellation of 272 | ;; resources/updated 273 | ;; notifications from the server. This should follow a previous 274 | ;; resources/subscribe request. 275 | ;; Both requests expect an empty response from the server 276 | (s/def ::resource-subscribe-unsubscribe-request 277 | (s/keys :req-un [:resource/uri])) 278 | 279 | ;;; Resource Updated Notification 280 | ;; [tag: resource_updated_notification] 281 | ;; A notification from the server to the client, informing it that a 282 | ;; resource has changed and may need to be read again. This should 283 | ;; only be sent if the client previously sent a resources/subscribe 284 | ;; request. 285 | ;; method: "notifications/resources/updated" 286 | (s/def ::resource-updated-notification (s/keys :req-un [:resource/uri])) 287 | 288 | ;;; Resource 289 | ;; A known resource that the server is capable of reading. 290 | ;; The URI of this resource. 291 | (s/def :resource/uri string?) 292 | ;; A human-readable name for this resource. This can be used by clients to 293 | ;; populate UI elements. 294 | (s/def :resource/name string?) 295 | ;; A description of what this resource represents. This can be used by clients 296 | ;; to improve the LLM's understanding of available resources. It can be thought 297 | ;; of like a "hint" to the model. 298 | (s/def :resource/description string?) 299 | ;; The MIME type of this resource, if known. 300 | (s/def :resource/mimeType string?) 301 | ;; The size of the raw resource content, in bytes (i.e., before base64 encoding 302 | ;; or any tokenization), if known. This can be used by Hosts to display file 303 | ;; sizes and estimate context window usage. 304 | (s/def :resource/size number?) 305 | (s/def ::resource 306 | (s/merge ::annotated (s/keys :req-un [:resource/uri :resource/name] 307 | :opt-un [:resource/description :resource/mimeType 308 | :resource/size]))) 309 | 310 | ;;; Resource Template 311 | ;; A template description for resources available on the server. 312 | ;; A URI template (according to RFC 6570) that can be used to construct 313 | ;; resource 314 | ;; URIs. 315 | (s/def :resource-template/uriTemplate string?) 316 | (s/def ::resource-template 317 | (s/merge ::annotated (s/keys 318 | :req-un [:resource-template/uriTemplate :resource/name] 319 | :opt-un [:resource/description :resource/mimeType]))) 320 | 321 | ;;; Resource Contents 322 | ;; The contents of a specific resource or sub-resource. 323 | (s/def :contents/resource 324 | (s/keys :req-un [:resource/uri] :opt-un [:resource/mimeType])) 325 | ;; The text of the item. This must only be set if the item can actually be 326 | ;; represented as text (not binary data). 327 | (s/def :contents/text string?) 328 | ;; A base64-encoded string representing the binary data of the item. 329 | (s/def :contents/blob string?) 330 | 331 | (s/def :contents/text-resource 332 | (s/merge :contents/resource (s/keys :req-un [:contents/text]))) 333 | (s/def :contents/blob-resource 334 | (s/merge :contents/resource (s/keys :req-un [:contents/blob]))) 335 | 336 | ;;; Prompts 337 | ;; [tag: list_prompts_request] 338 | ;; Sent from the client to request a list of prompts and prompt templates the 339 | ;; server has. 340 | (s/def ::list-prompts-request ::paginated-request) 341 | 342 | ;; The server's response to a prompts/list request from the client. 343 | (s/def :list-prompts-response/prompts (s/coll-of ::prompt)) 344 | (s/def :list-prompts-response/result 345 | (s/merge ::paginated-response (s/keys :req-un 346 | [:list-prompts-response/prompts]))) 347 | (s/def ::list-prompts-response 348 | (s/and (s/or :error ::coercer/response-error 349 | :list-prompts :list-prompts-response/result) 350 | (s/conformer second))) 351 | 352 | ;; [tag: get_prompt_request] 353 | ;; Used by the client to get a prompt provided by the server. 354 | (s/def :get-prompt-request/arguments (s/map-of keyword? string?)) 355 | (s/def ::get-prompt-request 356 | (s/keys :req-un [:prompt/name] :opt-un [:get-prompt-request/arguments])) 357 | 358 | ;; The server's response to a prompts/get request from the client. 359 | (s/def :get-prompt-response/messages (s/coll-of ::prompt-message)) 360 | (s/def :get-prompt-response/result 361 | (s/keys :req-un [:get-prompt-response/messages] 362 | :opt-un [:prompt/description])) 363 | (s/def ::get-prompt-response 364 | (s/and (s/or :error ::coercer/response-error 365 | :get-prompt :get-prompt-response/result) 366 | (s/conformer second))) 367 | 368 | ;;; Prompt 369 | (s/def :prompt/name string?) 370 | (s/def :prompt/description string?) 371 | (s/def :prompt/arguments (s/coll-of ::prompt-argument)) 372 | (s/def ::prompt 373 | (s/keys :req-un [:prompt/name] 374 | :opt-un [:prompt/description :prompt/arguments])) 375 | 376 | ;;; Prompt Argument 377 | ;; Describes an argument that a prompt can accept. 378 | (s/def :prompt-argument/name string?) 379 | (s/def :prompt-argument/description string?) 380 | ;; Whether this argument must be provided. 381 | (s/def :prompt-argument/required boolean?) 382 | (s/def ::prompt-argument 383 | (s/keys :req-un [:prompt-argument/name] 384 | :opt-un [:prompt-argument/description :prompt-argument/required])) 385 | 386 | ;;; Role 387 | ;; The sender or recipient of messages and data in a conversation. 388 | (s/def ::role #{"user" "assistant"}) 389 | 390 | ;;; Prompt Message 391 | ;; Describes a message returned as part of a prompt. This is similar 392 | ;; to `SamplingMessage`, but also supports the embedding of resources 393 | ;; from the MCP server. 394 | (s/def :prompt-message/content 395 | (s/and (s/or :text-content :content/text 396 | :image-content :content/image 397 | :audio-content :content/audio 398 | :embedded-resource :resource/embedded) 399 | (s/conformer second))) 400 | (s/def ::prompt-message (s/keys :req-un [::role :prompt-message/content])) 401 | 402 | ;;; Embedded Resource 403 | ;; The contents of a resource, embedded into a prompt or tool call result. It 404 | ;; is 405 | ;; up to the client how best to render embedded resources for the benefit of 406 | ;; the 407 | ;; LLM and/or the user. 408 | (s/def :embedded-resource/type #{"resource"}) 409 | (s/def :embedded-resource/resource 410 | (s/and (s/or :text-resource :contents/text-resource 411 | :blob-resource :contents/blob-resource) 412 | (s/conformer second))) 413 | (s/def :resource/embedded 414 | (s/merge ::annotated (s/keys :req-un [:embedded-resource/type 415 | :embedded-resource/resource]))) 416 | 417 | ;;; [tag: prompt_list_changed_notification] 418 | ;; An optional notification from the server to the client, informing it that 419 | ;; the 420 | ;; list of prompts it offers has changed. This may be issued by servers without 421 | ;; any previous subscription from the client. 422 | ;; method: "notifications/prompts/list_changed" 423 | (s/def ::prompt-list-changed-notification 424 | (s/keys :opt-un [:json-rpc.message/_meta])) 425 | 426 | ;;; Tools 427 | ;; [tag: list_tools_request] 428 | ;; Sent from the client to request a list of tools the server has. 429 | 430 | (s/def ::list-tools-request ::paginated-request) 431 | 432 | ;; The server's response to a tools/list request from the client. 433 | (s/def :list-tools-response/tools (s/coll-of ::tool)) 434 | (s/def :list-tools-response/result 435 | (s/merge ::paginated-response (s/keys :req-un [:list-tools-response/tools]))) 436 | (s/def ::list-tools-response 437 | (s/and (s/or :error ::coercer/response-error 438 | :list-tools :list-tools-response/result) 439 | (s/conformer second))) 440 | 441 | ;; Tool Call 442 | ;; [tag: call_tool_request] 443 | ;; The server's response to a tool call. 444 | ;; 445 | ;; Any errors that originate from the tool SHOULD be reported inside the result 446 | ;; object, with `isError` set to true, _not_ as an MCP protocol-level error 447 | ;; response. Otherwise, the LLM would not be able to see that an error occurred 448 | ;; and self-correct. 449 | ;; 450 | ;; However, any errors in _finding_ the tool, an error indicating that the 451 | ;; server does not support tool calls, or any other exceptional conditions, 452 | ;; should be reported as an MCP error response. 453 | 454 | ;; Used by the client to invoke a tool provided by the server. 455 | (s/def :call-tool-request/arguments (s/map-of keyword? any?)) 456 | (s/def ::call-tool-request 457 | (s/keys :req-un [:tool/name] :opt-un [:call-tool-request/arguments])) 458 | 459 | ;; The server's response to a tool call. 460 | (s/def ::content-list 461 | (s/coll-of (s/and (s/or :text :content/text 462 | :image :content/image 463 | :audio :content/audio 464 | :resource :resource/embedded) 465 | (s/conformer second)))) 466 | 467 | (s/def :call-tool-response/content ;; yes, this is a collection, and the 468 | ;; name is not 469 | ;; `contents`. This looks like a mistake they 470 | ;; made and kept for backwards compatibility. 471 | ::content-list) 472 | 473 | ;; If not set, this is assumed to be false (the call was successful). 474 | (s/def :call-tool-response/isError boolean?) 475 | 476 | ;; If the Tool does not define an outputSchema, `content` field MUST be present 477 | ;; in the result. 478 | (s/def :call-tool-response/unstructured-result 479 | (s/keys :req-un [:call-tool-response/content] 480 | :opt-un [:call-tool-response/isError])) 481 | 482 | ;; Tool result for tools that do declare an outputSchema. 483 | ;; [tag: structured-content-should-match-output-schema-exactly] 484 | (s/def :call-tool-response/structuredContent (s/map-of string? any?)) 485 | 486 | ;; If the Tool defines an outputSchema, `structuredContent` field MUST be 487 | ;; present in the result, and contain a JSON object that matches the schema. 488 | ;; `content` is for backward compatibility with older clients. 489 | (s/def :call-tool-response/structured-result 490 | (s/keys :req-un [:call-tool-response/structuredContent] 491 | :opt-un [:call-tool-response/content :call-tool-response/isError])) 492 | 493 | (s/def :call-tool-response/result 494 | (s/and (s/or :structured :call-tool-response/structured-result 495 | :unstructured :call-tool-response/unstructured-result) 496 | (s/conformer second))) 497 | 498 | (s/def ::call-tool-response 499 | (s/and (s/or :error ::coercer/response-error 500 | :call-tool :call-tool-response/result) 501 | (s/conformer second))) 502 | 503 | ;; [tag: tool_list_changed_notification] 504 | ;; An optional notification from the server to the client, informing it that 505 | ;; the list of tools it offers has changed. This may be issued by servers 506 | ;; without 507 | ;; any previous subscription from the client. 508 | ;; method: "notifications/tools/list_changed" 509 | (s/def ::tool-list-changed-notification 510 | (s/keys :opt-un [:json-rpc.message/_meta])) 511 | 512 | ;;; Tool 513 | ;; Definition for a tool the client can call. 514 | (s/def :tool/name string?) 515 | (s/def :tool/description string?) 516 | (s/def :tool/properties (s/map-of string? any?)) 517 | (s/def :tool/required (s/coll-of string?)) 518 | (s/def :schema/type #{"object"}) 519 | ;; A JSON Schema object defining the expected parameters for the tool. 520 | (s/def :tool/inputSchema 521 | (s/keys :req-un [:schema/type] :opt-un [:tool/properties :tool/required])) 522 | (s/def :tool/outputSchema ;; This is any? right now, but I'm copying 523 | ;; over from inputSchema to keep it 524 | ;; consistent 525 | (s/keys :req-un [:schema/type] :opt-un [:tool/properties])) 526 | (s/def ::tool 527 | (s/keys :req-un [:tool/name :tool/inputSchema] 528 | :opt-un [:tool/description :tool/outputSchema])) 529 | 530 | ;;; Logging 531 | ;; [tag: set_logging_level_request] 532 | ;; A request from the client to the server, to enable or adjust logging. 533 | ;; The level of logging that the client wants to receive from the server. The 534 | ;; server should send all logs at this level and higher (i.e., more severe) to 535 | ;; the client as notifications/logging/message. 536 | (s/def ::set-logging-level-request (s/keys :req-un [:logging/level])) 537 | 538 | ;; [tag: logging_message_notification] 539 | ;; Notification of a log message passed from server to client. If no 540 | ;; logging/setLevel request has been sent from the client, the server MAY 541 | ;; decide which messages to send automatically. 542 | ;; method: "notifications/message" 543 | (s/def :logging-message/logger string?) 544 | (s/def :logging-message/data any?) 545 | (s/def ::logging-message-notification 546 | (s/keys :req-un [:logging/level :logging-message/data] 547 | :opt-un [:logging-message/logger])) 548 | 549 | ;; The severity of a log message. These map to syslog message severities, as 550 | ;; specified in RFC-5424: 551 | ;; https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1 552 | (s/def :logging/level 553 | #{"debug" "info" "notice" "warning" "error" "critical" "alert" "emergency"}) 554 | 555 | ;;; Sampling 556 | ;; @TODO: Specs need to be cleaned up 557 | ;; A request from the server to sample an LLM via the client. The client has 558 | ;; full discretion over which model to select. The client should also inform 559 | ;; the 560 | ;; user before beginning sampling, to allow them to inspect the request (human 561 | ;; in the loop) and decide whether to approve it. 562 | (s/def :sampling-create-message-request/method #{"sampling/createMessage"}) 563 | (s/def :sampling-create-message-request/messages (s/coll-of ::sampling-message)) 564 | ;; An optional system prompt the server wants to use for sampling. The client 565 | ;; MAY modify or omit this prompt. 566 | (s/def :sampling-create-message-request/systemPrompt string?) 567 | ;; A request to include context from one or more MCP servers (including the 568 | ;; caller), to be attached to the prompt. The client MAY ignore this request. 569 | (s/def :sampling-create-message-request/includeContext 570 | #{"none" "thisServer" "allServers"}) 571 | (s/def :sampling-create-message-request/temperature number?) 572 | ;; The maximum number of tokens to sample, as requested by the server. The 573 | ;; client MAY choose to sample fewer tokens than requested. 574 | (s/def :sampling-create-message-request/maxTokens number?) 575 | (s/def :sampling-create-message-request/stopSequences (s/coll-of string?)) 576 | ;; Optional metadata to pass through to the LLM provider. The format of this 577 | ;; metadata is provider-specific. 578 | (s/def :sampling-create-message-request/metadata any?) 579 | (s/def ::sampling-create-message-request 580 | (s/keys :req-un [:sampling-create-message-request/messages 581 | :sampling-create-message-request/maxTokens] 582 | :opt-un [:sampling-create-message-request/modelPreferences 583 | :sampling-create-message-request/systemPrompt 584 | :sampling-create-message-request/includeContext 585 | :sampling-create-message-request/temperature 586 | :sampling-create-message-request/stopSequences 587 | :sampling-create-message-request/metadata])) 588 | 589 | ;; The client's response to a sampling/create_message request from the server. 590 | ;; The client should inform the user before returning the sampled message, to 591 | ;; allow them to inspect the response (human in the loop) and decide whether to 592 | ;; allow the server to see it. 593 | 594 | ;; The name of the model that generated the message. 595 | ;; @TODO: Specs need to be cleaned up 596 | (s/def :sampling-create-message-response/model string?) 597 | ;; The reason why sampling stopped, if known. 598 | (s/def :sampling-create-message-response/stopReason 599 | (s/and (s/or :known-reasons #{"endTurn" "stopSequence" "maxTokens"} 600 | :unknown-reasons string?) 601 | (s/conformer second))) 602 | (s/def :sampling-create-message-response/result 603 | (s/keys :req-un [:sampling-create-message-response/model] 604 | :opt-un [:sampling-create-message-response/stopReason])) 605 | 606 | ;; Describes a message issued to or received from an LLM API. 607 | (s/def :sampling-message/content 608 | (s/and (s/or :text :content/text 609 | :image :content/image 610 | :audio :content/audio) 611 | (s/conformer second))) 612 | (s/def ::sampling-message (s/keys :req-un [::role :sampling-message/content])) 613 | 614 | ;;; Annotated 615 | ;; Base for objects that include optional annotations for the client. The 616 | ;; client 617 | ;; can use annotations to inform how objects are used or displayed 618 | (s/def ::annotated (s/keys :opt-un [:annotated/annotations])) 619 | (s/def :annotated/annotations 620 | (s/keys :opt-un [:annotated/audience :annotated/priority])) 621 | ;; Describes who the intended customer of this object or data is. 622 | (s/def :annotated/audience (s/coll-of ::role)) 623 | ;; Describes how important this data is for operating the server. A value of 1 624 | ;; means "most important," and indicates that the data is effectively required, 625 | ;; while 0 means "least important," and indicates thatthe data is entirely 626 | ;; optional. 627 | (defn between-zero-and-one? [x] (<= 0 x 1)) 628 | (s/def :annotated/priority (s/and number? between-zero-and-one?)) 629 | 630 | ;; Text provided to or from an LLM. 631 | (s/def :content/text 632 | (s/merge ::annotated (s/keys :req-un [:text-content/type 633 | :text-content/text]))) 634 | (s/def :text-content/type #{"text"}) 635 | (s/def :text-content/text string?) 636 | 637 | ;; An image provided to or from an LLM. 638 | (s/def :content/image 639 | (s/merge ::annotated (s/keys :req-un [:image-content/type :image-content/data 640 | :image-content/mimeType]))) 641 | (s/def :image-content/type #{"image"}) 642 | ;; The base64-encoded image data. 643 | (s/def :image-content/data string?) 644 | (s/def :image-content/mimeType string?) 645 | 646 | ;; Audio provided to or from an LLM. 647 | (s/def :content/audio 648 | (s/merge ::annotated (s/keys :req-un [:audio-content/type :audio-content/data 649 | :audio-content/mimeType]))) 650 | 651 | (s/def :audio-content/type #{"audio"}) 652 | ;; The base64-encoded audio data. 653 | (s/def :audio-content/data string?) 654 | (s/def :audio-content/mimeType string?) 655 | 656 | ;;; Model Preferences 657 | ;; The server's preferences for model selection, requested of the client during 658 | ;; sampling. 659 | ;; 660 | ;; Because LLMs can vary along multiple dimensions, choosing the "best" model 661 | ;; is 662 | ;; rarely straightforward. Different models excel in different areas—some are 663 | ;; faster but less capable, others are more capable but more expensive, and so 664 | ;; on. This interface allows servers to express their priorities across 665 | ;; multiple dimensions to help clients make an appropriate selection for their 666 | ;; use case. 667 | ;; 668 | ;; These preferences are always advisory. The client MAY ignore them. It is 669 | ;; also up to the client to decide how to interpret these preferences and how 670 | ;; to balance them against other considerations. 671 | (s/def ::model-preferences 672 | (s/keys :opt-un [:model-preferences/hints :model-preferences/costPriority 673 | :model-preferences/speedPriority 674 | :model-preferences/intelligencePriority])) 675 | ;; Optional hints to use for model selection. If multiple hints are specified, 676 | ;; the client MUST evaluate them in order. The client SHOULD prioritize these 677 | ;; hints over the numeric priorities, but MAY still use the priorities to 678 | ;; select 679 | ;; from ambiguous matches. 680 | (s/def :model-preferences/hints (s/coll-of ::model-hint)) 681 | (s/def :model-preferences/costPriority number?) 682 | (s/def :model-preferences/speedPriority number?) 683 | (s/def :model-preferences/intelligencePriority number?) 684 | ;; A hint for a model name. The client SHOULD treat this as a substring of a 685 | ;; model name; for example: 686 | ;; - `claude-3-5-sonnet` should match `claude-3-5-sonnet-20241022` 687 | ;; - `sonnet` should match `claude-3-5-sonnet-20241022`, 688 | ;; `claude-3-sonnet-20240229`, etc. 689 | ;; - `claude` should match any Claude model 690 | ;; 691 | ;; The client MAY also map the string to a different provider's model name or a 692 | ;; different model family, as long as it fills a similar niche; for example: 693 | ;; - `gemini-1.5-flash` could match `claude-3-haiku-20240307` 694 | (s/def :model-hint/name string?) 695 | (s/def ::model-hint (s/keys :opt-un [:model-hint/name])) 696 | ;;; FIXME: For some reason, the compiler cannot find ::model-preferences if I 697 | ;;; use the spec before the definition. I don't know why, I'll look into it 698 | ;;; later. 699 | ;; The server's preferences for which model to select. The client MAY ignore 700 | ;; these preferences. 701 | (s/def :sampling-create-message-request/modelPreferences ::model-preferences) 702 | 703 | ;;; Completion 704 | ;; [tag: complete_request] 705 | ;; A request from the client to the server, to ask for completion options. 706 | (s/def :complete-request/ref 707 | (s/and (s/or :prompt-ref :ref/prompt 708 | :resource-ref :ref/resource) 709 | (s/conformer second))) 710 | (s/def :complete-request/argument 711 | (s/keys :req-un [:complete-request-argument/name 712 | :complete-request-argument/value])) 713 | (s/def :complete-request-argument/name string?) 714 | (s/def :complete-request-argument/value string?) 715 | (s/def ::complete-request 716 | (s/keys :req-un [:complete-request/ref :complete-request/argument])) 717 | 718 | ;; The server's response to a completion/complete request 719 | (s/def :complete-response/values (s/coll-of string?)) 720 | (s/def :complete-response/total number?) 721 | (s/def :complete-response/hasMore boolean?) 722 | (s/def :complete-response/completion 723 | (s/keys :req-un [:complete-response/values] 724 | :opt-un [:complete-response/total :complete-response/hasMore])) 725 | (s/def :complete-response/result 726 | (s/keys :req-un [:complete-response/completion])) 727 | (s/def ::complete-response 728 | (s/and (s/or :error ::coercer/response-error 729 | :complete :complete-response/result) 730 | (s/conformer second))) 731 | 732 | ;;; Reference Types 733 | (s/def :ref/type #{"ref/prompt" "ref/resource"}) 734 | (s/def :ref/prompt (s/keys :req-un [:ref/type :prompt/name])) 735 | (s/def :ref/resource (s/keys :req-un [:ref/type :resource/uri])) 736 | 737 | ;;; Roots 738 | ;; @TODO: Specs need to be cleaned up 739 | ;; Sent from the server to request a list of root URIs from the client. Roots 740 | ;; allow servers to ask for specific directories or files to operate on. A 741 | ;; common example for roots is providing a set of repositories or directories a 742 | ;; server should operate on. 743 | ;; 744 | ;; This request is typically used when the server needs to understand the file 745 | ;; system structure or access specific locations that the client has permission 746 | ;; to read from. 747 | ;; method: "roots/list" 748 | (s/def ::list-roots-request (s/keys :opt-un [:json-rpc.message/_meta])) 749 | ;; The client's response to a roots/list request from the server. This result 750 | ;; contains an array of Root objects, each representing a root directory or 751 | ;; file 752 | ;; that the server can operate on. 753 | (s/def :list-roots-response/roots (s/coll-of ::root)) 754 | (s/def :list-roots-response/result 755 | (s/keys :req-un [:list-roots-response/roots])) 756 | 757 | (s/def ::list-roots-response 758 | (s/and (s/or :error ::coercer/response-error 759 | :list-roots :list-roots-response/result) 760 | (s/conformer second))) 761 | 762 | ;; Represents a root directory or file that the server can operate on. 763 | (s/def :root/uri string?) 764 | (s/def :root/name string?) 765 | (s/def ::root (s/keys :req-un [:root/uri] :opt-un [:root/name])) 766 | 767 | ;; A notification from the client to the server, informing it that the list of 768 | ;; roots has changed. 769 | ;; 770 | ;; This notification should be sent whenever the client adds, removes, or 771 | ;; modifies any root. The server should then request an updated list of roots 772 | ;; using the ListRootsRequest. 773 | ;; method: "notifications/roots/list_changed" 774 | (s/def ::root-list-changed-notification 775 | (s/keys :opt-un [:json-rpc.message/_meta])) 776 | 777 | ;;; Client messages 778 | (s/def ::client-request 779 | (s/or :ping ::ping-request 780 | :initialize ::initialize-request 781 | :complete ::complete-request 782 | :set-logging-level ::set-logging-level-request 783 | :get-prompt ::get-prompt-request 784 | :list-prompts ::list-prompts-request 785 | :list-resources ::list-resources-request 786 | :list-resource-templates ::list-resource-templates-request 787 | :read-resource ::read-resource-request 788 | :subscribe ::resource-subscribe-unsubscribe-request 789 | :unsubscribe ::resource-subscribe-unsubscribe-request 790 | :call-tool ::call-tool-request 791 | :list-tools ::list-tools-request)) 792 | 793 | (s/def ::client-notification 794 | (s/or :cancelled ::cancelled-notification 795 | :progress ::progress-notification 796 | :initialized ::initialized-notification 797 | :root-list-changed ::root-list-changed-notification)) 798 | 799 | ;;; Server messages 800 | (s/def ::server-request 801 | (s/or :ping ::ping-request 802 | :sampling-create-message ::sampling-create-message-request 803 | :list-roots ::list-roots-request)) 804 | 805 | (s/def ::server-notification 806 | (s/or :cancelled ::cancelled-notification 807 | :progress ::progress-notification 808 | :logging-message ::logging-message-notification 809 | :resource-updated ::resource-updated-notification 810 | :resource-list-changed ::resource-list-changed-notification 811 | :tool-list-changed ::tool-list-changed-notification 812 | :prompt-list-changed ::prompt-list-changed-notification)) 813 | 814 | (s/def ::empty-response #(= {} %)) 815 | 816 | (s/def ::server-response 817 | (s/or :empty ::empty-response 818 | :initialize ::initialize-response 819 | :complete ::complete-response 820 | :get-prompt ::get-prompt-response 821 | :list-prompts ::list-prompts-response 822 | :list-resources ::list-resources-response 823 | :list-resource-templates ::list-resource-templates-response 824 | :read-resource ::read-resource-response 825 | :call-tool ::call-tool-response 826 | :list-tools ::list-tools-response)) 827 | 828 | (s/def :server-spec/handler ifn?) 829 | (s/def :server-spec/tool 830 | (s/merge ::tool (s/keys :req-un [:server-spec/handler]))) 831 | (s/def :server-spec/tools (s/coll-of :server-spec/tool)) 832 | (s/def :server-spec/prompt 833 | (s/merge ::prompt (s/keys :req-un [:server-spec/handler]))) 834 | (s/def :server-spec/prompts (s/coll-of :server-spec/prompt)) 835 | (s/def :server-spec/resource 836 | (s/merge ::resource (s/keys :req-un [:server-spec/handler]))) 837 | (s/def :server-spec/resources (s/coll-of :server-spec/resource)) 838 | (s/def ::server-spec 839 | (s/keys :req-un [:implementation/name :implementation/version] 840 | :opt-un [:server-spec/tools :server-spec/prompts 841 | :server-spec/resources])) 842 | 843 | ;; Helper functions for resource validation 844 | (defn valid-resource? [resource] (s/valid? ::resource resource)) 845 | 846 | (defn explain-resource [resource] (s/explain-data ::resource resource)) 847 | 848 | ;; Helper functions for prompt validation 849 | (defn valid-prompt? [prompt] (s/valid? ::prompt prompt)) 850 | 851 | (defn explain-prompt [prompt] (s/explain-data ::prompt prompt)) 852 | 853 | ;; Helper functions for tool validation 854 | (defn valid-tool? [tool] (s/valid? ::tool tool)) 855 | 856 | (defn explain-tool [tool] (s/explain-data ::tool tool)) 857 | 858 | ;; Helper functions for root validation 859 | (defn valid-root? [root] (s/valid? ::root root)) 860 | 861 | (defn explain-root [root] (s/explain-data ::root root)) 862 | 863 | ;; Helper functions for message content validation 864 | (defn valid-text-content? [content] (s/valid? :content/text content)) 865 | 866 | (defn valid-image-content? [content] (s/valid? :content/image content)) 867 | 868 | (defn valid-audio-content? [content] (s/valid? :content/audio content)) 869 | 870 | (defn valid-embedded-resource? [content] (s/valid? :resource/embedded content)) 871 | 872 | (defn explain-text-content [content] (s/explain-data :content/text content)) 873 | 874 | (defn explain-image-content [content] (s/explain-data :content/image content)) 875 | 876 | (defn explain-audio-content [content] (s/explain-data :content/audio content)) 877 | 878 | (defn explain-embedded-resource 879 | [content] 880 | (s/explain-data :resource/embedded content)) 881 | 882 | ;; Helper functions for sampling validation 883 | (defn valid-sampling-message? [msg] (s/valid? ::sampling-message msg)) 884 | 885 | (defn valid-model-preferences? [prefs] (s/valid? ::model-preferences prefs)) 886 | 887 | (defn explain-sampling-message [msg] (s/explain-data ::sampling-message msg)) 888 | 889 | (defn explain-model-preferences 890 | [prefs] 891 | (s/explain-data ::model-preferences prefs)) 892 | 893 | ;; Helper functions for implementation validation 894 | (defn valid-implementation? [impl] (s/valid? ::implementation impl)) 895 | 896 | (defn explain-implementation [impl] (s/explain-data ::implementation impl)) 897 | 898 | ;; Helper functions for server-spec validation 899 | (defn valid-server-spec? [spec] (s/valid? ::server-spec spec)) 900 | 901 | (defn explain-server-spec [spec] (s/explain-data ::server-spec spec)) 902 | --------------------------------------------------------------------------------