MCP Tools (Primitive)

MCP

Definition

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?

PrimitiveWho decides when it's usedTypical use
ToolsThe model — autonomously, based on taskActions: query DB, open issue, send email
ResourcesThe application — pre-loads contextRead-only data injection: file contents, docs
PromptsThe user — explicitly triggers a templateSlash 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

AspectMCP Tools primitiveNative API tool_use
Who defines the schemaMCP server (JSON Schema)Application (inline in API call)
DiscoveryClient calls tools/list at session startApplication hard-codes tool list per request
Execution pathClient → MCP server → tools/call → resultApplication handles result directly
PortabilityAny MCP client can use same serverTied to one application's API call
Control modelModel decides when to invokeModel 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
# 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_id

Frequently 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.