2025-11-25 features
The latest MCP spec adds six headline features: server description, icons, sampling with tools, elicitation (form + URL modes), tasks (experimental), and JSON Schema 2020-12 dialect. This page walks through each in worked-example form. For the version-by-version feature matrix see Protocol versions. For the migration writeup see docs/reference/MIGRATION-2025-11-25.md.
Server description
A new :description field on :server-info for human-readable server intent. Shows up in client UIs that display server metadata.
(server/create-session
{:server-info {:name "my-server"
:version "1.0.0"
:description "A helpful MCP server for data analysis"}
:tools [...]})
The field is included in the initialize response only when present. Clients that don't understand :description ignore it.
Icons
Visual icons for prompts, resources, tools, and resource templates. Add :icon to any registration; the toolkit's list handlers (prompt-list-handler, resource-list-handler, tool-list-handler, resource-templates-list-handler) include :icon in the response.
(def my-tool
{:name "read_file"
:title "File Reader"
:icon "https://example.com/icons/file.svg" ; ← https URL
:description "Reads a file from disk"
:input-schema {:type "object"
:properties {:path {:type "string"}}
:required [:path]}
:tool-fn read-file-fn})
;; or as a base64 data URI
(def my-tool
{:name "read_file"
:icon "data:image/svg+xml;base64,PHN2ZyB4bWxucz0i..."
...})
The Icon schema in mcp-toolkit.schema validates the format:
(schema/valid? schema/Icon "https://example.com/icon.png") ; true
(schema/valid? schema/Icon "data:image/svg+xml;base64,...") ; true
(schema/valid? schema/Icon "http://insecure.com/icon.png") ; false (must be https)
(schema/valid? schema/Icon "/local/path/icon.svg") ; false
(schema/valid? schema/Icon "data:application/json;base64,...") ; false (must be image/)
Use Schema validation to validate icons at registration time if you accept user-provided values.
Sampling with tools
In MCP, sampling is the server-to-client request "please run this prompt through your LLM and give me the response." The 2025-11-25 spec extends sampling with tool use: the LLM can call tools during the sampling round-trip, just like it does in a regular agent loop.
The request-sampling fn in mcp-toolkit.server carries :tools and :tool-choice:
(require '[mcp-toolkit.server :as server]
'[mcp-toolkit.schema :as schema])
;; Check capability before requesting
(when (server/client-supports-sampling-tools? context)
(server/request-sampling context
{:messages [{:role "user"
:content {:type "text"
:text "What's the weather in Tokyo?"}}]
:max-tokens 1000
:tools [(schema/sampling-tool
{:name "get_weather"
:description "Get current weather for a city"
:input-schema {:type "object"
:properties {:city {:type "string"}}
:required ["city"]}})]
:tool-choice (schema/tool-choice :auto)
:system-prompt "You are a helpful weather assistant."}))
:tool-choice modes (from schema/ToolChoiceMode):
"auto": model decides whether to call a tool (default)."required": model MUST call at least one tool."none": model MUST NOT call any tools.
The response shape includes :role "assistant", :content, :model, :stop-reason. When :stop-reason is "toolUse", :content contains tool-use blocks (schema/ToolUseContent); your code executes the tool, builds a tool-result-message, and re-issues request-sampling with the original messages plus the result message appended:
(p/let [response (server/request-sampling context original-request)]
(if (= (:stop-reason response) "toolUse")
(let [tool-uses (filter #(= (:type %) "tool_use") (:content response))
;; ... execute each tool, collect results ...
tool-results (mapv (fn [{:keys [id name input]}]
(schema/tool-result
{:tool-use-id id
:content {:type "text"
:text (run-tool name input)}}))
tool-uses)
followup (update original-request
:messages
(fn [msgs]
(-> msgs
(conj {:role "assistant" :content (:content response)})
(conj (schema/tool-result-message tool-results)))))]
(server/request-sampling context followup))
response))
The toolkit doesn't run the loop for you. It just gives you the building blocks (schema/tool-result, schema/tool-result-message). You decide when to stop (max iterations, max time, etc.).
The capability check client-supports-sampling-tools? reads (get-in client-capabilities [:sampling :tools]) from the session. If false, omit :tools / :tool-choice from the request, the toolkit's request-sampling doesn't strip them automatically.
Elicitation, form mode
Elicitation is "server asks the user a question." Form mode is the structured-data path: server sends a JSON Schema, client renders a form, user fills it in and submits.
(when (server/client-supports-form-elicitation? context)
(-> (server/request-elicitation context
(schema/form-elicitation
{:message "Please provide your project details"
:requested-schema {:type "object"
:properties {:name {:type "string"}
:description {:type "string"}
:tier {:type "string"
:enum ["free" "pro" "enterprise"]}}
:required ["name" "tier"]}}))
(p/then (fn [response]
(case (:action response)
"accept" (handle-input (:content response))
"decline" (log "User declined")
"cancel" (log "User cancelled"))))))
The response shape (schema/ElicitationResponse):
:action:"accept"/"decline"/"cancel".:content: the user's submission (only onaccept).
Critical: form mode MUST NOT request sensitive information. Passwords, API keys, OAuth tokens: those go through URL mode.
Elicitation, URL mode
URL mode is the OAuth path. Server tells the client "please send the user to this URL"; the user completes the flow out-of-band (in a browser); the client tells the server when it's done. The MCP client never sees the credentials.
(when (server/client-supports-url-elicitation? context)
(let [request-id (str (random-uuid))]
(-> (server/request-elicitation context
(schema/url-elicitation
{:elicitation-id request-id
:url "https://accounts.example.com/oauth/authorize?client_id=..."
:message "Please sign in to your account to continue"}))
(p/then (fn [response]
(case (:action response)
"accept"
(do
;; user agreed to navigate; OAuth happens out-of-band
;; later, when out-of-band flow completes, notify the client
(server/notify-elicitation-complete context request-id))
("decline" "cancel")
(log "User did not authorize")))))))
The flow:
- Server sends
elicitation/createwithmode: "url", an:elicitation-id, the:url, and a human-readable message. - Client shows the URL to the user; user navigates and completes the flow out-of-band.
- Client returns
:action "accept"once the user agrees to navigate (this is consent, not OAuth completion). - Out-of-band, your OAuth callback fires; you process the tokens; you call
(server/notify-elicitation-complete context elicitation-id)to nudge the client. - Client uses the notification to retry whatever request originally needed the OAuth token.
The schema validates that :url starts with https:// (or http://localhost for dev). HTTP without TLS is rejected.
Tasks (experimental)
Tasks are durable state machines for long-running operations. Instead of returning a result synchronously, the client creates a Task; the server polls (tasks/get) or awaits (tasks/result) until terminal status.
The states (schema/TaskStatus):
| Status | Terminal? | Meaning |
|---|---|---|
pending | no | Task accepted but not yet started |
running | no | Task in progress |
input_required | no | Task is paused waiting for elicitation / sampling |
completed | yes | Task succeeded |
failed | yes | Task failed (error result) |
cancelled | yes | Task was cancelled |
(require '[mcp-toolkit.server :as server]
'[mcp-toolkit.schema :as schema])
;; Server-side: poll a task
(p/let [task (server/request-task-get context "task-abc-123")]
(when (schema/terminal-status? (:status task))
(println "Done:" (:status task))))
;; Server-side: await result
(p/let [result (server/request-task-result context "task-abc-123")]
(handle-completed-task result))
;; Server-side: list all tasks (paginated)
(p/let [{:keys [tasks next-cursor]} (server/request-tasks-list context)]
(doseq [t tasks] (println (:task-id t) (:status t)))
(when next-cursor
;; fetch next page
(server/request-tasks-list context next-cursor)))
;; Server-side: cancel
(server/request-task-cancel context "task-abc-123")
Capability detection has multiple knobs because clients can support pieces independently:
(server/client-supports-tasks? context) ; any task ops
(server/client-supports-task-augmented-sampling? context) ; tasks/sampling/createMessage
(server/client-supports-task-augmented-elicitation? context) ; tasks/elicitation/create
(server/client-supports-tasks-list? context) ; tasks/list
(server/client-supports-tasks-cancel? context) ; tasks/cancel
request-task-cancel and request-tasks-list themselves check the relevant capability and return nil (don't send) when the client doesn't declare it. request-task-get and request-task-result always send (they're considered baseline).
When you (as the server) want to proactively notify the requestor of a status change, use notify-task-status:
(server/notify-task-status context updated-task)
The spec says receivers MAY send this and requestors MUST NOT rely on it, so always poll as a fallback.
schema/Task and schema/TaskStatus validate the shapes if you're constructing tasks server-side.
JSON Schema 2020-12 dialect
The spec adopts JSON Schema 2020-12 explicitly (vs. the earlier ambiguity about which draft). The toolkit ships a constant and a helper:
(require '[mcp-toolkit.schema :as schema])
schema/JSON_SCHEMA_DIALECT
;; => "https://json-schema.org/draft/2020-12/schema"
(schema/with-schema-dialect
{:type "object"
:properties {:name {:type "string"}
:age {:type "integer"
:minimum 0
:maximum 150}}
:required ["name"]})
;; => {:$schema "https://json-schema.org/draft/2020-12/schema"
;; :type "object"
;; :properties {...}
;; :required ["name"]}
Add the :$schema key to your :input-schema / :output-schema if you're targeting strict 2025-11-25 clients. Older clients ignore the field. The toolkit doesn't add it automatically, schema dialect is your call.
Capability detection summary
A 2025-11-25 server typically guards new-feature requests like this:
(let [{:keys [client-capabilities]} @session]
(cond-> {}
(contains? client-capabilities :elicitation)
(assoc :can-prompt-user true)
(get-in client-capabilities [:elicitation :url])
(assoc :can-oauth true)
(contains? client-capabilities :tasks)
(assoc :can-track-long-running true)
(get-in client-capabilities [:sampling :tools])
(assoc :can-let-llm-use-tools true)))
The mcp-toolkit.server namespace ships predicate fns for each, use those rather than hand-rolling lookups, so the capability paths stay in one place.
See also
docs/reference/MIGRATION-2025-11-25.md: the migration writeup with side-by-side before/after.- Schema validation: every schema referenced above is in
mcp-toolkit.schema. - Protocol versions: full version-by-version feature matrix.
- Spec, 2025-11-25