Guide · Anthropic SDK Advanced Features

MCP Server Extended Thinking — budget_tokens, thinking blocks, streaming reasoning

Three extended thinking behaviours catch MCP server authors off-guard: budget_tokens is a soft cap, not a hard limit — Claude may use fewer tokens if it reaches a conclusion early, but you still pay for all thinking tokens used at input token rates — setting a 10,000-token budget on a simple question still incurs cost proportional to the thinking tokens actually consumed; thinking blocks appear before text blocks in response.content, and must be filtered out before returning content to the calling agent — returning thinking blocks in MCP tool content causes some MCP clients to crash or display raw JSON to the user; and temperature must be set to 1 when extended thinking is enabled — any other temperature value returns a 400 error, not a warning, making this a silent misconfiguration if you pass temperature from a config file without checking the thinking flag.

TL;DR

Enable extended thinking with thinking: { type: 'enabled', budget_tokens: N } where N ≥ 1,024. Set temperature: 1 (required when thinking is enabled). Filter the response to return only type === 'text' blocks to the calling agent — log the type === 'thinking' blocks separately for debugging. Use thinking for complex multi-step reasoning tasks (code architecture, security analysis, math); skip it for simple lookups and classification where latency matters more than depth.

Enabling thinking and filtering response blocks

Extended thinking is supported on Claude Sonnet 4.6, Claude Opus 4.7, and later models. The minimum budget_tokens is 1,024. The response contains two block types: thinking blocks (Claude's internal reasoning, visible to you) and text blocks (the final answer for the user). Only the text blocks should be forwarded to the calling agent.

import Anthropic from '@anthropic-ai/sdk';
import { z } from 'zod';

const anthropic = new Anthropic();

server.tool(
  'deep_code_review',
  {
    code:           z.string().min(50).max(20_000),
    language:       z.enum(['typescript', 'python', 'go', 'rust', 'java']),
    review_depth:   z.enum(['quick', 'thorough']).default('thorough'),
    include_reasoning: z.boolean().default(false),
  },
  async ({ code, language, review_depth, include_reasoning }) => {
    // Use extended thinking only for 'thorough' reviews — skip for 'quick' to save cost
    const useThinking = review_depth === 'thorough';
    const budget_tokens = useThinking ? 8_000 : undefined;

    const requestParams: any = {
      model:      'claude-sonnet-4-6',
      max_tokens: 2_048 + (budget_tokens ?? 0),  // max_tokens must exceed budget_tokens
      messages: [{
        role: 'user',
        content: `Review this ${language} code for bugs, security issues, and design problems.
Provide specific, actionable findings with line references.

\`\`\`${language}
${code}
\`\`\``,
      }],
    };

    if (useThinking) {
      requestParams.thinking   = { type: 'enabled', budget_tokens };
      requestParams.temperature = 1;  // Required when thinking is enabled — any other value = 400
    }

    const response = await anthropic.messages.create(requestParams);

    // Separate thinking blocks from the answer
    const thinkingBlocks = response.content.filter(b => b.type === 'thinking');
    const textBlocks     = response.content.filter(b => b.type === 'text');

    // Log thinking for debugging — never expose to user unless explicitly requested
    if (thinkingBlocks.length > 0) {
      const thinkingText = thinkingBlocks.map(b => (b as any).thinking).join('\n---\n');
      console.log(`[thinking] ${thinkingText.slice(0, 200)}...`);
    }

    const answer = textBlocks.map(b => (b as any).text).join('\n');

    // Optionally include thinking in the tool response (for debugging builds)
    if (include_reasoning && thinkingBlocks.length > 0) {
      const thinking = thinkingBlocks.map(b => (b as any).thinking).join('\n');
      return {
        content: [
          { type: 'text', text: `[REASONING]\n${thinking}\n\n[REVIEW]\n${answer}` },
        ],
      };
    }

    return { content: [{ type: 'text', text: answer }] };
  }
);

The max_tokens parameter must be set to at least budget_tokens + your_expected_output_tokens. If max_tokens is less than budget_tokens, the API returns a 400 error. A safe default is max_tokens = budget_tokens + 2_048 — the extra 2,048 covers the text answer after reasoning completes.

Streaming thinking deltas

Extended thinking works with streaming. The stream emits content_block_start events with type: 'thinking' followed by content_block_delta events carrying thinking_delta chunks, then a content_block_stop. Text output follows the same pattern with text_delta. For long thinking budgets (16,000+ tokens), streaming is preferable to avoid hitting request timeouts.

server.tool(
  'analyze_architecture',
  {
    description: z.string().min(100).max(10_000),
    budget:      z.number().int().min(1_024).max(32_000).default(12_000),
  },
  async ({ description, budget }) => {
    const stream = anthropic.messages.stream({
      model:       'claude-sonnet-4-6',
      max_tokens:  budget + 2_048,
      temperature: 1,
      thinking:    { type: 'enabled', budget_tokens: budget },
      messages: [{
        role:    'user',
        content: `Analyse this system architecture and identify the top 5 risks:\n\n${description}`,
      }],
    });

    // Accumulate thinking and text separately
    let thinkingAcc = '';
    let textAcc     = '';

    stream.on('streamEvent', (event) => {
      if (event.type === 'content_block_delta') {
        if (event.delta.type === 'thinking_delta') {
          thinkingAcc += event.delta.thinking;
        } else if (event.delta.type === 'text_delta') {
          textAcc += event.delta.text;
        }
      }
    });

    const finalMessage = await stream.finalMessage();

    const usage = finalMessage.usage;
    console.log('thinking tokens used:', usage.input_tokens, '/ budget:', budget);

    // Return only the text answer — log thinking separately
    console.log('[arch-thinking] tokens:', thinkingAcc.length);
    return { content: [{ type: 'text', text: textAcc || 'No analysis produced.' }] };
  }
);

The usage.input_tokens field in the final message counts thinking tokens as input tokens — thinking consumes your input token quota, not a separate budget. If you are tracking costs per tool call, add usage.input_tokens (which includes thinking) and usage.output_tokens (the text answer only) together for the total billable token count.

Thinking blocks in multi-turn conversations

If your MCP tool implements a multi-turn conversation by passing previous messages back to the API, you must include thinking blocks from prior turns verbatim in the message history. Claude uses its previous thinking to inform subsequent responses — stripping thinking blocks from the history causes the model to lose its reasoning context and produces lower-quality follow-up answers.

// Maintain conversation history including thinking blocks
type ConversationTurn = {
  role:    'user' | 'assistant';
  content: any[];  // includes both 'thinking' and 'text' blocks for assistant turns
};

const history: ConversationTurn[] = [];

server.tool(
  'thinking_conversation',
  {
    message:         z.string().min(1).max(5_000),
    conversation_id: z.string().optional(),
  },
  async ({ message, conversation_id }) => {
    // Add the new user message to history
    history.push({ role: 'user', content: [{ type: 'text', text: message }] });

    const response = await anthropic.messages.create({
      model:       'claude-sonnet-4-6',
      max_tokens:  4_096 + 2_048,
      temperature: 1,
      thinking:    { type: 'enabled', budget_tokens: 4_096 },
      messages:    history,
    });

    // Store the FULL assistant response (thinking + text) in history
    // Do NOT filter out thinking blocks before storing — the model needs them next turn
    history.push({ role: 'assistant', content: response.content });

    // But only return text blocks to the calling agent
    const textOnly = response.content
      .filter(b => b.type === 'text')
      .map(b => (b as any).text)
      .join('\n');

    return { content: [{ type: 'text', text: textOnly }] };
  }
);

When to use extended thinking in MCP tools

Extended thinking adds latency (typically 5–30 seconds for a 10,000-token budget) and cost (thinking tokens billed at input rates). The trade-off is worth it for tasks where reasoning depth directly improves output quality — and wrong for tasks where the answer is lookup-based or latency-critical.

Task typeUse thinking?Reason
Architecture review (100+ component system)YesDependency graph reasoning improves finding quality
Security vulnerability analysisYesAttack chain reasoning requires multi-step deduction
Math / algorithm correctness proofYesStep-by-step symbolic reasoning is core to accuracy
Simple text classificationNoClassification is pattern matching — thinking adds cost, not value
Data extraction / parsingNoStructural extraction does not benefit from reasoning
Interactive Q&A where user waits <2sNoThinking latency unacceptable in interactive contexts
Legal or contract risk analysisYesClause interdependency reasoning improves finding quality
Code refactor suggestions for small functionNoToo simple — standard completion suffices