A server is built from three kinds of definitions, all plain maps with keyword keys. Wire data (arguments, results, schemas) uses string keys throughout, as clojure.data.json reads it.
(require '[presa.client :as client]
'[presa.content :as content]
'[presa.server :as server])
A tool's handler is (fn [arguments context]). What it returns becomes the
tools/call result:
| Handler returns | Result |
|---|---|
| a string | one text content block |
| a map | structuredContent, plus a text block holding its JSON |
(server/content blocks) | those content blocks (see presa.content) |
a map with "content" | that result, as is |
(server/tool-error message) | isError: true, for errors the model can act on |
| nil | an empty result |
A handler that throws reports an isError result with the exception's
message. To answer with a JSON-RPC error instead, throw server/error.
(def tools
[{:name "add"
:description "Add numbers."
:input-schema {"type" "object"
"properties" {"numbers" {"type" "array" "items" {"type" "number"}}}
"required" ["numbers"]}
:output-schema {"type" "object" "properties" {"sum" {"type" "number"}} "required" ["sum"]}
:handler (fn [{:strs [numbers]} _] {"sum" (reduce + numbers)})}
{:name "logo"
:description "A one-pixel PNG."
:handler (fn [_ _]
(server/content
[(content/text "Here is the logo:")
(content/image "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9Z4p8AAAAASUVORK5CYII="
"image/png")]))}])
Arguments are checked against :input-schema and structured results
against :output-schema with presa.schema, a dependency-free subset of
JSON Schema (type, enum, const, required, properties,
additionalProperties, items, the size and range keywords, pattern).
Invalid arguments become an isError result naming the path, so the model
can correct its call. :validate? false on the server turns this off, and
:validator replaces the validator: presa.schema.networknt/validator
gives full 2020-12 validation on the JVM with
com.networknt/json-schema-validator added.
A resource has a URI and a handler of the context. It returns a string (text), a byte array (blob, sent as base64), or contents maps.
A resource template has an RFC 6570 URI template ({var} for one path
segment, {+var} for the rest of the URI) and a handler of the variables
and the context. It returns nil when there is no such resource, which the
client sees as -32602 Resource not found with the URI in data.
(def people {"ada" "Ada Lovelace" "alan" "Alan Turing"})
(def resources
[{:uri "file:///readme.txt" :name "readme" :mime-type "text/plain"
:handler (fn [_] "Read me first.")}])
(def templates
[{:uri-template "people://{id}" :name "person" :mime-type "text/plain"
:handler (fn [{:strs [id]} _] (get people id))
:complete {"id" (fn [value _ _] (filter #(.startsWith ^String % value) (sort (keys people))))}}])
A prompt's handler takes the arguments (string to string) and the context,
and returns a string (one user message), a sequence of messages (strings,
or maps with "role" and "content"), or a map with "messages".
Required arguments are checked before the handler runs.
(def prompts
[{:name "introduce"
:description "Introduce a person."
:arguments [{:name "id" :description "Who" :required true}]
:handler (fn [{:strs [id]} _] (str "Introduce " (get people id) " in one sentence."))
:complete {"id" (fn [value _ _] (filter #(.startsWith ^String % value) (sort (keys people))))}}])
:complete maps an argument name to a completion function of the value
typed so far, the other arguments, and the context. It returns strings, or
a map with "values", "total" and "hasMore"; at most 100 values are
sent.
(def srv (server/server {:name "library" :version "0.1.0"
:tools tools
:resources resources
:resource-templates templates
:prompts prompts
;; list results of more than 10 items come in pages
:page-size 10}))
(def c (client/connect (client/in-process srv)))
(get (client/call-tool c "add" {"numbers" [1 2 3]}) "structuredContent")
;; => {"sum" 6}
(client/text (client/call-tool c "add" {"numbers" ["one"]}))
;; => "Invalid arguments at /numbers/0: expected number, got string"
(get-in (client/read-resource c "people://ada") [0 "text"])
;; => "Ada Lovelace"
(:message (ex-data (try (client/read-resource c "people://nobody") (catch Exception e e))))
;; => "Resource not found"
(get-in (client/get-prompt c "introduce" {"id" "alan"}) ["messages" 0 "content" "text"])
;; => "Introduce Alan Turing in one sentence."
(get (client/complete c {"type" "ref/prompt" "name" "introduce"} "id" "a") "values")
;; => ["ada" "alan"]
The capabilities a server advertises follow from what it defines: tools
when it has tools, resources for resources or templates, prompts,
completions when any definition has :complete, and logging with
:logging true.
(:server-capabilities (client/info c))
;; => {"tools" {} "resources" {} "prompts" {} "completions" {}}
Definitions can change while the server serves. Each request reads one
snapshot of them; pagination cursors from before a change expire; and with
:list-changed true, clients listening for list changes are told (see
subscriptions.md).
(server/add-tool! srv {:name "late" :description "Added while serving" :handler (fn [_ _] "late")})
(map #(get % "name") (client/list-tools c))
;; => ["add" "logo" "late"]
(server/remove-tool! srv "late")
(client/close c)
add-resource!, remove-resource!, add-resource-template!,
remove-resource-template!, add-prompt! and remove-prompt! work the same
way.
2026-07-28 list, read and discover results carry ttlMs and cacheScope.
:cache {:ttl-ms 60000 :scope "public"} on the server sets them (the
default is 0 and private); a presa client with :cache true keeps
results for their TTL (see client.md).
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |