Friday, 7 August 2026

Streaming responses and processing



Here's a concrete example. Let's say the user asks:

"What is the weather in Mumbai and convert 1000 INR to USD?"

Claude will stream back two tool calls. Here's exactly how the events flow:


The complete stream, event by event:

1. message_start

json
{
  "type": "message_start",
  "message": {
    "id": "msg_01XY",
    "type": "message",
    "role": "assistant",
    "content": [],
    "usage": { "input_tokens": 245, "output_tokens": 0 }
  }
}

What your handler does: Message is beginning. Initialize an empty content[] array to collect blocks into.


2. content_block_start — first tool call opens

json
{
  "type": "content_block_start",
  "index": 0,
  "content_block": {
    "type": "tool_use",
    "id": "tool_01ABC",
    "name": "get_weather",
    "input": {}
  }
}

What your handler does: A tool_use block is opening at index 0. You know the tool name (get_weather) and its id, but input is empty. Create a slot at content[0] for this block.


3. content_block_delta — input JSON arrives in fragments

json
{ "type": "content_block_delta", "index": 0,
  "delta": { "type": "input_json_delta", "partial_json": "{\"ci" } }
json
{ "type": "content_block_delta", "index": 0,
  "delta": { "type": "input_json_delta", "partial_json": "ty\": " } }
json
{ "type": "content_block_delta", "index": 0,
  "delta": { "type": "input_json_delta", "partial_json": "\"Mumbai\"}" } }

What your handler does: Keep appending each fragment to content[0]'s input buffer. After all three arrive the buffer holds {"city": "Mumbai"} — but don't parse yet, wait for the block to close.


4. content_block_stop — first tool call complete

json
{
  "type": "content_block_stop",
  "index": 0
}

What your handler does: Block at index 0 is done. Now parse the accumulated JSON {"city": "Mumbai"}. This is the first safe moment to call get_weather.


5. content_block_start — second tool call opens

json
{
  "type": "content_block_start",
  "index": 1,
  "content_block": {
    "type": "tool_use",
    "id": "tool_02DEF",
    "name": "convert_currency",
    "input": {}
  }
}

What your handler does: A second tool_use block opens at index 1. Create a slot at content[1].


6. content_block_delta — second tool input fragments

json
{ "type": "content_block_delta", "index": 1,
  "delta": { "type": "input_json_delta", "partial_json": "{\"amount\": 1000," } }
json
{ "type": "content_block_delta", "index": 1,
  "delta": { "type": "input_json_delta", "partial_json": " \"from\": \"INR\"," } }
json
{ "type": "content_block_delta", "index": 1,
  "delta": { "type": "input_json_delta", "partial_json": " \"to\": \"USD\"}" } }

What your handler does: Append each fragment to content[1]'s buffer. Don't parse yet.


7. content_block_stop — second tool call complete

json
{
  "type": "content_block_stop",
  "index": 1
}

What your handler does: Block at index 1 is done. Now parse {"amount": 1000, "from": "INR", "to": "USD"}. Safe to call convert_currency now.


8. message_delta — stop reason arrives

json
{
  "type": "message_delta",
  "delta": {
    "stop_reason": "tool_use",
    "stop_sequence": null
  },
  "usage": { "output_tokens": 89 }
}

What your handler does: Record stop_reason: tool_use — this tells you Claude stopped because it needs tool results, not because it finished. Final token counts also arrive here.


9. message_stop — stream complete

json
{
  "type": "message_stop"
}

What your handler does: Stream is done. Your assembled content[] array now looks exactly like a non-streamed response. Execute both tools, send results back, continue the loop.


The assembled content array at the end:

json
[
  { "type": "tool_use", "id": "tool_01ABC", 
    "name": "get_weather", "input": { "city": "Mumbai" } },
  { "type": "tool_use", "id": "tool_02DEF", 
    "name": "convert_currency", "input": { "amount": 1000, "from": "INR", "to": "USD" } }
]

From this point, treat it exactly like a non-streamed response.


What Claude knows immediately vs what it builds gradually

When Claude decides to call a tool, it knows two things instantly:

  • Which tool to call (get_weather)
  • The tool's id (tool_01ABC)

But the input arguments need to be generated token by token, just like text. Claude doesn't know {"city": "Mumbai"} all at once — it generates each character sequentially.

So the protocol splits accordingly:

content_block_start  →  carries what's known upfront
                        (type, name, id)

content_block_delta  →  carries what's generated gradually
                        (input arguments, fragment by fragment)

Why this design makes sense

