Tools & MCP
One derives clause serves three purposes: extraction, typed tool dispatch, and MCP export.
Defining tools
A tool is a named, described, schema-backed case class (typecast-tools):
import typecast.*
import typecast.tools.*
@desc("Look up the current weather for a city")
case class GetWeather(
@desc("IATA city code") city: String,
days: Int = 1 // defaults make arguments optional on the wire
) derives LlmTool
@desc("Book a meeting on the calendar")
case class BookMeeting(
attendees: List[String],
@desc("duration in minutes; must be positive") minutes: Int
) derives LlmTool
- The wire-visible tool name is the kebab-cased type name (
GetWeather→"get-weather"); override with@toolName("..."). - The type-level
@descis the tool description — prompt material, strongly recommended. - Argument schemas ride entirely on
LlmSchemaderivation; an existinggiven LlmSchemain scope (e.g. one carrying semantic validators) is reused.
Registering handlers and running
val tools = Tools(
handle[GetWeather](w => ToolOutcome.Reply(weather.lookup(w.city, w.days))),
handle[BookMeeting](m => ToolOutcome.Reply(calendar.book(m)))
.validated("positive-minutes") { m =>
if m.minutes > 0 then Vector.empty
else Vector(FieldError(JsonPath.root / "minutes", s"${m.minutes} is not a positive duration"))
}
)
val tc = TypeCast(transport, model = "claude-haiku-4-5")
val outcome = tc.withTools(tools).run(userMessage) // Either[ExtractionError, ToolRun]
Gotcha:
withToolsis an extension method — it requiresimport typecast.tools.*(typecast-coreis the frozen base; the tools surface lives in its own module).
run is deliberately single-step: one tool selection (with retries as configured) + exactly one
handler dispatch. Handlers never run on failed attempts — only on a fully validated terminal
success. runWithReport returns the same ExtractionReport as any extraction. Multi-turn agent
loops are out of scope for v1; compose run yourself (or use ToolRunner.runOnce for a
one-shot functional shape).
A ToolRun carries the decoded call and what the handler returned; recover the static argument
type by matching:
run.call.tool // "get-weather"
run.call.args match
case w: GetWeather => ...
case m: BookMeeting => ...
run.outcome // ToolOutcome.Reply(text) | ToolOutcome.Data(js)
Design rationale: a tool call is just structured output
TypeCast has no native tool-call protocol. A registry of N tools compiles to one
discriminated union schema — SUnion tagged by "tool", one variant per tool's argument object
— and the model's reply travels through the exact same pipeline as any extraction: tolerant
parse → schema-aligned repair → validation → strict decode → targeted retry.
That buys, with zero new machinery:
- code fences / trailing commas / surrounding prose repaired at parse time,
- fuzzy key and enum matching, unambiguous coercions, fuzzy tool-name resolution, and
discriminator inference when the model omits
"tool"but the argument shape uniquely identifies one variant, - unknown tools and malformed arguments reported as JSONPath-addressed errors that drive targeted retries.
Malformed tool arguments — a top agent-reliability complaint — get full repair/retry parity with extractions. Provider-native tool-use protocols are a documented future enhancement.
v1 limits: tool names must be unique (duplicates rejected at construction); argument types with
colliding simple names share the definitions namespace (same caveat as LlmSchema).
MCP export
typecast-mcp exports a registry as a Model Context Protocol server in one line:
import typecast.mcp.McpServer
McpServer.from(tools).serveStdio() // blocks; serves newline-delimited JSON-RPC on stdin/stdout
McpServer.from(tools, name = "support-tools", version = "1.2.0") customizes the serverInfo;
serve(in, out) targets arbitrary streams (that overload is what the test suite drives).
Scope: the stable core of MCP revision 2025-06-18 over stdio — initialize, ping,
tools/list, tools/call — zero new dependencies, single-threaded, sequential. tools/call
arguments go through the same parse → align → validate → decode path as everything else, so MCP
clients get identical alignment and error rendering. Tool-execution failures return MCP's
isError: true result (not a protocol error), per spec.
Plugging into Claude Code
Package a tiny main that builds your registry and serves stdio:
then register it in your project's .mcp.json (or via claude mcp add):
{
"mcpServers": {
"support-tools": {
"command": "java",
"args": ["-jar", "support-tools-mcp.jar"]
}
}
}
Claude Code will call tools/list to discover the derived schemas (names, @desc descriptions,
argument types) and dispatch tools/call straight into your typed handlers.