call_tool#
call_tool invokes one tool by name with a JSON arguments object. Every call runs the full governed path — there is no MCP-only shortcut around authorization, approval, or sanitization.
What happens on a call#
In order:
- Entitlement gate. A non-entitled brand (not on Growth/Enterprise) gets a
permission error immediately — no further work.
- Rate limit. The per-api-key and per-brand limits are checked
(fail-closed). Over-limit returns a retryable error. See Rate limits.
- Reachability / scope pre-flight. The tool must pass the same filter as
list_tools: exposable, non-sensitive, and within the key's scopes. If not, you get a generic permission error — the server does not reveal whether the tool exists, is disabled, is sensitive, or is just out of scope.
- Governed execution. The call goes through tool lookup → policy evaluation
(default-deny, exact scope match) → approval gate → execution.
- Approval gate. If the tool requires approval, the call does not
execute. An approval request is queued and a permission error is returned that includes the approval request id (poll/await out-of-band).
- Result envelope. Success returns the tool payload; failure returns an
error_class and a sanitized_error (the only error text that ever crosses the boundary — never policy internals, provider errors, PII, or key material).
Request#
The standard MCP tools/call request:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "get_order_status",
"arguments": { "order_id": "ord_12345" }
}
}name— the tool name fromlist_tools.arguments— a JSON object validated against the tool'sinputSchema. Invalid
input returns a validation error.
Response#
Success#
Over the MCP wire, a successful call returns a content array whose text is the JSON-encoded tool payload, with isError: false:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{ "type": "text", "text": "{\"order_id\":\"ord_12345\",\"status\":\"shipped\"}" }
],
"isError": false
}
}The underlying result envelope also carries a truncated flag: when true, the payload was clipped to a size/row ceiling and should carry a cursor for pagination.
Error#
A failed call returns the sanitized message as text with isError: true:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{ "type": "text", "text": "Tool not found or not available with your current api key." }
],
"isError": true
}
}Internally each failure also carries one of five error_class values (permission, validation, terminal, retryable, dependency) — see Errors & error classes for how to branch on them.
Examples#
TypeScript#
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
const transport = new StdioClientTransport({
command: "qaqnuz-mcp",
args: [],
env: { QAQNUZ_API_KEY: process.env.QAQNUZ_API_KEY! },
});
const client = new Client({ name: "my-integration", version: "1.0.0" });
await client.connect(transport);
const res = await client.callTool({
name: "get_order_status",
arguments: { order_id: "ord_12345" },
});
if (res.isError) {
// res.content[0].text is a sanitized, safe-to-log message.
console.error("call_tool failed:", res.content[0]?.text);
} else {
const payload = JSON.parse(res.content[0]?.text ?? "null");
console.log("status:", payload.status);
}
await client.close();Python#
import json
import os
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
server = StdioServerParameters(
command="qaqnuz-mcp",
args=[],
env={"QAQNUZ_API_KEY": os.environ["QAQNUZ_API_KEY"]},
)
async def main():
async with stdio_client(server) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
result = await session.call_tool(
"get_order_status",
arguments={"order_id": "ord_12345"},
)
text = result.content[0].text if result.content else "null"
if result.isError:
print("call_tool failed:", text)
else:
payload = json.loads(text)
print("status:", payload["status"])Billing & attribution#
- A billable call is one
call_toolthat reaches execution.list_toolsand
any auth / permission / rate-limit rejection are not billable.
- Every reachable call is attributed to your specific api key in the audit trail,
so usage is traceable per key.
Notes#
call_toolnever throws at the protocol layer — all outcomes (including
auth, scope, and rate-limit failures) come back as an enveloped result with isError set appropriately.
- Approval-gated tools (
write/externalwith approval required) will not
execute synchronously; treat the returned approval id as the handle to track the decision.