Guide · Anthropic SDK Advanced Features
MCP Server Anthropic Files API — upload, file_id reuse, PDF handling
Three Files API behaviours catch MCP server authors off-guard: uploaded files are referenced by file_id across as many requests as needed — but they are scoped to your API key, not a specific user or session — any request using your API key can reference any file you've uploaded, so never store sensitive user documents as files without per-user API key isolation; files must be referenced using { type: 'document', source: { type: 'file', file_id: '...' } } in message content — not as a string or URL — passing the file_id as a string in the message text simply sends the ID as text to the model, not the file content; and the API does not deduplicate uploads — uploading the same PDF twice produces two file objects with two different IDs, both billed and both consuming storage quota, so your MCP server must track whether a document is already uploaded before calling anthropic.files.upload().
TL;DR
Upload files with anthropic.beta.files.upload({ file: ... }) (currently in beta). Reference them in messages as { type: 'document', source: { type: 'file', file_id: uploadedFile.id } }. Cache the file_id keyed on a content hash so the same document is never uploaded twice. Delete files when done with anthropic.beta.files.delete(file_id). Files API is most valuable when the same large document is queried multiple times — it saves re-transmitting the document bytes on every call.
Uploading a document and caching the file_id
The Files API accepts PDF, plain text, HTML, Markdown, CSV, and several image formats. The upload returns a file object with an id field — store this ID and reuse it for all subsequent messages that need to reference the same document. The primary benefit is bandwidth: a 500 KB PDF uploaded once can be referenced in dozens of messages without re-transmitting the bytes each time.
import Anthropic from '@anthropic-ai/sdk';
import { createHash } from 'crypto';
import { readFile } from 'fs/promises';
import Database from 'better-sqlite3';
import { z } from 'zod';
const anthropic = new Anthropic({ defaultHeaders: { 'anthropic-beta': 'files-api-2025-04-14' } });
const db = new Database('./data.db');
// Track uploads to avoid re-uploading the same document.
// Key = sha256 of file bytes; value = Anthropic file_id
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())
)`);
async function getOrUploadFile(
content: Buffer,
filename: string,
mimeType: string,
): Promise<string> {
const hash = createHash('sha256').update(content).digest('hex');
// Check local cache first
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;
// Not cached — upload to Anthropic Files API
const file = await (anthropic as any).beta.files.upload({
file: new File([content], filename, { type: mimeType }),
});
// Persist the mapping so future calls skip the upload
db.prepare('INSERT OR IGNORE INTO file_cache (content_hash, file_id, filename) VALUES (?, ?, ?)')
.run(hash, file.id, filename);
return file.id;
}
// MCP tool: analyse a PDF document, reusing file_id when the same doc is seen again
server.tool(
'analyze_document',
{
pdf_base64: z.string().describe('Base64-encoded PDF content'),
filename: z.string().default('document.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: 1024,
messages: [
{
role: 'user',
content: [
{
type: 'document',
source: { type: 'file', file_id },
// Optional: add cache_control here to also prompt-cache the doc context
// cache_control: { type: 'ephemeral' },
},
{ type: 'text', text: question },
],
},
],
});
const text = response.content.find((b: any) => b.type === 'text')?.text ?? '';
return { content: [{ type: 'text', text }] };
}
);
The File constructor is available globally in Node.js 20+. For older Node.js versions, use a Blob with the file bytes: new Blob([content], { type: mimeType }) and pass it with a filename property. The SDK sends a multipart/form-data upload request internally.
Using file_id in messages — correct content block format
Files are referenced using the document content block type with source.type === 'file'. The common mistake is passing the file ID as plain text in the message — the model receives the literal string "file_abc123" instead of the document content.
// CORRECT — references the file content
const correctMessage = {
role: 'user',
content: [
{
type: 'document',
source: { type: 'file', file_id: 'file_abc123' },
},
{ type: 'text', text: 'Summarise the key findings of this document.' },
],
};
// INCORRECT — sends the string "file_abc123" as the message
const wrongMessage = {
role: 'user',
content: 'Please summarise file_abc123',
};
// ALSO INCORRECT — source.type must be 'file', not 'url' or 'base64' when using file_id
const alsoWrong = {
role: 'user',
content: [
{
type: 'document',
source: { type: 'base64', data: 'file_abc123' }, // wrong — this should be actual base64
},
],
};
The document block type works for text-extractable files (PDF, TXT, HTML, MD, CSV). For images (JPEG, PNG, GIF, WEBP), use the image block type with source: { type: 'file', file_id: '...' }. Never mix the two — sending a PDF file_id in an image block returns a 400 error.
Listing and deleting files
Files persist until explicitly deleted or until account quota limits are reached. For an MCP server that processes user-uploaded documents, implement a cleanup tool that deletes files after the session ends to avoid accumulating stale data and incurring storage costs.
// Tool: list all uploaded files for this API key
server.tool(
'list_uploaded_files',
{ limit: z.number().int().min(1).max(100).default(20) },
async ({ limit }) => {
const files = await (anthropic as any).beta.files.list({ limit });
const summary = files.data.map((f: any) => ({
id: f.id,
filename: f.filename,
size_bytes: f.size,
created_at: f.created_at,
}));
return { content: [{ type: 'text', text: JSON.stringify(summary) }] };
}
);
// Tool: delete a file by ID
server.tool(
'delete_uploaded_file',
{ file_id: z.string().startsWith('file_') },
async ({ file_id }) => {
await (anthropic as any).beta.files.delete(file_id);
// Also remove from our local cache
db.prepare('DELETE FROM file_cache WHERE file_id = ?').run(file_id);
return {
content: [{ type: 'text', text: JSON.stringify({ deleted: true, file_id }) }],
};
}
);
// Automated cleanup: delete files older than 24 hours from our local cache,
// then delete them from Anthropic too.
async function purgeOldFiles(): Promise<void> {
const cutoff = Math.floor(Date.now() / 1000) - 86_400; // 24 hours ago
const stale = db.prepare('SELECT file_id FROM file_cache WHERE created_at < ?')
.all(cutoff) as Array<{ file_id: string }>;
for (const { file_id } of stale) {
try {
await (anthropic as any).beta.files.delete(file_id);
db.prepare('DELETE FROM file_cache WHERE file_id = ?').run(file_id);
} catch {
// File may already be deleted — ignore 404s
}
}
}
Run purgeOldFiles() on a timer (e.g. every 6 hours) to keep your quota usage in check. The Anthropic Files API enforces per-account storage limits — exceeding them causes upload requests to fail with a 413 error. Proactive cleanup avoids hitting this limit in production.
Combining Files API with prompt caching
Files API and prompt caching are complementary: the Files API avoids re-transmitting document bytes over the wire, while prompt caching avoids re-processing those bytes through the model's context on repeated queries. For maximum efficiency on a document that is queried many times, use both together.
// Combine file_id reference with cache_control on the document block
const response = await (anthropic as any).messages.create({
model: 'claude-sonnet-4-6',
max_tokens: 512,
messages: [
{
role: 'user',
content: [
{
type: 'document',
source: { type: 'file', file_id },
cache_control: { type: 'ephemeral' },
// ↑ Both: skip re-transmitting bytes (Files API)
// AND skip re-processing in the context window (prompt cache)
},
{ type: 'text', text: question },
],
},
],
});
When a document exceeds 1,024 tokens (virtually all PDFs), adding cache_control to the document block activates prompt caching for the document's token representation. The first call writes the cache entry; subsequent calls with the same file_id and cache_control hit the cache and pay 90% less for those input tokens. The Files API prevents re-upload; the prompt cache prevents re-tokenisation — together they make large-document MCP tools significantly cheaper to operate.