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 caching — cache_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 API — anthropic.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 thinking — thinking: { 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 type | Calling context | Recommended mode | Key features |
|---|---|---|---|
| Code review in IDE | Developer waiting <2s | Synchronous, no thinking | Prompt caching on style rules |
| Nightly code quality sweep | CI pipeline, 30 min budget | Batch + extended thinking | Batch API + budget_tokens 8k |
| Contract analysis — single question | User in web UI | Synchronous | Files API + prompt caching |
| Contract analysis — 500 contracts | Legal team monthly sweep | Batch | Batch API + Files API (reuse) |
| Architecture risk analysis | Developer reviewing design doc | Synchronous + thinking | budget_tokens 12k + streaming |
| Screenshot accessibility audit | CI pipeline, 5 min budget | Batch | Batch API + image downscaling |
| Document Q&A (same doc, many questions) | Interactive session | Synchronous | Files API + prompt cache on doc |
| Security vulnerability scan | Security team quarterly review | Batch + thinking | Batch + budget_tokens + Files API |
| Support ticket classification | Async pipeline, hours acceptable | Batch | Batch API + Haiku model |
| PDF table extraction | User uploads, expects <10s | Synchronous | Document 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.
| Feature | Symptom | Root cause | Fix |
|---|---|---|---|
| Prompt caching | cache_read_input_tokens stays 0 after first call | System prompt under 1,024 tokens — caching silently skipped | Expand static content until prefix exceeds 1,024 tokens |
| Prompt caching | Cache hit rate drops to 0% intermittently | Tool called less frequently than once per 5 min — TTL expires between calls | Warmup request at startup; schedule keep-alive if call frequency < 1/5min |
| Prompt caching | Every call shows cache_creation, never cache_read | cache_control placed on dynamic user message — different key each call | Move breakpoint to last static element in system array, before user content |
| Files API | Same PDF uploaded multiple times, quota accumulating | No content-hash dedup — API does not deduplicate uploads | SHA-256 content hash cache in SQLite; check before every upload call |
| Files API | Model receives "file_abc123" as text, not document content | file_id passed as text string in message content instead of document block | Use { type: 'document', source: { type: 'file', file_id } } block format |
| Files API | Upload fails with 413 error | Per-account storage quota exceeded from accumulated stale files | Run purgeOldFiles() on timer; delete files when session ends |
| Batch API | batches.results() returns empty — no output | Called before batch reaches 'ended' status — empty iterator with no error | Check batch.processing_status === 'ended' before calling .results() |
| Batch API | API rejects batch with 400 — duplicate custom_id | Two items in the same batch share the same custom_id | Deduplicate with Set before calling batches.create() |
| Batch API | Results not retrievable — expired | Batch results expire 24 hours after creation | Persist results to own storage (SQLite/S3) immediately when batch ends |
| Extended thinking | 400 error on API call | temperature not set to exactly 1, or max_tokens ≤ budget_tokens | Set temperature: 1 and max_tokens = budget_tokens + 2_048 |
| Extended thinking | MCP client crashes or shows raw JSON | Thinking blocks included in tool response content | Filter response.content to only type === 'text' blocks before returning |
| Extended thinking | Follow-up answers worse than first turn | Thinking blocks stripped from history before next API call | Include full response.content (thinking + text) in assistant history |
| Vision | 400 error: "unsupported media type" | PDF sent as type: 'image' block instead of type: 'document' | PDFs always use type: 'document'; images use type: 'image' |
| Vision | Base64 decode error from API | Data URI prefix (data:image/png;base64,) not stripped before sending | data.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 A | Feature B | Interaction | Combined effect |
|---|---|---|---|
| Prompt caching | Files API | Complementary — both reduce cost independently | Cache system prompt; Files API avoids re-upload; add cache_control to document block for two-layer caching |
| Prompt caching | Extended thinking | Independent — both work simultaneously | Cache the large system prompt; pay thinking cost only for the reasoning phase |
| Prompt caching | Batch API | Partial — prompt caching may not apply in Batch context | Batch requests share API infrastructure but not necessarily the same cache pool; test empirically |
| Files API | Extended thinking | Additive — file upload savings stack with thinking quality | Upload document once; apply thinking budget for deep analysis |
| Files API | Batch API | Complementary — upload once; reference in each batch item | Same file_id in all batch request params; avoids per-item upload overhead |
| Vision (base64) | Prompt caching | Independent — image tokens not cached, system prompt tokens are | Cache system instructions; pay full image token cost per call |
| Vision (URL source) | Files API | Alternative strategies — both avoid re-transmitting bytes | URL source if image is publicly accessible; Files API if image is private or frequently reused |
| Extended thinking | Batch API | Valid combination — thinking works inside batch requests | Highest 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.