Building with the Claude API, part 3: Claude features and MCP
Modules 6 and 7: thinking, images and PDFs, citations, caching and MCP.
Series Claude Academy: Build with the API Part 3 of 6
- Building with the Claude API, part 1: Requests, evals and prompting
- Building with the Claude API, part 2: Tool use and RAG
- Building with the Claude API, part 3: Claude features and MCP
- Building with the Claude API, part 4: Claude Code, computer use and agents
- Claude with Amazon Bedrock
- Claude on Google Cloud's Vertex AI
A walkthrough of the Claude Academy course Building with the Claude API (Build with the API track, 67 lessons, 8 quizzes), split into four parts; this is part 3. Each lesson is summarised with my own tables and examples; take the course for the full lessons. All the courses are listed in the Claude Academy course index.
Note: This summary comes mainly from the public lesson text. A few details were added from docs.claude.com or the sibling Bedrock/Vertex courses, such as reranking, contextual retrieval and adaptive thinking. Verify those details against the lessons and current docs.
6. Features of Claude
Extended thinking
- When thinking is enabled, the response contains a
thinkingblock, Claude’s reasoning, followed by atextblock. - Cost and latency: you pay for thinking tokens, and responses are slower. Optimize the prompt and run evals first. Enable thinking only if accuracy still falls short.
- Each
thinkingblock has asignature. It stops anyone tampering with the reasoning when you send it back in later turns. - If safety systems flag the reasoning, you get a
redacted_thinkingblock instead. It is encrypted, but you can pass it back unchanged so Claude keeps its context. - Thinking is not compatible with prefill and limits
temperaturesettings. - To test that your code handles redacted thinking, put a special trigger string in the message. The string is given in the lesson.
response = client.messages.create(
model=model, max_tokens=4000, # must exceed budget
thinking={"type": "enabled", "budget_tokens": 1024}, # min 1024
messages=messages,
)
for block in response.content:
print(block.type) # "thinking" / "redacted_thinking" / "text"
- Newer models (per the sibling course and docs) use adaptive thinking:
thinking={"type": "adaptive"}, guided by aneffortparameter. On the newest models,budget_tokensis deprecated or rejected. Claude 4.5-era models such as Haiku 4.5 still usebudget_tokens.
Images (vision)
| Limit | Value |
|---|---|
| Images per request | Up to 100 |
| Size | 5 MB per image |
| Dimensions | 8000 px for a single image; 2000 px each when sending multiple |
| Token cost | ≈ width × height / 750 |
| Source | base64 (or URL) |
import base64
with open("image.png", "rb") as f:
data = base64.standard_b64encode(f.read()).decode("utf-8")
add_user_message(messages, [
{"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": data}},
{"type": "text", "text": "What do you see in this image?"},
])
- Prompt engineering applies to images too. In the course’s marble example, a simple prompt counted 13 marbles when there were 12. Adding step-by-step counting instructions plus a verification step, or a one-shot example image, fixed it.
- The fire-risk example uses satellite images and a numbered analysis rubric:
- Find the residence.
- Check how much the trees overhang the roof.
- Assess the fire risk.
- Check the defensible space around the house.
- Give a 1–4 rating.
PDFs
with open("earth.pdf", "rb") as f:
pdf = base64.standard_b64encode(f.read()).decode("utf-8")
add_user_message(messages, [
{"type": "document", "source": {"type": "base64", "media_type": "application/pdf", "data": pdf}},
{"type": "text", "text": "Summarize the document in one sentence"},
])
- Claude reads the text, images, charts and tables inside the PDF.
Citations
- To enable citations, add
"title"and"citations": {"enabled": True}to the document block. - Each citation has these fields:
cited_textdocument_indexdocument_titlestart_page_number/end_page_number
- Plain-text sources (
{"type": "text", "media_type": "text/plain", "data": …}) returnCitationCharLocation, which uses character positions instead of pages. - Use citations to build a UI with numbered references, hover cards and highlighted source text. This builds user trust and lets users verify answers.
Prompt caching
- Normally the model throws away its input processing after each request. Caching stores that work:
- The first request writes to the cache.
- Follow-up requests read from it, which is cheaper and faster.
- The cache lasts 5 minutes and is refreshed each time it is used. A 1-hour TTL is available at a higher write cost.
Rules
- Caching is not automatic. You add cache breakpoints with
"cache_control": {"type": "ephemeral"}, and they must go on longhand content blocks. - Everything up to and including the breakpoint is cached. Content after the breakpoint is not.
- The cached prefix must be identical on later requests. Changing one character, such as adding “Please”, invalidates the cache.
- The cache is built in this order: tools → system → messages. You can use up to 4 breakpoints.
- Breakpoints can go on the system prompt, the last tool, images, documents, text, and tool_use or tool_result blocks.
- There is a minimum cacheable length: 1024 tokens for many models, and 4096 for Haiku 4.5 as stated in the course. Content below the minimum is silently not cached.
- Check
usage.cache_creation_input_tokens(written) andusage.cache_read_input_tokens(read). A single request can both read and write, for example when the tools are unchanged but the system prompt is new.
params = {"model": model, "max_tokens": 1000, "messages": messages}
if tools:
tools_clone = tools.copy()
last = tools_clone[-1].copy() # don't mutate the original schema
last["cache_control"] = {"type": "ephemeral"}
tools_clone[-1] = last
params["tools"] = tools_clone
if system:
params["system"] = [{"type": "text", "text": system,
"cache_control": {"type": "ephemeral"}}]
Code execution and the Files API
- Files API: upload a file once, get a
file_idback, and reference it in later messages instead of re-sending the bytes. - Code execution tool:
{"type": "code_execution_20250522", "name": "code_execution"}is server-side. Anthropic runs Python in an isolated Docker container with no network access. - To combine them, upload a CSV (the course uses streaming-service churn data), reference it with
{"type": "container_upload", "file_id": …}, and ask Claude to analyze it and make plots. - The response contains
server_tool_useblocks and code-execution result blocks. Generated files, such as plots, come back as file IDs that you download through the Files API. - These features have required beta headers, for example for the files API and code execution. Check the current docs for tool versions and headers.
7. Model Context Protocol (MCP)
- MCP is a communication layer. It moves tool definitions and execution off your server and onto MCP servers, which are often written by the service owner (GitHub, Sentry and so on).
- MCP is not a replacement for tool use. It changes who writes the tools. Claude still uses ordinary tool calling.
- An MCP client is transport-agnostic: it can use stdio (common when running locally), HTTP or WebSockets.
- Key messages:
ListToolsRequest/ListToolsResultCallToolRequest/CallToolResult- Equivalent messages for resources and prompts.
- Request flow:
- The user query reaches your server.
- The MCP client lists the available tools.
- Claude is called with those tools.
- Claude returns a
tool_use. - The client sends
CallToolRequestto the MCP server. - The result goes back to Claude.
- Claude gives the final answer.
Course project: CLI document chatbot
- Download
cli_project.zip, which containsmain.py,mcp_client.pyandmcp_server.py. Put your API key in.env. - Run it with
uv run main.py(orpython main.py). Check that a simple question such as “what’s 1+1?” works before you add MCP features. - In real projects you usually build either the client or the server, not both.
# mcp_server.py
from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp.prompts import base
from pydantic import Field
mcp = FastMCP("DocumentMCP", log_level="ERROR")
docs = {"deposition.md": "...", "report.pdf": "...", "plan.md": "..."}
@mcp.tool(name="read_doc_contents", description="Read the contents of a document and return it as a string.")
def read_document(doc_id: str = Field(description="Id of the document to read")):
if doc_id not in docs:
raise ValueError(f"Doc with id {doc_id} not found")
return docs[doc_id]
@mcp.resource("docs://documents", mime_type="application/json") # direct resource
def list_docs() -> list[str]:
return list(docs.keys())
@mcp.resource("docs://documents/{doc_id}", mime_type="text/plain") # templated resource
def fetch_doc(doc_id: str) -> str:
return docs[doc_id]
@mcp.prompt(name="format", description="Rewrites the contents of the document in Markdown format.")
def format_document(doc_id: str = Field(description="Id of the document to format")) -> list[base.Message]:
return [base.UserMessage(f"Reformat the document <document_id>{doc_id}</document_id> in markdown...")]
if __name__ == "__main__":
mcp.run(transport="stdio")
- MCP Inspector: run
mcp dev mcp_server.pyto open a browser UI where you can test tools, resources and prompts without Claude. - Client: the
MCPClientclass wraps aClientSessionand is used asasync with MCPClient(command="uv", args=["run", "mcp_server.py"]) as c:. Its methods:list_tools()returnssession.list_tools().tools.call_tool(name, input)returnssession.call_tool(...).read_resource(uri)callssession.read_resource(AnyUrl(uri)), takescontents[0], and parses JSON whenmimeType == "application/json".list_prompts()andget_prompt(name, args)return the prompt’s.messages.
- In the CLI, resources power @mentions: the selected document’s content is injected into the prompt. Prompts power /commands such as
/format plan.md.
Which primitive?
| Primitive | Controlled by | Use when | Example |
|---|---|---|---|
| Tools | Model | You want to give Claude new capabilities | Claude runs JS to compute √3 |
| Resources | App | You want to get data into your app’s UI or context | Autocomplete, “Add from Google Drive” |
| Prompts | User | You want to offer predefined workflows | Slash commands, starter buttons |