Learn/Claude Academy: Build with the APIguide

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
  1. Building with the Claude API, part 1: Requests, evals and prompting
  2. Building with the Claude API, part 2: Tool use and RAG
  3. Building with the Claude API, part 3: Claude features and MCP
  4. Building with the Claude API, part 4: Claude Code, computer use and agents
  5. Claude with Amazon Bedrock
  6. 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 thinking block, Claude’s reasoning, followed by a text block.
  • 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 thinking block has a signature. 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_thinking block instead. It is encrypted, but you can pass it back unchanged so Claude keeps its context.
  • Thinking is not compatible with prefill and limits temperature settings.
  • 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 an effort parameter. On the newest models, budget_tokens is deprecated or rejected. Claude 4.5-era models such as Haiku 4.5 still use budget_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:
    1. Find the residence.
    2. Check how much the trees overhang the roof.
    3. Assess the fire risk.
    4. Check the defensible space around the house.
    5. 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_text
    • document_index
    • document_title
    • start_page_number / end_page_number
  • Plain-text sources ({"type": "text", "media_type": "text/plain", "data": …}) return CitationCharLocation, 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) and usage.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_id back, 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_use blocks 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 / ListToolsResult
    • CallToolRequest / CallToolResult
    • Equivalent messages for resources and prompts.
  • Request flow:
    1. The user query reaches your server.
    2. The MCP client lists the available tools.
    3. Claude is called with those tools.
    4. Claude returns a tool_use.
    5. The client sends CallToolRequest to the MCP server.
    6. The result goes back to Claude.
    7. Claude gives the final answer.

Course project: CLI document chatbot

  • Download cli_project.zip, which contains main.py, mcp_client.py and mcp_server.py. Put your API key in .env.
  • Run it with uv run main.py (or python 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.py to open a browser UI where you can test tools, resources and prompts without Claude.
  • Client: the MCPClient class wraps a ClientSession and is used as async with MCPClient(command="uv", args=["run", "mcp_server.py"]) as c:. Its methods:
    • list_tools() returns session.list_tools().tools.
    • call_tool(name, input) returns session.call_tool(...).
    • read_resource(uri) calls session.read_resource(AnyUrl(uri)), takes contents[0], and parses JSON when mimeType == "application/json".
    • list_prompts() and get_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