How to Test Claude API Function Calls Locally
How to test Claude API function calls locally
Testing Claude's function calling (tool use) locally means simulating the full loop — sending a request with tool definitions, inspecting the tool_use block Claude returns, running your actual function against that input, and feeding the result back as a tool_result — without deploying anything. You can do this entirely from your terminal or a local script using curl, Node.js, or Python, as long as you have valid API credentials and a way to inspect raw request/response JSON.
The short answer: write a small local script that defines your tools, calls the Messages API, checks the stop_reason for tool_use, executes the matching local function with the arguments Claude provided, and sends the result back in a follow-up request. Below is exactly how to set that up, plus the debugging techniques that catch most function-calling bugs before they reach production.
Why local testing matters for tool use
Function calling bugs are rarely about the model — they're about schema mismatches, malformed JSON arguments, or your execution code not handling edge cases Claude legitimately produces (empty strings, missing optional fields, wrong types if your schema is loose). Catching these locally, with fast iteration and full visibility into the raw payloads, is much cheaper than debugging them in a deployed service with logs scattered across infrastructure.
Set up a minimal local test harness
You don't need a server. A single script that performs the request-execute-respond loop is enough.
1. Define your tool schema precisely
{
"name": "get_weather",
"description": "Get current weather for a city",
"input_schema": {
"type": "object",
"properties": {
"city": { "type": "string" },
"unit": { "type": "string", "enum": ["celsius", "fahrenheit"] }
},
"required": ["city"]
}
}
Loose schemas are the number one source of bugs. If unit isn't constrained with enum, you'll eventually get values your code doesn't expect.
2. Send the initial request and inspect stop_reason
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "claude-sonnet-4-5",
"max_tokens": 1024,
"tools": [{"name":"get_weather","description":"Get current weather","input_schema":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}],
"messages": [{"role":"user","content":"What is the weather in Lisbon?"}]
}'
Look at the response's stop_reason. If it's tool_use, Claude is asking you to call a function — it has not called anything itself. This is the part developers new to tool use misunderstand most often: Claude never executes code; it returns structured input for you to execute locally.
3. Extract the tool call and run your function
const response = await callClaude(messages, tools);
const toolUseBlock = response.content.find(b => b.type === "tool_use");
if (toolUseBlock) {
const { name, input, id } = toolUseBlock;
const result = await localFunctions[name](input); // your real logic
messages.push({ role: "assistant", content: response.content });
messages.push({
role: "user",
content: [{
type: "tool_result",
tool_use_id: id,
content: JSON.stringify(result)
}]
});
const followUp = await callClaude(messages, tools);
console.log(followUp.content);
}
Run this with node test-tools.js and you have a full local loop with no deployment required.
Techniques that catch bugs faster
Log the raw JSON, not a formatted summary. Print JSON.stringify(response, null, 2) before doing anything else. Malformed arguments, unexpected nesting, or missing fields are obvious in raw JSON and easy to miss in a pretty-printed abstraction.
Test with adversarial prompts, not just the happy path. Ask ambiguous questions ("what's the weather there?" with no prior context) and see whether Claude asks a clarifying question instead of calling the tool with a guessed value. Your harness should handle both stop_reason: "tool_use" and stop_reason: "end_turn" gracefully.
Mock your function first, then wire in the real one. Start with a stub that just echoes back fixed JSON. This isolates whether a bug is in the tool-calling loop or in your actual business logic.
Use a fixtures file for multi-turn conversations. Save real messages arrays from previous runs to disk as JSON and replay them when testing. This lets you reproduce a specific tool-calling bug without re-running the whole conversation from scratch every time.
Validate input against your JSON schema before executing. Even with a strict schema, validate the input object defensively — use ajv (Node) or jsonschema (Python) — and log a clear error if it fails, rather than letting your function throw an opaque exception.
When you want this behind a simple HTTP endpoint
If you're testing locally but eventually want your app (or a teammate, or a frontend) to call Claude's tool-use flow over plain HTTPS rather than managing raw Anthropic credentials and SDKs everywhere, SubToAPI turns your existing Claude access into an API with its own sub_live_... keys. Tool use, streaming, and usage metadata work the same way as the examples above — you send tools and inspect tool_use blocks — just against api.subtoapi.app instead of managing keys per environment. See the tool use docs and quickstart for the exact request shape, or check pricing if you're testing across a team with shared seats.
Either way, the local loop — request, inspect tool_use, execute, respond — is identical. Building that script once and keeping it in your repo as a debugging tool pays off every time you add or change a tool definition.
Questions
Does Claude actually execute my functions during testing? No. Claude only returns a tool_use block describing which function to call and with what arguments. Your local script or application code is responsible for executing the real function and sending the result back as a tool_result.
Can I test tool use without a live API key? You need valid credentials to get real model behavior, but you can reduce calls by mocking the function execution step and reusing saved conversation fixtures to replay scenarios without resending the initial request every time.
What's the fastest way to debug a malformed tool argument? Log the full raw JSON response before parsing it, and validate the input object against your JSON schema with a library like ajv. Most issues trace back to a schema that's too loose (missing enum, required, or type constraints).