Tool calling

Tools let Claude request that your application execute a function and report its result back. Each custom tool is described by a name, a description, and an input schema. See JSON Schemas: structured outputs & tools for schema background — Claude’s custom tools use their own small schema model (ToolInputSchema/PropertySchema), or accept raw JSON Schema via Tool.CustomRaw for schemas those types can’t express.

Custom tools

Define your own tools that Claude calls and your application executes:

//> using dep com.softwaremill.sttp.ai::claude:0.11.0

import sttp.ai.claude.models.{ClaudeModel, ContentBlock, Message, PropertySchema, Tool, ToolInputSchema}
import sttp.ai.claude.requests.MessageRequest

val weatherTool = Tool(
  name = "get_weather",
  description = "Get current weather for a location",
  inputSchema = ToolInputSchema(
    `type` = "object",
    properties = Map(
      "location" -> PropertySchema(`type` = "string", description = Some("City name")),
      "unit" -> PropertySchema(`type` = "string", `enum` = Some(List("celsius", "fahrenheit")))
    ),
    required = Some(List("location"))
  )
)

val request = MessageRequest.withTools(
  model = ClaudeModel.ClaudeSonnet5.value,
  messages = List(Message.user(List(ContentBlock.text("What's the weather in Paris?")))),
  maxTokens = 1000,
  tools = List(weatherTool)
)

Answering tool calls

When Claude decides to call a tool, the response contains ContentBlock.ToolUse blocks. Execute the tool yourself, then send the result back as a Message.toolResult (a user-role message with a ContentBlock.ToolResult), together with the conversation so far:

//> using dep com.softwaremill.sttp.ai::claude:0.11.0

import sttp.ai.claude.ClaudeSyncClient
import sttp.ai.claude.models.{ClaudeModel, ContentBlock, Message, PropertySchema, Tool, ToolInputSchema}
import sttp.ai.claude.requests.MessageRequest

object ToolRoundTrip:
  def main(args: Array[String]): Unit =
    val claude = ClaudeSyncClient.fromEnv
    try {
      val weatherTool = Tool(
        name = "get_weather",
        description = "Get current weather for a location",
        inputSchema = ToolInputSchema(
          `type` = "object",
          properties = Map("location" -> PropertySchema(`type` = "string", description = Some("City name"))),
          required = Some(List("location"))
        )
      )

      val question = Message.user(List(ContentBlock.text("What's the weather in Paris?")))

      val first = claude.createMessage(
        MessageRequest.withTools(
          model = ClaudeModel.ClaudeHaiku4_5.value,
          messages = List(question),
          maxTokens = 1000,
          tools = List(weatherTool)
        )
      )

      val toolUses = first.content.collect { case tu: ContentBlock.ToolUse => tu }

      val toolResults = toolUses.map { tu =>
        // ... execute the tool yourself, e.g. look up the weather for tu.input("location")
        Message.toolResult(tu.id, "22C, sunny")
      }

      val second = claude.createMessage(
        MessageRequest.withTools(
          model = ClaudeModel.ClaudeHaiku4_5.value,
          messages = List(question, Message.assistant(first.content)) ++ toolResults,
          maxTokens = 1000,
          tools = List(weatherTool)
        )
      )

      second.content.foreach {
        case t: ContentBlock.Text => println(t.text)
        case _                    => ()
      }
    } finally claude.close()

Predefined tools

Currently supported:

  • Tool.WebSearch (web_search_20250305)

//> using dep com.softwaremill.sttp.ai::claude:0.11.0

import sttp.ai.claude.ClaudeSyncClient
import sttp.ai.claude.models.{ClaudeModel, ContentBlock, Message, Tool}
import sttp.ai.claude.requests.MessageRequest

object WebSearchExample:
  def main(args: Array[String]): Unit =
    val claude = ClaudeSyncClient.fromEnv
    try {
      val request = MessageRequest.withTools(
        model = ClaudeModel.ClaudeSonnet5.value,
        messages = List(Message.user(List(ContentBlock.text("What was the most recent SpaceX launch?")))),
        maxTokens = 1024,
        tools = List(Tool.WebSearch.default)
      )

      val response = claude.createMessage(request)

      response.content.foreach {
        case t: ContentBlock.Text => println(t.text)
        case s: ContentBlock.ServerToolUse =>
          println(s"Searched for: ${s.input.get("query").flatMap(_.asString).getOrElse("")}")
        case r: ContentBlock.WebSearchToolResult =>
          r.content match {
            case ContentBlock.WebSearchToolResultBlock.Results(items) =>
              items.foreach(it => println(s"- ${it.title}${it.url}"))
            case ContentBlock.WebSearchToolResultBlock.Error(code) =>
              println(s"Web search failed: $code")
          }
        case _ => ()
      }
    } finally claude.close()

Both custom and predefined tools can be passed in the same tools list.

Using the agent loop

For a full automatic tool-calling loop — the model calls a tool, your code runs it, the result is fed back, repeat until the model produces a final answer — use ClaudeAgent with type-safe AgentTools instead of driving createMessage by hand: see the agent loop.