├── 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 | [](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 |
--------------------------------------------------------------------------------