Anthropic SDK Advanced Features · 2026-08-06 · Anthropic SDK arc

Anthropic SDK Advanced Features for MCP Servers: Prompt Caching, Files API, Batch API, Extended Thinking, and Vision

Five Anthropic SDK advanced features — prompt caching, Files API, Batch API, extended thinking, and vision and multimodal inputs — appear in Claude-powered MCP servers for the same structural reason: a tool handler that calls Claude once per invocation with a large system prompt, a PDF document, or a high-resolution image can cost ten to fifty times more per call than a handler that uses all five features in combination, and the cost difference is invisible unless you instrument cache_read_input_tokens, measure file reuse rate, measure batch queue depth, count thinking tokens, and estimate image token cost before calling the API. Each of the five features addresses a different dimension of the cost-quality-latency triangle that every Claude-powered MCP tool must balance: prompt caching targets input token cost for the static prefix of your prompt — the system instructions, tool schemas, and reference documents that don't change between calls — and delivers up to 90% savings on those tokens when the same prefix is seen within the 5-minute ephemeral TTL; Files API targets bandwidth cost for large binary documents that would otherwise be re-transmitted as base64 on every call, letting you upload a PDF once and reference it by file_id across as many requests as needed without re-encoding the bytes; Batch API targets per-call overhead cost for workloads that don't require interactive latency, delivering 50% cost reduction by queuing up to 10,000 requests and processing them asynchronously over minutes to hours; extended thinking targets answer quality for tasks where Claude's standard single-pass generation produces surface-level analysis, by allocating a token budget for internal reasoning that happens before the visible response and billing it at input token rates rather than output rates; and vision and multimodal inputs extends all of the above to image and PDF content, but with content block type constraints that are not obvious from the API surface — PDFs are document blocks, not image blocks, and images cost tokens proportional to pixel area rather than byte size, making a 4K screenshot more expensive than a 500-page text document in tokens. The first structural pattern is cost efficiency architecture — how prompt caching, Files API, and Batch API compose together into a three-layer cost stack that reduces token spend by 60–90% for document-processing MCP tools without changing the tool's visible behavior: prompt caching places cache_control: { type: 'ephemeral' } at the boundary between the static system prompt (instructions, schemas, reference text that is identical on every call) and the dynamic user query (the document, question, or code being analyzed — unique per call), so subsequent calls by the same or different users pay 90% less for the system prompt tokens; Files API handles the document layer — the PDF, HTML, or CSV that changes per-session but not per-question — by uploading it once and caching the file_id keyed on a SHA-256 content hash, so a user who asks five questions about the same contract only transmits the contract bytes once across the network rather than five times; and Batch API handles the workload layer — when a tool is called with fifty items to classify or analyze, submitting them individually at synchronous rates costs twice what batch submission costs, and if the tool is called non-interactively (by a scheduled workflow, not a waiting user), the latency penalty of batching is irrelevant. The second structural pattern is input modality handling — the mapping from what content looks like to the calling agent (bytes, base64 strings, file paths, URLs) to the exact content block format the Anthropic API requires — because the four supported modalities (text, image, PDF, and Files API references) use four different content block structures, and sending any modality in the wrong structure returns a 400 error at best or silently misroutes the content at worst: images use { type: 'image', source: { type: 'base64', media_type: '...', data: '...' } } for private content or { type: 'image', source: { type: 'url', url: '...' } } for publicly reachable URLs; PDFs use { type: 'document', source: { type: 'base64', media_type: 'application/pdf', data: '...' } } (never the image block type); Files API references use { type: 'document', source: { type: 'file', file_id: '...' } } for documents and { type: 'image', source: { type: 'file', file_id: '...' } } for image files — and mixing these block types (PDF in an image block, file_id as a text string in the message, image with application/pdf media_type) produces errors that are not always self-descriptive. The third structural pattern is the latency-vs-depth trade-off spectrum — where the same MCP tool use case falls at different points on the spectrum depending on the calling context: an IDE-integrated code review tool needs sub-2-second response times and therefore cannot use extended thinking or batch processing; a background code quality sweep triggered nightly by a CI pipeline can afford 30 seconds of extended thinking and batch processing of all files in parallel; a weekly compliance report generator should submit all documents as a single batch job, cache the compliance schema as the system prompt, reference each document via Files API, and retrieve results hours later. This post maps all five features against all three patterns, provides annotated code for the full cost stack, covers 14 failure modes with root cause and fix, and provides a ten-row feature selection guide for common MCP tool categories.

TL;DR

