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 on accept).

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:

  1. Server sends elicitation/create with mode: "url", an :elicitation-id, the :url, and a human-readable message.
  2. Client shows the URL to the user; user navigates and completes the flow out-of-band.
  3. Client returns :action "accept" once the user agrees to navigate (this is consent, not OAuth completion).
  4. Out-of-band, your OAuth callback fires; you process the tokens; you call (server/notify-elicitation-complete context elicitation-id) to nudge the client.
  5. 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):

StatusTerminal?Meaning
pendingnoTask accepted but not yet started
runningnoTask in progress
input_requirednoTask is paused waiting for elicitation / sampling
completedyesTask succeeded
failedyesTask failed (error result)
cancelledyesTask 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