MCP Tools (Primitive)
MCPDefinition
The model-controlled MCP primitive. Claude autonomously decides when to invoke MCP tools based on task requirements. Distinct from Resources (app-controlled) and Prompts (user-invoked). The invocation path mirrors the API's native tool_use → tool_result exchange.
Example Usage
Expose a run_sql_query tool via MCP so Claude can autonomously retrieve data it needs, rather than the application pre-fetching everything before each request.
Why It Matters for the CCA-F Exam
Tools are the most frequently tested MCP primitive. Expect questions that ask you to distinguish when to use a Tool (model decides, action-oriented) versus a Resource (app provides, read-only context injection). Also expect questions about the tool_use_id pairing requirement and how tool descriptions affect model behaviour.
In Depth
MCP defines three primitive capability types. Understanding which primitive to use for a given integration need is a core exam skill. This page focuses on the Tools primitive — the model-controlled one.
The Control Model: Who Decides?
| Primitive | Who decides when it's used | Typical use |
|---|---|---|
| Tools | The model — autonomously, based on task | Actions: query DB, open issue, send email |
| Resources | The application — pre-loads context | Read-only data injection: file contents, docs |
| Prompts | The user — explicitly triggers a template | Slash commands, reusable prompt fragments |
Tools are the right primitive whenever the timing and arguments of an operation depend on how the task is unfolding mid-conversation. The model reads the tool's description and decides whether to call it, what arguments to supply, and how to incorporate the result. The application does not schedule this — that autonomy is precisely what makes Tools powerful and why their descriptions must be precise.
The tool_use → tool_result Round-Trip
When the model invokes an MCP tool, the conversation exchange looks like this at the JSON level:
Model output (tool invocation):
{
"type": "tool_use",
"id": "toolu_01XyZ",
"name": "run_sql_query",
"input": { "query": "SELECT count(*) FROM orders WHERE status = 'open'" }
}The MCP client receives this block, routes it to the server's tools/call endpoint, and returns:
Application reply (tool result):
{
"type": "tool_result",
"tool_use_id": "toolu_01XyZ",
"content": [{"type": "text", "text": "{\"count\": 47}"}]
}The tool_use_id must exactly match the id from the invocation block — the model uses this pairing to correlate results with the right tool call. This is identical to native API tool_use mechanics; MCP simply adds server-side execution between the two steps.
Why Description Quality Determines Reliability
Because the model selects and parameterises tools from their descriptions alone, a vague description is a reliability bug. Good tool descriptions answer three questions: *What does this tool do?* *When should the model use it?* *What should the model NOT use it for?* The third question is often skipped, leading to the model calling a mutation tool when it should have called a read-only one.
How It Compares
| Aspect | MCP Tools primitive | Native API tool_use |
|---|---|---|
| Who defines the schema | MCP server (JSON Schema) | Application (inline in API call) |
| Discovery | Client calls tools/list at session start | Application hard-codes tool list per request |
| Execution path | Client → MCP server → tools/call → result | Application handles result directly |
| Portability | Any MCP client can use same server | Tied to one application's API call |
| Control model | Model decides when to invoke | Model decides when to invoke (same) |
Example
Tool description is the contract between the server author and the model. The explicit 'Do NOT use for INSERT/UPDATE/DELETE' clause prevents the model from attempting writes through a read-only tool.
# Python MCP server — tool declaration with explicit when-to-use / when-NOT-to-use guidance
from mcp.server import Server
from mcp.types import Tool, TextContent
app = Server("analytics-server")
@app.list_tools()
async def list_tools():
return [
Tool(
name="run_sql_query",
description=(
"Execute a read-only SQL SELECT query against the analytics database and return "
"results as a JSON array. Use when the user asks for metrics, counts, or data "
"that requires a database lookup. "
"Do NOT use for INSERT, UPDATE, or DELETE — this tool only accepts SELECT statements."
),
inputSchema={
"type": "object",
"properties": {
"query": {"type": "string", "description": "A SQL SELECT statement"}
},
"required": ["query"]
}
)
]
@app.call_tool()
async def call_tool(name: str, arguments: dict):
if name == "run_sql_query":
rows = await db.execute_read(arguments["query"])
return [TextContent(type="text", text=json.dumps(rows))]
# The client will wrap this result in a tool_result block with the matching tool_use_idFrequently Asked Questions
Is there a difference between MCP Tools and the API's tool_use blocks?
Mechanically they work the same way — the model emits a tool_use block, gets a tool_result back. The difference is in the infrastructure layer: MCP Tools are defined on a server process that the client discovers at session start, while native API tool_use schemas are supplied inline per request. MCP adds portability and server-side lifecycle management.
Why does the description of a Tool matter so much?
Because the model selects and parameterises tools based on their descriptions alone. A poorly written description leads to wrong tool selection, incorrect parameter construction, or the model calling a tool when it should not. Think of the description as the tool's contract with the model — it should describe what the tool does, when to use it, and any important constraints.
Can a Tool return an error back to the model?
Yes. The tool_result block has an `is_error` field. When set to true, the model receives the error text as context and can decide how to proceed — retry with different arguments, ask the user for clarification, or abandon the subtask. Returning structured error messages (not raw exceptions) gives the model actionable information.