Five features, three patterns. (1) Cost efficiency architecture: Prompt cachingcache_control: { type: 'ephemeral' } on the last element of your static system prompt array; never on dynamic user messages; minimum 1,024 tokens before the breakpoint or caching silently skips; read usage.cache_read_input_tokens to confirm hits; warmup request at startup to pre-populate the cache so first real call hits rather than writes. Files API — upload with new File([buffer], filename, { type: mimeType }) passed to anthropic.beta.files.upload(); reference as { type: 'document', source: { type: 'file', file_id } }; cache file_id keyed on SHA-256 content hash so same document never re-uploaded; combine with cache_control on the document block for maximum savings. Batch APIanthropic.messages.batches.create({ requests }) with unique custom_id per item; return batch_id immediately from tool; expose a second get_batch_results tool that polls retrieve(batchId).processing_status and streams results via batches.results(batchId) only when status is 'ended'. (2) Input modality handling: Images — base64 with stripped data URI prefix or public HTTPS URL; downscale to 1,568 px on the long edge (4K = ~6,000 tokens); four media types: jpeg/png/gif/webp. PDFs — document block, not image block; base64 only (no URL source). Files API — { type: 'document', source: { type: 'file', file_id } }, never as a text string. (3) Latency-vs-depth spectrum: Extended thinkingthinking: { type: 'enabled', budget_tokens: N } with N ≥ 1,024; temperature must be exactly 1 (any other value = 400); max_tokens must exceed budget_tokens; filter response to return only type === 'text' blocks — thinking blocks crash some MCP clients; include thinking blocks verbatim in multi-turn history or follow-up quality degrades; use for complex reasoning tasks (architecture review, security analysis, math proofs) not classification or lookup.

Pattern 1 — Cost Efficiency Architecture: Composing the Three-Layer Cost Stack

The three cost reduction features — prompt caching, Files API, and Batch API — target different layers of token cost in a Claude-powered MCP tool, and they compose without interference: a tool handler can simultaneously use prompt caching on its system instructions, Files API for the document being analyzed, and Batch API for the workload tier (when the same document needs to be analyzed across fifty question types in a compliance sweep). The mistake is treating these as alternatives rather than as a stack — enabling only prompt caching while still re-transmitting a 300-page PDF on every call, or enabling Files API without prompt caching, misses the largest available savings.

Layer 1 — Prompt caching for the static system prefix

Prompt caching targets the invariant portion of every API call — the system prompt, tool schemas, coding style guides, domain glossaries, and safety rules that are identical across every tool invocation regardless of what the user is asking about. The cache key is computed from the longest prefix of the request that ends at a cache_control: { type: 'ephemeral' } marker. Everything after the marker (the dynamic user query) varies freely without affecting the cache key.

The 5-minute ephemeral TTL means a cache entry persists as long as the tool is called at least once every 5 minutes — each cache hit resets the timer. An MCP server receiving one tool call per minute keeps its cache warm indefinitely. An MCP server called once per hour on a cron schedule will miss the cache on every call. For the latter pattern, the warmup strategy (a dummy request at startup) ensures the first real call after a deployment or container restart hits the cache rather than creating a new entry with the 25%-premium cache-write cost.

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

const anthropic = new Anthropic();

// System prompt — loaded once at module init.
// This is the static prefix that will be cached.
const COMPLIANCE_SYSTEM_PROMPT = `You are a contract compliance analyst.
You evaluate contracts against a regulatory framework and output structured findings.

Output format:
{
  "compliant": true|false,
  "risk_level": "HIGH" | "MEDIUM" | "LOW" | "NONE",
  "findings": [
    { "clause": "section reference", "issue": "description", "severity": "HIGH|MEDIUM|LOW" }
  ],
  "summary": "one-paragraph executive summary"
}

Rules:
- Never assume compliance for ambiguous clauses — flag them at MEDIUM risk
- Reference exact section numbers from the document
- Identify missing required clauses as HIGH severity findings`;

// Warmup: pre-populate the cache before the server starts accepting connections.
// The cache_creation_input_tokens cost here is shifted out of the first user's call.
async function warmupPromptCache(): Promise<void> {
  try {
    const res = await anthropic.messages.create({
      model: 'claude-sonnet-4-6',
      max_tokens: 1,
      system: [
        { type: 'text', text: COMPLIANCE_SYSTEM_PROMPT, cache_control: { type: 'ephemeral' } },
      ],
      messages: [{ role: 'user', content: 'warmup' }],
    });
    const written = res.usage.cache_creation_input_tokens;
    console.log(written > 0 ? `Cache warmed: ${written} tokens` : 'Cache already warm');
  } catch {
    // Non-fatal — first real call will write the entry
  }
}

await warmupPromptCache();

The minimum cacheable size is 1,024 tokens. System prompts shorter than this threshold are processed normally — the API silently ignores the cache_control marker and returns zero for both cache_creation_input_tokens and cache_read_input_tokens. This is the single most common reason developers confirm their breakpoints are placed correctly but see zero cache metrics: the system prompt is under the minimum threshold. The fix is to expand the static content — add a more detailed tool schema, a reference table, or an example output — until the prefix exceeds 1,024 tokens.

Layer 2 — Files API for the per-session document layer

