ProvenPin Install

Anthropic TypeScript SDK tool use loop, tested with a mocked fetch: working versions (tested 2026-10-15)

Run a Claude tool_use loop with the official Anthropic SDK and Zod-validated tool inputs, tested offline with an injected fetch.

passed2 passed · 0 failed2026-10-15 11:51 UTC
Status
passed
Last passed
2026-10-15 11:51 UTC
Tests
2 / 2
Language
TypeScript
Runtime
node v22.23.3
pinned versions4
@anthropic-ai/sdk@0.134.0zod@4.6.7tsx@4.23.15@types/node@24.19.2
run it
npm install --no-audit --no-fund --ignore-scripts=false
npm test
test environment
node v22.23.3
linux 6.17.0-1022-azure
x64
runner: github-actions 38730348195

The code

Exactly the files that passed. Your agent can pull them with get_recipe("anthropic-sdk-mocked-tool-use") (install), or see them on GitHub.

package.json
{
  "name": "anthropic-sdk-mocked-tool-use",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "node --import tsx --test test/*.test.ts"
  },
  "dependencies": {
    "@anthropic-ai/sdk": "0.134.0",
    "zod": "4.6.7"
  },
  "devDependencies": {
    "tsx": "4.23.15",
    "@types/node": "24.19.2"
  }
}
src/agent.ts
import Anthropic from "@anthropic-ai/sdk";
import { z } from "zod";

const LookupInput = z.object({ sku: z.string().regex(/^[A-Z]+-\d+$/) });
export const tools: Anthropic.Tool[] = [{
  name: "lookup_stock",
  description: "Units in stock for a SKU",
  input_schema: { type: "object", properties: { sku: { type: "string" } }, required: ["sku"] },
}];

export async function ask(client: Anthropic, question: string, stock: (sku: string) => number) {
  const messages: Anthropic.MessageParam[] = [{ role: "user", content: question }];
  for (let i = 0; i < 5; i++) {
    const res = await client.messages.create({ model: "claude-sonnet-4-5", max_tokens: 512, messages, tools });
    messages.push({ role: "assistant", content: res.content });
    if (res.stop_reason !== "tool_use") return res.content.filter((b) => b.type === "text").map((b) => b.text).join("");
    const results: Anthropic.ToolResultBlockParam[] = [];
    for (const block of res.content) {
      if (block.type !== "tool_use") continue;
      const p = LookupInput.safeParse(block.input);
      results.push(p.success
        ? { type: "tool_result", tool_use_id: block.id, content: String(stock(p.data.sku)) }
        : { type: "tool_result", tool_use_id: block.id, content: "invalid sku", is_error: true });
    }
    messages.push({ role: "user", content: results });
  }
  throw new Error("too many turns");
}
test/agent.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import Anthropic from "@anthropic-ai/sdk";
import { ask } from "../src/agent.ts";

const msg = (content: object[], stop_reason: string) => ({ id: "m", type: "message", role: "assistant", model: "claude-sonnet-4-5", content, stop_reason, stop_sequence: null, usage: { input_tokens: 1, output_tokens: 1 } });

function fake(replies: object[], seen: any[]) {
  const fetch = async (_u: any, init: any) => {
    seen.push(JSON.parse(init.body));
    return new Response(JSON.stringify(replies.shift()), { status: 200, headers: { "content-type": "application/json" } });
  };
  return new Anthropic({ apiKey: "test", baseURL: "http://mock.local", fetch: fetch as any, maxRetries: 0 });
}

test("runs the tool and returns the final text", async () => {
  const seen: any[] = [];
  const c = fake([
    msg([{ type: "tool_use", id: "tu1", name: "lookup_stock", input: { sku: "MUG-1" } }], "tool_use"),
    msg([{ type: "text", text: "There are 7 mugs." }], "end_turn"),
  ], seen);
  assert.equal(await ask(c, "How many mugs?", () => 7), "There are 7 mugs.");
  assert.deepEqual(seen[1].messages[2].content[0], { type: "tool_result", tool_use_id: "tu1", content: "7" });
});

test("invalid tool input is returned as is_error", async () => {
  const seen: any[] = [];
  const c = fake([
    msg([{ type: "tool_use", id: "tu1", name: "lookup_stock", input: { sku: "bad" } }], "tool_use"),
    msg([{ type: "text", text: "Need a valid SKU." }], "end_turn"),
  ], seen);
  await ask(c, "?", () => 0);
  assert.equal(seen[1].messages[2].content[0].is_error, true);
});

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.