Your handler needs the tool name and id as early as possible — before the input is even complete. Here's why:

For logging: You can immediately log "Claude is calling get_weather" without waiting for the full input.

For UI feedback: You can show the user "Fetching weather..." the moment the tool name arrives, not after all fragments land.

For parallel preparation: In complex pipelines, knowing the tool name early lets you prepare resources before the arguments are fully assembled.

If the name and id also came through deltas, you'd have to buffer everything and wait until content_block_stop just to know which tool was called — which defeats the purpose of streaming entirely.


Simple mental model:

content_block_start   =   the envelope        (who is this for, what type)
content_block_delta   =   the letter inside   (the actual generated content)
content_block_stop    =   seal the envelope   (safe to read now)

The envelope arrives whole and instantly. The letter is written gradually, word by word.

Tool Schemas (MCP)

 With tool-use, you’re not steering language toward a good answer anymore, you’re handing Claude a set of actions and trusting it to pick the right one; that pick is driven almost entirely by what you wrote in the schema.


A developer registers two tools, including search_knowledge_base and get_cached_result. The tool names are distinct, but Claude’s tool selection weighs descriptions (tools->schema->description) heavily; when descriptions overlap, name alone is not sufficient to disambiguate. 


Handles well

Routing Claude to the right tool reliably when descriptions are specific and exclusion conditions are stated.

Poor fit.
Two tools that do similar things and need ever-longer descriptions to keep apart: at that point, merge them into one tool with a type parameter instead.


**Description should be of two sentences, one to say when to use it, one to say when not to. That's the whole fix.**

Examples:

get_weather

"description": "Returns current weather for a given city. 
Use this when the user asks about current weather, temperature, 
or climate conditions for a specific city. Do not use this for 
historical weather data or weather forecasts."

convert_currency

"description": "Converts an amount from one currency to another 
using current exchange rates. Use this when the user asks to 
convert a specific amount between currencies. Do not use this 
for fetching exchange rates without a conversion amount, or for 
cryptocurrency conversions."

get_traffic

"description": "Returns current traffic conditions for a given 
city. Use this when the user asks about traffic, road conditions, 
or travel delays in a city. Do not use this for route planning, 
estimated travel time between two specific addresses, or traffic 
outside supported cities."

The context cost problem with MCP

Every tool definition you load from an MCP server eats into your context window — even if that tool is never called in the current conversation. Connect three MCP servers with 20 tools each, and 60 tool definitions are sitting in your context before the first user message even arrives.

Two ways to control this:

defer_loading — delays loading a tool definition until Claude actually needs it. Reduces upfront context cost when a server has a large tool list.

enabled — lets you register a server but selectively expose only the tools you want Claude to see. Others are hidden entirely.


Communication Protocols

How the client actually talks to the server — two transports:

stdio — for local servers. Your app spawns the MCP server as a subprocess and communicates over standard input/output. Simple, no network needed.

Streamable HTTP — for remote servers. Your app connects over the network, uses HTTP POST for sending messages to the server, and an optional SSE stream for server-initiated messages. This is the current standard — an older SSE-only transport exists but is deprecated, don't use it for new integrations.

The two sides of MCP

MCP has a server side and a client side. You wrote the server side.

Your Java Code          MCP Client (your app)        Claude
─────────────────       ──────────────────────       ──────
You define tools   →    ListToolsRequest fired   →   Receives tool
schemas manually        to your server               definitions
in buildToolsSchema()   automatically                automatically

What you automated vs what you didn't

You still wrote the tool schemas — that part is manual and always will be on the server side. Someone has to define what get_weather accepts as input.

What MCP automates is the delivery of those schemas to Claude. Without MCP, you would write the schemas twice:

  • Once in your server/tool implementation
  • Once again in your Claude API call's tools array

With MCP, you write them once in buildToolsSchema(), and the MCP client fetches them via ListToolsRequest and passes them to Claude automatically. Claude never needs you to manually copy-paste those definitions into the API call.


In your code specifically

java
case "tools/list" -> {
    ObjectNode result = mapper.createObjectNode();
    result.set("tools", buildToolsSchema());  // ← you defined these once
    send(out, successResponse(id, result));
}

When a MCP client connects to your server and fires ListToolsRequest, it gets these definitions back and forwards them to Claude — without you writing them again in the API call.


example : 