The Files API targets the document layer — the PDF, HTML specification, or CSV dataset that the tool is asked to analyze. Unlike the system prompt (identical on every call), the document varies by session: a contract review tool processes a different contract for each user session, but the same contract may be queried multiple times within a session (summarize, find clause X, extract party names). Without Files API, every question about the same contract re-transmits the full PDF bytes as base64, burning bandwidth and connection time proportional to the file size on every call. With Files API, the PDF is uploaded once per unique document and referenced by file_id on every subsequent request.

Files are scoped to the API key, not to a session or user — any request using your API key can reference any file you've uploaded. For multi-tenant MCP servers, this means file IDs must not be exposed to users, and sensitive user documents require per-user API key isolation if you use the Files API. The content-hash cache pattern handles deduplication: the same PDF uploaded twice gets two different IDs (the API does not deduplicate), both consuming storage quota.

import { createHash } from 'crypto';
import Database from 'better-sqlite3';

const db = new Database('./data.db');
db.exec(`CREATE TABLE IF NOT EXISTS file_cache (
  content_hash TEXT PRIMARY KEY,
  file_id      TEXT NOT NULL,
  filename     TEXT,
  created_at   INTEGER DEFAULT (unixepoch())
)`);

// Upload a document once; return the cached file_id on subsequent calls.
// This avoids the most common Files API waste: re-uploading the same PDF
// every time a new question arrives about the same document.
async function getOrUploadFile(
  content: Buffer,
  filename: string,
  mimeType: string,
): Promise<string> {
  const hash = createHash('sha256').update(content).digest('hex');
  const cached = db.prepare('SELECT file_id FROM file_cache WHERE content_hash = ?')
    .get(hash) as { file_id: string } | undefined;
  if (cached) return cached.file_id;

  const file = await (anthropic as any).beta.files.upload({
    file: new File([content], filename, { type: mimeType }),
  });
  db.prepare('INSERT OR IGNORE INTO file_cache (content_hash, file_id, filename) VALUES (?,?,?)')
    .run(hash, file.id, filename);
  return file.id;
}

// MCP tool: analyze a contract PDF.
// First call uploads the PDF; subsequent calls for the same PDF skip the upload.
server.tool(
  'analyze_contract',
  {
    pdf_base64: z.string().describe('Base64-encoded PDF'),
    filename:   z.string().default('contract.pdf'),
    question:   z.string().min(1).max(2_000),
  },
  async ({ pdf_base64, filename, question }) => {
    const pdfBuffer = Buffer.from(pdf_base64, 'base64');
    const file_id = await getOrUploadFile(pdfBuffer, filename, 'application/pdf');

    const response = await (anthropic as any).messages.create({
      model:      'claude-sonnet-4-6',
      max_tokens: 1_024,
      system: [
        // Layer 1: cached system prompt
        { type: 'text', text: COMPLIANCE_SYSTEM_PROMPT, cache_control: { type: 'ephemeral' } },
      ],
      messages: [
        {
          role: 'user',
          content: [
            {
              // Layer 2: Files API reference — no bytes transmitted here,
              // only the file_id reference. Also add cache_control to prompt-cache
              // the document's token representation (saves 90% on repeated questions).
              type:          'document',
              source:        { type: 'file', file_id },
              cache_control: { type: 'ephemeral' },
            },
            { type: 'text', text: question },  // Layer 3: dynamic query — not cached
          ],
        },
      ],
    });

    const { cache_creation_input_tokens, cache_read_input_tokens } = response.usage;
    console.log('cache', { created: cache_creation_input_tokens, read: cache_read_input_tokens });

    const text = response.content.find((b: any) => b.type === 'text')?.text ?? '';
    return { content: [{ type: 'text', text }] };
  }
);

Note the two cache_control markers: one on the system prompt (Layer 1) and one on the document block (Layer 2). Both activate prompt caching simultaneously. The first call to a given contract writes two cache entries — one for the system prompt prefix, one for the system prompt + document prefix. Subsequent calls with the same contract and the same system prompt hit both cache layers: cache_read_input_tokens reflects the combined savings from both entries. For a 50,000-token PDF analyzed ten times in a session, the first call pays the full cost; calls 2–10 pay approximately 10% of the document token cost (the question tokens only).

Layer 3 — Batch API for the workload tier

The Batch API applies to the workload tier: when an MCP tool is invoked not by a waiting user but by an automated workflow that needs to analyze fifty contracts, classify ten thousand support tickets, or generate a weekly report from a hundred data files, synchronous processing is both slower and twice as expensive as batch processing. The Batch API costs 50% less per call at the price of asynchronous latency — results arrive in minutes to hours, not milliseconds.

The two-tool pattern is mandatory: one tool submits the batch and returns the batch_id immediately; a second tool polls for completion and retrieves results when the batch reaches processing_status === 'ended'. Calling batches.results(batchId) on an in-progress batch returns an empty async iterator with no error — this silent empty return is the most common source of confusion: the developer sees no output and assumes the batch failed, when it is simply still running.

