This guide explains how to create tools for ClojureMCP using simple Clojure maps, without requiring ClojureMCP as a library dependency. This approach allows you to create standalone tools that can be registered with any ClojureMCP server.
A tool is defined as a Clojure map with the following structure:
{:name "tool-name" ; String: unique identifier for the tool
:description "Tool description" ; String: human-readable description
:schema {...} ; Map: JSON Schema defining input parameters
:tool-fn (fn [exchange params callback] ...)} ; Function: tool implementationThe :schema field should be a Clojure map that represents a JSON Schema. This defines the structure and validation rules for the tool's input parameters:
{:type "object"
:properties {"param1" {:type "string"
:description "Description of param1"}
"param2" {:type "integer"
:description "Description of param2"}}
:required ["param1"]} ; List of required parameter namesThe :tool-fn has the following signature:
(fn [exchange params callback]
;; Tool implementation
)Where:
exchange- Typically ignored for simple tools (can be used for advanced features)params- A Clojure map with string keys containing the tool's input parameterscallback- A function to call with the results
The callback function expects two arguments:
(callback result-vector error-boolean)Where:
result-vector- A vector of strings containing the tool's outputerror-boolean-trueif an error occurred,falseotherwise
Here's a complete example of a weather tool that doesn't depend on ClojureMCP:
(ns weather-tool.core
(:require [clojure.string :as str]))
(defn fetch-weather
"Mock function to fetch weather data. In a real implementation,
this would call a weather API."
[location]
;; Simulate weather data
(case (str/lower-case location)
"london" {:temp 15 :condition "Partly cloudy" :humidity 65}
"new york" {:temp 22 :condition "Sunny" :humidity 45}
"tokyo" {:temp 18 :condition "Rainy" :humidity 80}
"sydney" {:temp 25 :condition "Clear" :humidity 55}
;; Default for unknown locations
{:temp 20 :condition "Unknown" :humidity 50}))
(defn format-weather-report
"Format weather data into a readable string."
[{:keys [temp condition humidity]} location]
(format "Weather in %s: %d°C, %s, Humidity: %d%%"
location temp condition humidity))
(def weather-tool
{:name "get_weather"
:description "Get the current weather for a specified location"
:schema {:type "object"
:properties {"location" {:type "string"
:description "The city or location to get weather for"}}
:required ["location"]}
:tool-fn (fn [_exchange params callback]
(try
;; Extract location from params (string keys)
(let [location (get params "location")]
(if location
;; Fetch and format weather data
(let [weather-data (fetch-weather location)
report (format-weather-report weather-data location)]
;; Success: return result vector with false error flag
(callback [report] false))
;; Error: missing required parameter
(callback ["Error: Location parameter is required"] true)))
;; Handle any exceptions
(catch Exception e
(callback [(str "Error fetching weather: " (.getMessage e))] true))))})
;; Example of registering the tool with a ClojureMCP server
;; (This would be done by the server, not the tool library)
#_
(defn register-weather-tool [mcp-server]
(clojure-mcp.core/add-tool mcp-server weather-tool))Here's a more complex example with multiple parameters and validation:
(ns calculator-tool.core)
(defn safe-divide [a b]
(if (zero? b)
(throw (ex-info "Division by zero" {:a a :b b}))
(/ a b)))
(def calculator-tool
{:name "calculator"
:description "Perform basic arithmetic operations"
:schema {:type "object"
:properties {"operation" {:type "string"
:enum ["add" "subtract" "multiply" "divide"]
:description "The arithmetic operation to perform"}
"a" {:type "number"
:description "First operand"}
"b" {:type "number"
:description "Second operand"}}
:required ["operation" "a" "b"]}
:tool-fn (fn [_exchange params callback]
(try
;; Extract parameters
(let [operation (get params "operation")
a (get params "a")
b (get params "b")]
;; Validate all parameters are present
(cond
(nil? operation)
(callback ["Error: operation parameter is required"] true)
(nil? a)
(callback ["Error: parameter 'a' is required"] true)
(nil? b)
(callback ["Error: parameter 'b' is required"] true)
:else
;; Perform the calculation
(let [result (case operation
"add" (+ a b)
"subtract" (- a b)
"multiply" (* a b)
"divide" (safe-divide a b)
(throw (ex-info "Invalid operation"
{:operation operation})))]
(callback [(str "Result: " result)] false))))
;; Handle exceptions
(catch Exception e
(callback [(str "Calculation error: " (.getMessage e))] true))))})-
Parameter Validation: Always validate that required parameters are present before processing.
-
Error Handling: Wrap your tool logic in try-catch blocks and use the error flag appropriately.
-
Result Format: Always return results as a vector of strings, even for single-line outputs.
-
String Keys: Remember that
paramsuses string keys, not keywords. -
Pure Functions: Keep your tool logic in pure functions that can be tested independently of the MCP framework.
-
Documentation: Provide clear descriptions for both the tool and its parameters.
You can test your tool function independently:
(defn test-weather-tool []
;; Create a test callback that prints results
(let [test-callback (fn [result error?]
(println "Result:" result)
(println "Error?" error?))]
;; Test with valid input
((:tool-fn weather-tool) nil {"location" "London"} test-callback)
;; Test with missing input
((:tool-fn weather-tool) nil {} test-callback)))To use your tool with a ClojureMCP server, the server administrator would simply add your tool map to their server configuration:
;; In the server's code (not your tool library)
(require '[clojure-mcp.core :as mcp])
(require '[weather-tool.core :as weather])
(defn setup-server [mcp-server]
;; Register your tool
(mcp/add-tool mcp-server weather/weather-tool))This separation allows tool authors to create and distribute tools without any dependency on ClojureMCP, while server operators can easily integrate any compatible tools.