{
"model": "claude-sonnet-4-6",
"max_tokens": 1024,
"tools": [
{
"name": "get_current_time",
"description": "Returns the current time for a given timezone",
"input_schema": {
"type": "object",
"properties": {
"timezone": {
"type": "string",
"description": "IANA timezone e.g. Asia/Kolkata, America/New_York"
}
},
"required": [
"timezone"
]
}
},
{
"name": "get_weather",
"description": "Returns current weather for a given city",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name e.g. Mumbai, New York"
}
},
"required": [
"city"
]
}
},
{
"name": "get_traffic",
"description": "Returns current traffic for a given city",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name e.g. Mumbai, New York"
}
},
"required": [
"city"
]
}
},
{
"name": "convert_currency",
"description": "Converts an amount from one currency to another",
"input_schema": {
"type": "object",
"properties": {
"amount": {
"type": "number",
"description": "Amount to convert"
},
"from": {
"type": "string",
"description": "Source currency e.g. INR"
},
"to": {
"type": "string",
"description": "Target currency e.g. USD"
}
},
"required": [
"amount",
"from",
"to"
]
}
}
],
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "I'm flying from Mumbai to New York tomorrow. What's the weather there, what time is it currently in New York, and how much is 5000 INR in USD? and what's the traffic like in New York at the same time."
}
]
},
{
"role": "assistant",
"content": [
{
"type": "text",
"text": "Sure! Let me fetch all of that information for you simultaneously right away!"
},
{
"type": "tool_use",
"id": "toolu_01HySPSs563HzdhK84sE8xEA",
"name": "get_weather",
"input": {
"city": "New York"
},
"caller": {
"type": "direct"
}
},
{
"type": "tool_use",
"id": "toolu_01VD6agYVE8nAWVqohwrr8Pk",
"name": "get_current_time",
"input": {
"timezone": "America/New_York"
},
"caller": {
"type": "direct"
}
},
{
"type": "tool_use",
"id": "toolu_01BE92qZfTnz7NrWu4DCK8S9",
"name": "convert_currency",
"input": {
"amount": 5000,
"from": "INR",
"to": "USD"
},
"caller": {
"type": "direct"
}
},
{
"type": "tool_use",
"id": "toolu_01JHtuSTZzdHjUXBeULddVFQ",
"name": "get_traffic",
"input": {
"city": "New York"
},
"caller": {
"type": "direct"
}
}
]
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01HySPSs563HzdhK84sE8xEA",
"content": "28°C, Sunny"
},
{
"type": "tool_result",
"tool_use_id": "toolu_01VD6agYVE8nAWVqohwrr8Pk",
"content": "12:15 PM, Tuesday 28 Jul 2026"
},
{
"type": "tool_result",
"tool_use_id": "toolu_01BE92qZfTnz7NrWu4DCK8S9",
"content": "5000.00 INR = 60.00 USD"
},
{
"type": "tool_result",
"tool_use_id": "toolu_01JHtuSTZzdHjUXBeULddVFQ",
"content": "6am to 12pm, moderate traffic, 12pm-4pm light traffic, after 4pm heavy traffic due to office hours"
}
]
}
]
}


Extended Thinking

 Without thinking blocks carried back, Claude is re-reasoning from scratch each turn. On a complex plan it usually reaches the same conclusion — but not always. The thinking block carry-back ensures Claude is continuing the same reasoning thread, not starting a fresh one that happens to look similar.

Think of it like this:

Without carry-back:  Claude re-derives the plan each turn
                     → Usually same result, occasionally diverges

With carry-back:     Claude continues its original plan each turn
                     → Deterministically consistent

When does that difference actually matter?

Honestly, for simple 2-3 step tool loops — it probably doesn't. Your turns example would work fine.

It matters when:

  • The plan has 6-8 dependent steps
  • Claude made a conditional decision early on ("if A returns X, skip C")
  • That condition is not visible in the assistant message, only in the thinking block
  • By turn 5, Claude has forgotten its own conditional and executes C anyway

What will happen if we don't send the thinking block back in the Turn?

Without thinking block carry-back:

Turn 1: Claude reasons A→B→C→D→E→F  (full reasoning)
Turn 2: Claude reasons A→B→C→D→E→F  (full reasoning again)
Turn 3: Claude reasons A→B→C→D→E→F  (full reasoning again)
...repeats every turn

Wasteful, and risks diverging on complex plans.


With thinking block carry-back:

Turn 1: Claude reasons A→B→C→D→E→F  (full reasoning)
Turn 2: Claude continues from where Turn 1 left off  (picks up at B)
Turn 3: Claude continues from where Turn 2 left off  (picks up at C)
...progresses forward each turn

Two benefits this gives you:

Efficiency — Claude isn't re-deriving the same plan repeatedly. It already knows where it is in the plan.