import { createHash } from 'crypto';

// Tool 1: submit a batch of contracts for compliance analysis
server.tool(
  'batch_analyze_contracts',
  {
    contracts: z.array(z.object({
      id:         z.string(),           // stable ID — becomes custom_id
      pdf_base64: z.string(),
      filename:   z.string().default('contract.pdf'),
    })).min(1).max(10_000),
    question: z.string().min(1).max(500).default('Identify all HIGH severity compliance issues.'),
  },
  async ({ contracts, question }) => {
    const requests = await Promise.all(contracts.map(async ({ id, pdf_base64, filename }) => {
      const buf     = Buffer.from(pdf_base64, 'base64');
      const file_id = await getOrUploadFile(buf, filename, 'application/pdf');

      return {
        custom_id: id,  // caller-supplied stable ID; must be unique within the batch
        params: {
          model:      'claude-haiku-4-5-20251001',  // cheapest model for bulk work
          max_tokens: 512,
          system:     COMPLIANCE_SYSTEM_PROMPT,     // no caching in batch (different session pool)
          messages: [
            {
              role: 'user',
              content: [
                { type: 'document', source: { type: 'file', file_id } },
                { type: 'text', text: question },
              ],
            },
          ],
        },
      };
    }));

    // Deduplicate by custom_id — the API rejects duplicate IDs within a batch
    const seen = new Set<string>();
    const deduped = requests.filter(r => { if (seen.has(r.custom_id)) return false; seen.add(r.custom_id); return true; });

    const batch = await anthropic.messages.batches.create({ requests: deduped });
    return {
      content: [{
        type: 'text',
        text: JSON.stringify({
          batch_id:      batch.id,
          status:        batch.processing_status,
          request_count: batch.request_counts.processing,
          expires_at:    batch.expires_at,
          message:       'Poll get_batch_results with this batch_id when ready.',
        }),
      }],
    };
  }
);

// Tool 2: poll status and retrieve results when complete
server.tool(
  'get_batch_results',
  { batch_id: z.string() },
  async ({ batch_id }) => {
    const batch = await anthropic.messages.batches.retrieve(batch_id);
    if (batch.processing_status !== 'ended') {
      const c = batch.request_counts;
      return {
        content: [{
          type: 'text',
          text: JSON.stringify({ status: batch.processing_status, processing: c.processing, succeeded: c.succeeded, errored: c.errored, message: 'Not ready. Retry in 60s.' }),
        }],
      };
    }

    const results: any[] = [];
    for await (const r of anthropic.messages.batches.results(batch_id)) {
      if (r.result.type === 'succeeded') {
        const text = r.result.message.content.find((b: any) => b.type === 'text')?.text ?? '{}';
        try { results.push({ id: r.custom_id, ...JSON.parse(text) }); }
        catch { results.push({ id: r.custom_id, error: 'parse_failed', raw: text }); }
      } else {
        results.push({ id: r.custom_id, error: r.result.type });
      }
    }
    return { content: [{ type: 'text', text: JSON.stringify(results) }] };
  }
);

The three-layer cost stack — prompt caching on system instructions + Files API for document reuse + Batch API for volume discounts — produces multiplicative rather than additive savings. A contract analysis tool that processes 1,000 contracts monthly, each 100,000 tokens of PDF, analyzing each contract an average of three times: without any optimization, cost scales linearly with total input tokens. With prompt caching (90% savings on the 2,000-token system prompt across all 3,000 requests) + Files API (3× document reuse means only 1,000 unique uploads instead of 3,000) + Batch API (50% cost reduction on the inference layer): the combined effect reduces LLM spend to approximately 15–20% of the unoptimized baseline.

Pattern 2 — Input Modality Handling: The Content Block Type Map

The Anthropic API expresses all inputs — text, images, PDFs, and Files API references — as typed content blocks inside the messages array. The block type determines how the API interprets the content, and sending a modality in the wrong block type produces either an explicit API error (400 with a descriptive message) or a silent misrouting where the model receives something other than what the tool intended. The four modalities have four distinct content block structures, and the distinctions are not always obvious from the API surface alone.

Images: base64 vs URL source, and the pixel-to-token cost

Images are sent as type: 'image' blocks with a source that is either { type: 'base64', media_type: '...', data: '...' } or { type: 'url', url: '...' }. The four accepted media types for base64 are image/jpeg, image/png, image/gif, and image/webp. URL-sourced images must be publicly reachable HTTPS URLs — Claude fetches them server-side during the API call and does not send authentication headers, so private URLs, localhost addresses, signed URLs with short expiries, and VPC-internal URLs all fail silently or with a fetch error.

