Claude API Inline Tool Definitions: Add Tools Mid-Conversation

TL;DR
Claude API inline tools beta: define, reshape or upgrade a tool mid-conversation with one system message and keep the prompt cache. Code included.
Last updated: October 17, 2026
Since September 22, 2026, the Claude API lets you put a full tool definition inside a mid-conversation role: "system" message: a tool_addition block whose tool is {"type": "tool_definition", "definition": {...}}, sent behind the inline-tools-2026-09-15 beta header. An agent can gain a tool, change a tool's schema, or move a server tool to a newer version partway through a session without touching the tools array, so the cached prefix keeps hitting and only the appended message is processed as new input. The verdict: use it for tools you genuinely cannot know at the first request; anything you know up front still belongs in tools, with defer_loading: true if the model should not see it yet.
This guide covers what shipped, why the position of a tool change matters for caching, the exact request shapes (with TypeScript type-checked against the current SDK), the MCP variant with tool-list pinning, the error codes and limits, and how it compares with the other ways to change tools mid-session.
What changed on September 22#
Three layers now exist, and it helps to keep them apart:
- Mid-conversation system messages. A
role: "system"entry inmessagesthat applies operator-level instructions from that point onward, without editing the top-levelsystemfield. - By-reference tool changes.
tool_additionandtool_removalblocks inside such a message that name a tool already declared intools(tool_reference), an individual MCP tool (mcp_tool_referencewithserver_nameandname), or a whole MCP toolset (mcp_toolset_referencewithserver_name). These shipped first undermid-conversation-tool-changes-2026-07-01. - By-value definitions (new). With
inline-tools-2026-09-15, atool_additionblock can carry the full definition. Withmcp-client-2026-09-15as well, that definition can be an MCP toolset, and responses record each server's fetched tool list in anmcp_tool_listingblock that pins the list when you send it back.
The release notes list the inline definitions entry on September 22, 2026, the same day as Claude Opus 5.5 (see the Opus 5.5 release guide for the model side).
| Beta header | What it unlocks | Where |
|---|---|---|
mid-conversation-tool-changes-2026-07-01 | Add or remove tools by reference only | Claude API, Amazon Bedrock, Google Cloud |
inline-tools-2026-09-15 | By-value definitions, plus everything by-reference | Claude API |
mcp-client-2026-09-15 (with the above) | mcp_toolset as a definition, mcp_tool_listing recording and pinning | Claude API |
Per Anthropic's docs, mid-conversation tool changes are in beta on the same models as mid-conversation system messages: Claude Fable 5.1, Mythos 5.1, Fable 5, Mythos 5, Opus 5.5, Opus 4.8, Opus 5, Sonnet 5.5 and Haiku 5.5. They are not available on Claude Sonnet 5.
Why it matters: the cache prefix order#
Prompt caching hashes the request prefix in a fixed order: tools, then system, then messages. A hit requires the prefix to match a recent request byte for byte up to the breakpoint. The tools array sits at the very start, so any edit to it, even adding one tool at the end, produces a new hash and the whole conversation is reprocessed at full input price.
A mid-conversation system message goes at the end of the history instead. Every earlier byte is unchanged, the existing cache entry still matches, and the next request pays fresh-input price only for the appended message. That is the entire value of this feature, so the cost math in the prompt caching production guide and the break-even numbers in prompt caching economics apply directly: the longer the session, the more an avoided full-prefix miss is worth.
%% title: Where a tool change lands in the cached prefix
flowchart LR
A[tools] --> B[system] --> C[messages 1..n]
C --> D[new system msg<br/>tool_addition]
E[Edit tools array] -.->|invalidates| A
D -->|only new input| F[Cache hit on 1..n]Two caveats from the docs. Caching is opt-in: without cache_control (the top-level automatic field or an explicit breakpoint) nothing is cached and there are no savings to preserve. And very short conversations fall under the minimum cacheable prompt length, so cache_read_input_tokens stays at 0 until the conversation grows. Do not judge the feature on a three-message test.
How to define a tool mid-conversation#
The steps, as documented:
- Send the beta header.
anthropic-beta: inline-tools-2026-09-15, orbetas: ["inline-tools-2026-09-15"]onclient.beta.messages.createin the SDK. - Keep at least one non-deferred tool in
tools. A tool search tool counts. Iftoolshas no non-deferred tool, the request is still accepted, but the first tool you define by value changes the start of the rendered prompt and costs one full cache miss on that request. - Append a system message after a user turn. That includes a user turn carrying
tool_resultblocks. The message holds atool_additionblock whosetoolis atool_definition. - Let the model call it. From that position onward, the model can call the tool exactly as it calls one declared in
tools.
This is the message from Anthropic's docs, defining a db_query tool by value:
{
"role": "system",
"content": [
{
"type": "tool_addition",
"tool": {
"type": "tool_definition",
"definition": {
"name": "db_query",
"description": "Run a read-only SQL query against the analytics database.",
"input_schema": {
"type": "object",
"properties": { "sql": { "type": "string" } },
"required": ["sql"]
}
}
}
}
]
}
The definition is any tools entry: a custom tool, or an Anthropic-defined client or server tool, with its usual configuration including cache_control and defer_loading.
Here is the same flow in TypeScript. get_weather stays in tools as the stable non-deferred tool, and db_query arrives only when the app discovers it (say, the user connects a database):
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const BETAS: Anthropic.Beta.AnthropicBeta[] = ["inline-tools-2026-09-15"];
// One stable, non-deferred tool keeps the start of the rendered prompt fixed.
const tools: Anthropic.Beta.BetaToolUnion[] = [
{
name: "get_weather",
description: "Get the current weather for a location.",
input_schema: {
type: "object",
properties: { location: { type: "string", description: "City name" } },
required: ["location"],
},
},
];
const messages: Anthropic.Beta.BetaMessageParam[] = [
{ role: "user", content: "How many orders shipped yesterday?" },
];
// Your app discovers db_query only now (say, the user connected a database).
// Append it by value instead of editing `tools`.
function defineTool(definition: Anthropic.Beta.BetaToolUnion): Anthropic.Beta.BetaMessageParam {
return {
role: "system",
content: [
{ type: "tool_addition", tool: { type: "tool_definition", definition } },
],
};
}
messages.push(
defineTool({
name: "db_query",
description: "Run a read-only SQL query against the analytics database.",
input_schema: {
type: "object",
properties: { sql: { type: "string" } },
required: ["sql"],
},
}),
);
const response = await client.beta.messages.create({
model: "claude-opus-5-5",
max_tokens: 1024,
betas: BETAS,
cache_control: { type: "ephemeral" },
tools,
messages,
});
for (const block of response.content) {
if (block.type === "tool_use") {
console.log(block.name, JSON.stringify(block.input));
}
}
Examples in this post were type-checked with tsc --noEmit (strict, moduleResolution: bundler) against @anthropic-ai/sdk@0.135.0 and TypeScript 7.0.2; SDK support for inline definitions landed in 0.128.0 on September 22. They were not run against the live API.
%% title: Adding db_query mid-session
sequenceDiagram
participant App
participant API as Claude API
App->>API: tools + history + system(tool_addition db_query)
API-->>App: tool_use db_query
App->>API: tool_result + next turn
API-->>App: answer (prefix read from cache)Resending an identical definition changes nothing, so a client can safely include it again on a retry without worrying about duplicates.
Change a schema or upgrade a server tool version#
To change a tool's schema, or to move an Anthropic server tool to a newer dated version, send a different definition under the same name. The new definition replaces the earlier one from that position onward; earlier turns keep the old one, so history stays byte-identical. Continuing the example, db_query gains a region enum after the user asks for a regional breakdown:
// Answer every tool_use before the next user turn.
async function runQuery(input: unknown): Promise<string> {
return JSON.stringify({ input, rows: [] }); // your database call goes here
}
const toolResults: Anthropic.Beta.BetaContentBlockParam[] = [];
for (const block of response.content) {
if (block.type === "tool_use") {
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: await runQuery(block.input),
});
}
}
messages.push({ role: "assistant", content: response.content });
messages.push({
role: "user",
content: [...toolResults, { type: "text", text: "Break it down by region." }],
});
// Same name, new schema: replaces the earlier definition from here on.
messages.push(
defineTool({
name: "db_query",
description: "Run a read-only SQL query. Set `region` to scope the query.",
input_schema: {
type: "object",
properties: {
sql: { type: "string" },
region: { type: "string", enum: ["us", "eu", "apac"] },
},
required: ["sql"],
},
}),
);
// Withdraw it later with a reference, not a definition.
const removeDbQuery: Anthropic.Beta.BetaMessageParam = {
role: "system",
content: [
{ type: "tool_removal", tool: { type: "tool_reference", name: "db_query" } },
],
};
void removeDbQuery;
The next request sends the same tools, betas and cache_control with the longer messages array. Append removeDbQuery (after a later user turn) only when you want to withdraw the tool.
The rules that apply here:
- Identical resend is a no-op. Only a different definition under the same name replaces anything.
- Same name, different tool type is an error. It returns 400 with
error.details.error_codeset totool_name_conflict. A newer version of the same server tool is not a different type, so version bumps are allowed. - Dated server tools keep their own headers. If a server tool you define by value needs its own beta header, send that header on every later request in the conversation, not just the one that defined it.
- Removal still takes a reference.
tool_removaluses{"type": "tool_reference", "name": "db_query"}, never a definition. A removed tool can be defined or re-offered later with anothertool_addition.
The schema-change case is where inline definitions do something no earlier cache-preserving mechanism could. With by-reference changes you can only toggle tools that were in tools from the first request, so reshaping a schema meant editing tools and paying a full-conversation miss.
Add an MCP server mid-conversation#
If the tool that appears mid-session is a remote MCP server, send both inline-tools-2026-09-15 and mcp-client-2026-09-15. Connection details still go in mcp_servers as usual; the tool_addition definition is an mcp_toolset that names the server. Per the docs, a tool_addition block never holds a server URL or token, and the mcp_toolset object is the same one you would put in tools, including default_config and configs.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const messages: Anthropic.Beta.BetaMessageParam[] = [
{ role: "user", content: "What's on my calendar tomorrow?" },
{
role: "system",
content: [
{
type: "tool_addition",
tool: {
type: "tool_definition",
definition: { type: "mcp_toolset", mcp_server_name: "calendar" },
},
},
],
},
];
const response = await client.beta.messages.create({
model: "claude-opus-5-5",
max_tokens: 1024,
betas: ["inline-tools-2026-09-15", "mcp-client-2026-09-15"],
mcp_servers: [
{
type: "url",
url: "https://mcp.example.com/calendar",
name: "calendar",
authorization_token: process.env.CALENDAR_TOKEN,
},
],
tools: [
{
name: "get_weather",
description: "Get the current weather for a location.",
input_schema: {
type: "object",
properties: { location: { type: "string" } },
required: ["location"],
},
},
],
messages,
});
// The response can start with mcp_tool_listing blocks. Never read content[0].
for (const block of response.content) {
if (block.type === "mcp_tool_listing") {
console.log("listed", block.mcp_server_name, block.tools.map((t) => t.name));
} else if (block.type === "text") {
console.log(block.text);
}
}
// Send the assistant turn back unchanged so the recorded listing pins the tool list.
messages.push({ role: "assistant", content: response.content });
// Or pin explicitly: copy a listing into the toolset's `tools` field.
const listing = response.content.find(
(b): b is Anthropic.Beta.BetaMCPToolListingBlock => b.type === "mcp_tool_listing",
);
if (listing) {
const pinned: Anthropic.Beta.BetaMCPToolset = {
type: "mcp_toolset",
mcp_server_name: "calendar",
tools: listing.tools,
};
console.log(pinned.tools?.length);
}
The token comes from an environment variable here; never hardcode it in source.
When the API fetches a server's tool list for a response, that response starts with an mcp_tool_listing block, one per server fetched. Three practical consequences:
- Never read
content[0]blindly. Iterate and branch onblock.type, as above. Code that assumes the first block is text will break the first time a listing appears. - Send the assistant turn back unchanged, listing included, and keep sending
mcp-client-2026-09-15on every request that carries it. Later requests use the recorded list instead of asking the server again, so the tool set the model sees stays stable for the session even if the server changes its tools. - Or pin it yourself. Copy the listing's
toolsinto themcp_toolset'stoolsfield. The API then does not ask the server, and per the MCP connector docs the response has nomcp_tool_listingblock.
%% title: MCP tool list recording and pinning
sequenceDiagram
participant App
participant API as Claude API
participant MCP as MCP server
App->>API: system(tool_addition mcp_toolset)
API->>MCP: list tools
API-->>App: mcp_tool_listing + reply
App->>API: send assistant turn back unchanged
Note over API,MCP: later requests reuse the recorded listmcp-client-2026-09-15 includes everything mcp-client-2025-11-20 does, so send it in place of the older header rather than both. The connector's existing limits still apply: it supports only tool calls from the MCP spec, and servers must be publicly reachable over HTTP (Streamable HTTP or SSE), so a local STDIO server cannot be connected directly. For background on server types and transports, see the complete guide to MCP servers.
Gotchas and limits#
Placement. A system message carrying text, tool_addition or tool_removal must immediately follow a user turn (including one with tool_result blocks) or an assistant turn ending in a server tool result, and must either end the messages array or be followed by an assistant turn. It cannot be the first message and cannot sit between a tool_use and its tool_result. Anything else is a 400.
Paused turns. tool_addition and tool_removal are not accepted right after a paused assistant turn (one ending in a server tool result), although text is. Resume the paused turn first, then send the tool change in the next system message.
Turn-scoped messages are text-only. A system message with clear_at: "next_user_message" returns 400 if it carries tool blocks. Use a separate system message without clear_at for tool changes.
Never edit or delete a sent system message. It invalidates the cache from that point, and on Fable 5.1, Opus 5.5, Sonnet 5.5 and Haiku 5.5 it also invalidates thinking blocks in every later assistant turn. To change course, append a new system message (a later definition or a tool_removal).
cache_control placement. Put it on the tool_addition block or inside the definition, not both. It counts toward the request's breakpoint limit, and a deferred definition (defer_loading: true) cannot carry it.
Unsupported tool types. During the beta some tool types, including the computer use tool, cannot be defined in a message and return a 400 that says so. Declare those in tools and add them by reference.
Empty tools edge case. No non-deferred tool in tools means one full cache miss on the request that first defines a tool by value. Keep one stable tool there.
Hard limits. Any of these returns 400 with error_code available_tools_limit_exceeded:
| Limit | Value |
|---|---|
| Deferred tools available after any message | 10,000 |
| Tools defined after the first user message that are still available | 10,000 |
| Size of definitions sent after the first user message that are still available | 4 MB (4,194,304 bytes) |
| Rendered tool text | 4 MB (4,194,304 bytes) |
Unresolved references. A by-reference tool_addition naming a tool that is not declared in tools returns 400 with error_code tool_reference_unresolved. If the tool really is new, define it by value instead.
Platform scope. By-value definitions are Claude API only. On Amazon Bedrock and Google Cloud, the documented path is by-reference changes under mid-conversation-tool-changes-2026-07-01, so a multi-cloud agent needs the declare-up-front pattern as its common denominator.
System content is operator authority. Claude treats system messages as instructions from you, the operator. Tool definitions you place there, including their descriptions, are read with that weight, so never paste a third-party tool description or retrieved text into a system message unreviewed. The auditing checklist in tool use production patterns applies to anything you define inline.
Compaction. If you also use compaction, the TypeScript SDK types show compaction blocks carrying a tool_changes array of tool_addition and tool_removal entries; send those back unchanged with the block.
Inline definitions vs the alternatives#
There are four documented ways to change what tools the model has mid-session. Each has a job it does well.
Edit tools directly. The simplest option and it works on every platform and model, with no beta header. The tradeoff is a full-conversation cache miss on every change, which is fine for short sessions and expensive for long agent loops.
Declare up front, defer_loading, then add by reference. Anthropic's recommended path for any tool you know at the first request: put it in tools with defer_loading: true and surface it with a tool_reference when relevant. It preserves the cache and works on the Claude API, Bedrock and Google Cloud. It cannot introduce a tool you did not know about at request one, and it cannot change a schema.
Tool search tool. With tool_search_tool_regex_20251119 or tool_search_tool_bm25_20251119, Claude searches your own catalog of deferred tools and loads only what it needs. You still provide every definition in tools, with at least one non-deferred. Best for very large catalogs you already know.
Inline by-value definitions. For tools discovered or reshaped mid-session: a user connects a data source, a plugin is installed, a server tool moves to a newer version, an MCP server becomes available. Claude API only, in beta.
Edit tools | Up front + defer_loading + reference | Tool search tool | Inline definition | |
|---|---|---|---|---|
| Handles a tool unknown at first request? | Yes | No | No | Yes |
| Change a schema mid-session? | Yes | No | No | Yes |
| Cache impact of a change | Full-conversation miss | Only the appended message | Catalog stays fixed in tools | Only the appended message |
| Platforms | All | Claude API, Bedrock, Google Cloud | See tool search docs | Claude API |
| Header | None | mid-conversation-tool-changes-2026-07-01 or inline-tools-2026-09-15 | Tool type version | inline-tools-2026-09-15 |
These compose. A realistic agent declares its core tools and a deferred catalog up front, uses a tool search tool if the catalog is large, and reserves inline definitions for what the user brings mid-session.
%% title: Which tool-change mechanism to use
flowchart TD
A{Known at first request?} -->|Yes| B{Hundreds of tools?}
B -->|Yes| C[Tool search tool + defer_loading]
B -->|No| D[tools + defer_loading + reference]
A -->|No| E{MCP server?}
E -->|Yes| F[mcp_toolset definition + both headers]
E -->|No| G[tool_definition by value]FAQ#
Does adding a tool mid-conversation break the prompt cache?#
No, provided caching is enabled with cache_control and tools contains at least one non-deferred tool. The tools array and earlier messages stay byte-identical, so only the appended system message is processed as new input. With no non-deferred tool in tools, the first by-value definition costs one full cache miss.
Which models and platforms support inline tool definitions?#
Inline (by-value) definitions are in beta on the Claude API with inline-tools-2026-09-15. Mid-conversation tool changes are documented for Claude Fable 5.1, Mythos 5.1, Fable 5, Mythos 5, Opus 5.5, Opus 4.8, Opus 5, Sonnet 5.5 and Haiku 5.5, and are not available on Claude Sonnet 5. Bedrock and Google Cloud get by-reference changes only.
Can I change a tool's schema without restarting the conversation?#
Yes. Send a new definition under the same name in a later system message; it replaces the earlier one from that position onward. Reusing the name for a different type of tool returns 400 with tool_name_conflict, but a newer version of the same server tool is allowed.
Do I still need mid-conversation-tool-changes-2026-07-01?#
Not on the Claude API: inline-tools-2026-09-15 also covers adding and removing tools by reference, so you do not need to send both. Keep the older header where you run on Amazon Bedrock or Google Cloud, which support by-reference changes but not by-value definitions.
Why does my MCP response start with an mcp_tool_listing block?#
With mcp-client-2026-09-15, a response for which the API fetched a server's tool list starts with one mcp_tool_listing block per server. Skip it when looking for text, send the assistant turn back unchanged so later requests reuse the recorded list, or copy its tools into the mcp_toolset to pin the list yourself.
Sources#
- Anthropic docs: Define tools in a message (beta)
- Anthropic docs: Add an MCP server mid-conversation (beta)
- Anthropic docs: Mid-conversation tool changes and limitations
- Anthropic docs: Pin an MCP server's tool list
- Claude Platform release notes (September 22, 2026)
- Anthropic docs: Tool search tool
- Anthropic docs: Prompt caching
- Anthropic TypeScript SDK changelog (0.128.0)
Continue Reading#
- Tool Use in the Claude API: Production Patterns - schemas, tool loops and auditing imported definitions, the foundation this feature builds on.
- Prompt Caching in the Claude API: A Production Guide - breakpoints, cache reads and how to confirm the hits this feature is meant to protect.
- Claude Effort Levels Explained - per-message effort is the sibling mid-conversation system message feature, with the same placement rules.
Get the next deep dive like this in your inbox
One email a week on Claude and the rest of the AI dev stack. Free.
Read next on Claude Code
Tool Use in the Claude API: Production Patterns for Reliable Agents
Master tool use in the Claude API. Schema design, retry logic, multi-step loops, and the failure modes that only show up at 10k calls a day.
12 min readPrompt Caching in the Claude API: A Production Guide
Cut Claude API spend by up to 90% with prompt caching. Real numbers, TypeScript SDK code, and the gotchas Anthropic's docs gloss over.
11 min readClaude Opus 5.5 Developer Guide: Pricing, API, Claude Code
Claude Opus 5.5 costs $4/$20 per million tokens with $0.20 cache reads. SDK examples, Claude Code setup, and when to pick it over Sonnet 5.5 or Haiku 5.5.
11 min readTechnical content at the intersection of AI and development. Building with AI agents, Claude Code, and modern dev tools - then showing you exactly how it works.