Consistency — Any conditional decision Claude made in Turn 1 ("if A returns X, skip D") is preserved in the thinking block. Without carry-back, that condition might be re-derived differently in Turn 4.

System prompts - Four techniques

Four techniques that give Claude a reliable output shape

 1. Wrong shape of output (got a sentence, wanted JSON)

You didn't tell Claude how to format the answer — only what to answer. Claude fills that gap with whatever looks reasonable. Fix: add an explicit output constraint ("respond only in JSON with these fields: ...").

A developer wants Claude to label support tickets as BILLING, TECHNICAL, or ESCALATION. The first attempt is just:

"You are a support classifier. Classify the ticket."

Claude returns "Billing" sometimes, "billing" other times, and occasionally a full sentence. The downstream code expects an exact label — so it breaks. This is exactly the first row explains : wrong output shape, missing an output constraint


2. Content drifts over a conversation

Your instructions were loose enough that Claude gradually shifted tone, scope, or focus across turns. Fix: write a proper system prompt that locks in the role, boundaries, and format rules upfront — those rules then apply to every single response, not just the first one.

Imagine you're building a customer support bot for a software product. Your prompt is just:

"You are a helpful assistant. Answer the user's questions."

Turn 1 — User asks about a billing issue. Claude answers correctly, stays on topic.

Turn 3 — User casually mentions they're stressed about work. Claude starts offering life advice and a sympathetic tone.

Turn 5 — User asks a vague question. Claude now answers it like a general-purpose assistant, not a support agent — recommending Google searches, going off-topic.

Nothing in the prompt said stay scoped to software support only, don't shift tone, don't answer questions outside this domain. So Claude drifted — not because it misunderstood, but because there was no standing rule to hold it in place across turns.

Fix: A system prompt that locks the contract:

"You are a support agent for Acme Software. Answer only questions about billing, accounts, and product features. Do not offer personal advice. Keep responses under 3 sentences."

Now those rules apply to every single response, regardless of where the conversation wanders.



3. Structure is right but invented (Few-shot)

Claude understood the task but made up a structure you never asked for. The problem is that describing a structure in words is imprecise — Claude interprets the description, not the actual shape you have in mind. Fix: give it one concrete example of an input and the exact output you want. Showing beats telling here.

Few-shot examples show Claude the exact label and casing to return, and XML tags keep those examples separate from the instruction so Claude does not read them as part of the task:

System: "You are a support classifier. Classify each ticket into exactly one of: BILLING, TECHNICAL, ESCALATION. Return only the label. No other text." <sample_input>My account shows two charges for April.</sample_input> <ideal_output>BILLING</ideal_output> <sample_input>The API keeps returning a 429 error.</sample_input> <ideal_output>TECHNICAL</ideal_output> User: <ticket>I was charged twice for the same month.</ticket>

XML tags — the examples are wrapped in <sample_input> / <ideal_output> tags so Claude can clearly see where the instruction ends and where each example starts and stops. Without that separation, Claude might treat the examples as part of the task description.

Wrap them with descriptive tag names like <my_code> and <docs> and the boundary becomes unambiguous. You do not need to use official XML tag names; descriptive names that match your content work best.


4. Works on your test cases, breaks on edge cases

You validated the prompt against inputs you thought of, so it handles those fine. But the prompt has no rule for anything outside that set. Fix: when you spot a breaking variant, name it explicitly in the prompt ("if the field is empty, return null") or add an example that covers it.

Back to the ticket classifier. You've added the output constraint and few-shot examples. You test it on 20 tickets — works perfectly. You ship it.

A week later the router starts breaking. The input coming in looks like this:

"I don't know, maybe it's a billing thing? Or maybe technical? I'm not sure."

Claude returns: BILLING / TECHNICAL — because nothing in the prompt said what to do when a ticket is ambiguous. Your router expects exactly one label, so it breaks.

Your prompt handled the happy path — clear, unambiguous tickets. But it had no rule for the case it never saw during testing.

Fix: Name the variant explicitly in the prompt:

"If the ticket is ambiguous or spans multiple categories, return ESCALATION."

Or add a few-shot example that covers it:

<sample_input>Not sure if this is a billing or account issue.</sample_input>
<ideal_output>ESCALATION</ideal_output>

The prompt wasn't wrong — it just had a gap that your test inputs never exposed. The fix is closing that specific gap, not rewriting the whole prompt.



The common thread: Claude can only follow rules you actually wrote down. Every gap in the prompt is a decision Claude makes for you — sometimes correctly, sometimes not.