The token cost of images is the single most common source of unexpected API bills in vision-capable MCP servers: image tokens are proportional to pixel area, not file size. A 4K screenshot (3,840 × 2,160 pixels) costs approximately 6,120 input tokens regardless of whether it is a 500 KB JPEG or a 15 MB PNG. Sending three 4K screenshots in one tool call consumes ~18,000 input tokens before any text is added, at a higher per-token cost than text tokens on some model tiers. The fix is systematic downscaling: resize to 1,568 px on the long edge before sending, which reduces a 4K screenshot from ~6,120 tokens to ~1,530 tokens at minimal visible quality loss for most analysis tasks.

import sharp from 'sharp';

const MAX_DIM = 1_568;  // max long-edge dimension — above this, token cost grows fast

async function downscaleIfNeeded(input: Buffer): Promise<{ buffer: Buffer; mediaType: string }> {
  const img  = sharp(input);
  const meta = await img.metadata();
  const maxDim = Math.max(meta.width ?? 0, meta.height ?? 0);

  // Convert to JPEG on resize — smaller base64 payload than PNG
  const resized = maxDim > MAX_DIM
    ? await img.resize({ width: MAX_DIM, height: MAX_DIM, fit: 'inside' }).jpeg({ quality: 85 }).toBuffer()
    : await img.jpeg({ quality: 90 }).toBuffer();

  return { buffer: resized, mediaType: 'image/jpeg' };
}

// Estimate token cost before calling the API — guard against runaway spend
function estimateImageTokens(widthPx: number, heightPx: number): number {
  const tilesW = Math.ceil(widthPx / 512);
  const tilesH = Math.ceil(heightPx / 512);
  return 85 + (170 * tilesW * tilesH);
}

server.tool(
  'analyze_screenshot',
  {
    image_base64: z.string().describe('Base64-encoded PNG or JPEG — data URI prefix will be stripped'),
    question:     z.string().min(1).max(1_000).default('Describe what you see.'),
  },
  async ({ image_base64, question }) => {
    // Strip data URI prefix if present — the API requires raw base64, not a data URI
    const raw = Buffer.from(
      image_base64.replace(/^data:[^;]+;base64,/, ''),
      'base64',
    );

    const { buffer: processed, mediaType } = await downscaleIfNeeded(raw);
    const data = processed.toString('base64');

    // Quick cost check before sending — abort if image is surprisingly expensive
    const meta  = await sharp(processed).metadata();
    const estTokens = estimateImageTokens(meta.width ?? 0, meta.height ?? 0);
    if (estTokens > 5_000) {
      return { content: [{ type: 'text', text: `Estimated ${estTokens} image tokens. Reduce resolution or crop before sending.` }], isError: true };
    }

    const response = await anthropic.messages.create({
      model:      'claude-sonnet-4-6',
      max_tokens: 1_024,
      messages: [{
        role: 'user',
        content: [
          { type: 'image', source: { type: 'base64', media_type: mediaType as any, data } },
          { type: 'text', text: question },
        ],
      }],
    });

    const text = response.content.find(b => b.type === 'text')?.text ?? '';
    return { content: [{ type: 'text', text }] };
  }
);

PDFs: document block, not image block

PDFs use the type: 'document' content block with source: { type: 'base64', media_type: 'application/pdf', data: '...' }. This is the single most commonly misapplied constraint in multimodal MCP tools: PDFs visually contain images and layouts, so developers intuitively try the image block type. Sending a PDF as type: 'image' with media_type: 'application/pdf' returns a 400 error from the API. Sending a PDF as type: 'image' with media_type: 'image/jpeg' (treating the PDF bytes as JPEG) returns a base64 decode or content parsing error. The document block processes PDFs correctly with full text extraction, layout understanding, table parsing, and embedded image understanding.

// CORRECT: PDF as document block
const correctPDFMessage = {
  role: 'user',
  content: [
    {
      type:   'document',
      source: {
        type:       'base64',
        media_type: 'application/pdf',  // only valid media_type for PDF document blocks
        data:       pdf_base64,          // raw base64, no data URI prefix
      },
    },
    { type: 'text', text: 'Summarize this document.' },
  ],
};

// WRONG: PDF as image block — returns API error
const wrongPDFMessage = {
  role: 'user',
  content: [
    {
      type:   'image',                   // ← wrong block type for PDFs
      source: { type: 'base64', media_type: 'application/pdf', data: pdf_base64 },
    },
  ],
};

// ALSO WRONG: file_id passed as text string in the message
const alsoWrongMessage = {
  role: 'user',
  content: 'Please analyze file_abc123',  // ← model receives the literal string, not the file content
};

// CORRECT: Files API reference as document block
const correctFileMessage = {
  role: 'user',
  content: [
    {
      type:   'document',
      source: { type: 'file', file_id: 'file_abc123' },  // ← correct structure
    },
    { type: 'text', text: 'Summarize this document.' },
  ],
};

For tools that receive content from diverse sources — file uploads, URLs, clipboard, camera captures — the modality routing decision must happen before the API call. A single dispatcher function that inspects the MIME type, URL scheme, and file extension, then constructs the correct content block, prevents the modality mismatch errors that appear as intermittent 400s in production:

type ContentBlock =
  | { type: 'image';    source: { type: 'base64'; media_type: string; data: string } }
  | { type: 'image';    source: { type: 'url'; url: string } }
  | { type: 'document'; source: { type: 'base64'; media_type: string; data: string } }
  | { type: 'document'; source: { type: 'file'; file_id: string } };

async function buildContentBlock(
  input: { base64?: string; url?: string; file_id?: string; mimeType?: string }
): Promise<ContentBlock> {
  if (input.file_id) {
    // Files API reference — works for both documents and images (file_id knows its type)
    return { type: 'document', source: { type: 'file', file_id: input.file_id } };
  }
  if (input.url) {
    // URL source — images only; PDFs require base64
    const mimeType = input.mimeType ?? 'image/jpeg';
    if (mimeType === 'application/pdf') {
      throw new Error('PDFs cannot be sent as URL sources — use base64 instead');
    }
    return { type: 'image', source: { type: 'url', url: input.url } };
  }
  if (input.base64 && input.mimeType) {
    const data = input.base64.replace(/^data:[^;]+;base64,/, '');
    if (input.mimeType === 'application/pdf') {
      return { type: 'document', source: { type: 'base64', media_type: 'application/pdf', data } };
    }
    // All other MIME types treated as images
    return { type: 'image', source: { type: 'base64', media_type: input.mimeType as any, data } };
  }
  throw new Error('Must provide file_id, url, or both base64 and mimeType');
}

The content block type map in summary: images → type: 'image' with base64 or public URL; PDFs → type: 'document' with base64 only; text files, HTML, CSV, Markdown → type: 'document' with base64 and appropriate MIME type; Files API references → type: 'document' or type: 'image' (depending on whether the file is a document or image) with source: { type: 'file', file_id }.

Pattern 3 — The Latency-vs-Depth Trade-off Spectrum

The three calling modes — synchronous (standard Messages API), extended thinking (synchronous with internal reasoning), and async batch (Batch API) — differ in latency, cost, and answer quality. The right choice depends not on the task itself but on the calling context: the same code review task warrants a different mode when it is triggered by a developer pressing Cmd+Shift+R in their IDE (interactive, needs <2s) versus when it is triggered by a nightly CI pipeline sweeping 500 files (non-interactive, can afford 30 minutes). MCP tools that expose only one calling mode for a task type leave performance or quality on the table for the other calling context.

Extended thinking: the temperature-1 constraint and the thinking-block filter

Extended thinking adds internal reasoning — Claude works through the problem before producing the visible response. The visible response is higher quality for complex tasks (architecture review, security vulnerability analysis, multi-step math proofs) because Claude can explore alternative interpretations, backtrack from wrong assumptions, and build up chains of inference before committing to an answer. The cost is latency (5–30 seconds for a 10,000-token budget) and per-token cost (thinking tokens are billed at input token rates, not output rates, but the budget can be large).

Three constraints create the most common misconfigurations. First: temperature must be exactly 1 when extended thinking is enabled. Any other value — including values like 0.7 passed from a configuration file or environment variable without a thinking-mode check — returns a 400 error immediately. This is a hard constraint, not a recommendation. Second: max_tokens must exceed budget_tokens. Setting both to 8,000 returns a 400 error because Claude needs additional output capacity beyond the thinking budget to produce the visible response. A safe default is max_tokens = budget_tokens + 2_048. Third: thinking blocks appear in response.content before text blocks, and some MCP clients fail or display raw JSON when a tool response contains non-text content blocks. The thinking blocks must be filtered out before returning the response to the calling agent.

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'),
  },
  async ({ code, language, review_depth }) => {
    const useThinking  = review_depth === 'thorough';
    const budget       = useThinking ? 8_000 : undefined;

    const params: any = {
      model:      'claude-sonnet-4-6',
      max_tokens: (budget ?? 0) + 2_048,  // always > budget_tokens
      messages: [{
        role: 'user',
        content: `Review this ${language} code for bugs, security issues, and design problems.\n\n\`\`\`${language}\n${code}\n\`\`\``,
      }],
    };

    if (useThinking) {
      params.thinking   = { type: 'enabled', budget_tokens: budget };
      params.temperature = 1;  // required — omitting this or using any other value = 400
    }

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

    // Filter out thinking blocks — only text blocks go back to the agent
    const thinkingBlocks = response.content.filter(b => b.type === 'thinking');
    const textBlocks     = response.content.filter(b => b.type === 'text');

    // Log thinking for debugging — useful for tuning the budget_tokens value
    if (thinkingBlocks.length > 0) {
      console.log(`[thinking] tokens used: ${response.usage.input_tokens - (response.usage.output_tokens ?? 0)}`);
    }

    const answer = textBlocks.map(b => (b as any).text).join('\n');
    return { content: [{ type: 'text', text: answer || 'No findings.' }] };
  }
);

Thinking blocks in multi-turn conversations

If the MCP tool maintains a multi-turn conversation by passing prior messages back to the API, thinking blocks from previous assistant turns must be included verbatim in the message history. Stripping them before the next turn causes Claude to lose the reasoning context it built during the prior turn, producing lower-quality follow-ups. This creates a storage obligation: the tool's conversation history must retain the full response.content array (which includes thinking blocks) for the assistant turn, even though only the text blocks are returned to the calling agent.

type ConvTurn = { role: 'user' | 'assistant'; content: any[] };
const history: ConvTurn[] = [];

server.tool(
  'thinking_chat',
  {
    message:     z.string().min(1).max(5_000),
    session_id:  z.string().optional(),
  },
  async ({ message }) => {
    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 FULL response (thinking + text) in history — needed for continuity
    history.push({ role: 'assistant', content: response.content });

    // Return ONLY text to calling agent — never expose thinking blocks
    const text = response.content
      .filter(b => b.type === 'text')
      .map(b => (b as any).text)
      .join('\n');

    return { content: [{ type: 'text', text: text || 'No response.' }] };
  }
);

Choosing the right mode for each calling context

The decision table below maps task types and calling contexts to the recommended combination of features. The goal is to match capability to requirement: don't pay for extended thinking on tasks that don't need deep reasoning, and don't skip batch processing on workloads where the 50% discount is free money given the available latency budget.

Task typeCalling contextRecommended modeKey features
Code review in IDEDeveloper waiting <2sSynchronous, no thinkingPrompt caching on style rules
Nightly code quality sweepCI pipeline, 30 min budgetBatch + extended thinkingBatch API + budget_tokens 8k
Contract analysis — single questionUser in web UISynchronousFiles API + prompt caching
Contract analysis — 500 contractsLegal team monthly sweepBatchBatch API + Files API (reuse)
Architecture risk analysisDeveloper reviewing design docSynchronous + thinkingbudget_tokens 12k + streaming
Screenshot accessibility auditCI pipeline, 5 min budgetBatchBatch API + image downscaling
Document Q&A (same doc, many questions)Interactive sessionSynchronousFiles API + prompt cache on doc
Security vulnerability scanSecurity team quarterly reviewBatch + thinkingBatch + budget_tokens + Files API
Support ticket classificationAsync pipeline, hours acceptableBatchBatch API + Haiku model
PDF table extractionUser uploads, expects <10sSynchronousDocument block + prompt caching

Failure Modes Table — 14 Silent and Loud Bugs Across All Five Features

The following table catalogs the most commonly encountered failure modes across the five features. Silent failures — where the API returns HTTP 200 but the feature is not working — are the most dangerous because they produce incorrect cost tracking, missed cache savings, and subtly degraded output without any error to trigger an alert.

FeatureSymptomRoot causeFix
Prompt cachingcache_read_input_tokens stays 0 after first callSystem prompt under 1,024 tokens — caching silently skippedExpand static content until prefix exceeds 1,024 tokens
Prompt cachingCache hit rate drops to 0% intermittentlyTool called less frequently than once per 5 min — TTL expires between callsWarmup request at startup; schedule keep-alive if call frequency < 1/5min
Prompt cachingEvery call shows cache_creation, never cache_readcache_control placed on dynamic user message — different key each callMove breakpoint to last static element in system array, before user content
Files APISame PDF uploaded multiple times, quota accumulatingNo content-hash dedup — API does not deduplicate uploadsSHA-256 content hash cache in SQLite; check before every upload call
Files APIModel receives "file_abc123" as text, not document contentfile_id passed as text string in message content instead of document blockUse { type: 'document', source: { type: 'file', file_id } } block format
Files APIUpload fails with 413 errorPer-account storage quota exceeded from accumulated stale filesRun purgeOldFiles() on timer; delete files when session ends
Batch APIbatches.results() returns empty — no outputCalled before batch reaches 'ended' status — empty iterator with no errorCheck batch.processing_status === 'ended' before calling .results()
Batch APIAPI rejects batch with 400 — duplicate custom_idTwo items in the same batch share the same custom_idDeduplicate with Set before calling batches.create()
Batch APIResults not retrievable — expiredBatch results expire 24 hours after creationPersist results to own storage (SQLite/S3) immediately when batch ends
Extended thinking400 error on API calltemperature not set to exactly 1, or max_tokensbudget_tokensSet temperature: 1 and max_tokens = budget_tokens + 2_048
Extended thinkingMCP client crashes or shows raw JSONThinking blocks included in tool response contentFilter response.content to only type === 'text' blocks before returning
Extended thinkingFollow-up answers worse than first turnThinking blocks stripped from history before next API callInclude full response.content (thinking + text) in assistant history
Vision400 error: "unsupported media type"PDF sent as type: 'image' block instead of type: 'document'PDFs always use type: 'document'; images use type: 'image'
VisionBase64 decode error from APIData URI prefix (data:image/png;base64,) not stripped before sendingdata.replace(/^data:[^;]+;base64,/, '') before encoding check

Feature Combination Reference

The five features are not mutually exclusive — they compose freely. The following reference shows which combinations are valid, which are complementary, and which combinations don't interact (each works independently regardless of the other).

Feature AFeature BInteractionCombined effect
Prompt cachingFiles APIComplementary — both reduce cost independentlyCache system prompt; Files API avoids re-upload; add cache_control to document block for two-layer caching
Prompt cachingExtended thinkingIndependent — both work simultaneouslyCache the large system prompt; pay thinking cost only for the reasoning phase
Prompt cachingBatch APIPartial — prompt caching may not apply in Batch contextBatch requests share API infrastructure but not necessarily the same cache pool; test empirically
Files APIExtended thinkingAdditive — file upload savings stack with thinking qualityUpload document once; apply thinking budget for deep analysis
Files APIBatch APIComplementary — upload once; reference in each batch itemSame file_id in all batch request params; avoids per-item upload overhead
Vision (base64)Prompt cachingIndependent — image tokens not cached, system prompt tokens areCache system instructions; pay full image token cost per call
Vision (URL source)Files APIAlternative strategies — both avoid re-transmitting bytesURL source if image is publicly accessible; Files API if image is private or frequently reused
Extended thinkingBatch APIValid combination — thinking works inside batch requestsHighest quality, lowest per-call cost; highest latency — for background deep-analysis workloads

The most powerful combination for document-processing MCP tools is the full five-feature stack: prompt caching on the system prompt + Files API for the document + cache_control on the document block (within a synchronous call) + extended thinking when reasoning depth matters + Batch API when volume justifies async latency. Each feature works independently, meaning you can adopt them incrementally — start with prompt caching (easiest, lowest risk), add Files API (requires content-hash cache infrastructure), add extended thinking (requires temperature and max_tokens adjustment), and add Batch API last (requires two-tool design and result persistence logic).

Integration with AliveMCP Health Monitoring

MCP servers that use Anthropic advanced features introduce new health probe dimensions that a standard HTTP ping cannot detect. The uptime probe at AliveMCP pings your server's health endpoint every 60 seconds — but for Claude-powered tools, the health endpoint must be extended to probe the Anthropic SDK integration itself, not just the HTTP listener.

For prompt caching: the health probe should send a warmup request and log whether it received cache_creation_input_tokens or cache_read_input_tokens. A probe that consistently shows cache_creation_input_tokens > 0 after the first warm-up means the cache is not persisting — the breakpoint may be misplaced, or the prompt has drifted below 1,024 tokens due to a code change. For Files API: the health probe should verify that the local SQLite content-hash cache is not full and that a test upload succeeds (to detect quota exhaustion before it affects production calls). For Batch API: the health probe should list recent batches and alert if any batch older than 2 hours has not yet reached ended status — a sign of Anthropic-side queue pressure. For extended thinking: the health probe should check that the temperature: 1 constraint is still in the handler code (a linting rule, not a runtime check — but caught early by a CI step that greps for temperature near budget_tokens).

Wiring a AliveMCP check to your tool server's health endpoint gives you the 60-second ping cadence for HTTP-level liveness. The application-level SDK health checks above run on a separate slower cadence (every 5–15 minutes) and report to your internal metrics system — together, they cover both the infrastructure layer and the AI feature layer.

Summary

Five Anthropic SDK advanced features — prompt caching, Files API, Batch API, extended thinking, and vision and multimodal inputs — map to three structural design decisions in every Claude-powered MCP tool. The cost efficiency architecture decision: which features compose into a cost stack appropriate for the tool's usage pattern (caching for frequent calls with a stable system prompt; Files API for large documents queried multiple times; Batch API for non-interactive volume workloads). The input modality handling decision: which content block type correctly delivers each input format to the model (type: 'image' for images with base64 or public URL; type: 'document' for PDFs; source: { type: 'file', file_id } for Files API references — and never a file_id as a text string or a PDF in an image block). The latency-vs-depth trade-off decision: which calling mode — synchronous standard, synchronous extended thinking, or async batch — matches the response time budget and quality requirements of the calling context, with the same task potentially warranting different modes in IDE interactive versus nightly CI contexts.

The fourteen failure modes in the table above are not theoretical: each reflects a behavior where the API returns HTTP 200 or a superficially correct response while silently not delivering the intended feature. Cache metrics at zero when caching was believed to be active. Batch results returning an empty list on a completed batch that was polled before the processing_status was checked. Thinking blocks in tool response content causing MCP client crashes. Data URI prefixes causing silent base64 decode failures. Files API file_ids passed as text strings receiving the literal ID as the model's input. Treating these as known failure modes rather than surprises — and building the corresponding guards (metrics logging, status checks before result retrieval, response content filtering, data URI stripping) into every handler that uses these features — is what separates a Claude-powered MCP tool that works reliably in production from one that works in development but produces intermittent issues under real workloads.