Tool calling and MCP still need execution boundaries
Define input, output, error, and permission contracts and practice with an offline tool dispatcher.
Design a validated, authorized tool interface and understand what MCP does and does not provide.
Reliable tool use comes from contracts rather than how plausible a model's output looks. The model proposes a tool and arguments; the application decides whether execution is allowed.
MCP standardizes interfaces such as tool discovery and invocation. Interoperability does not automatically grant resource permission or provide business idempotency.
Define four contracts
The input contract defines fields, types, limits, and required values. The result contract describes success. The error contract separates invalid input, denied access, transient failure, and uncertain outcomes. The permission contract limits who can act on which resources.
Tool names should describe a clear action. Combining reading and writing behind an ambiguous name makes selection and authorization harder.
For side effects, also define an operation identifier, retry conditions, and required confirmation. A valid JSON schema solves only the structural part of input validation.
Let the executor make the final decision
Send model output through a registered dispatcher. Do not directly execute a generated function name or pass output to eval or an unrestricted shell.
Validate arguments and authorization again on the server. Neither client-side checks nor model instructions replace this boundary. Also limit returned size, structure, and sensitive fields.
Practice dispatch locally
This Python example implements one local word-count tool. It is not an MCP server. It demonstrates an allowlist and argument checks, with the allowed tool set supplied by the application.
def dispatch(call, allowed_tools):
if not isinstance(call, dict) or set(call) != {"name", "arguments"}:
raise ValueError("invalid call")
name = call["name"]
if name != "word_count" or name not in allowed_tools:
raise PermissionError("tool not allowed")
args = call["arguments"]
if not isinstance(args, dict) or set(args) != {"text"}:
raise ValueError("invalid arguments")
text = args["text"]
if not isinstance(text, str) or len(text) > 2000:
raise ValueError("invalid text")
return {"count": len(text.split())}
result = dispatch({
"name": "word_count", "arguments": {"text": "small useful tools"}
}, {"word_count"})
assert result == {"count": 3}
try:
dispatch({"name": "run_shell", "arguments": {}}, {"word_count"})
except PermissionError:
print("Unregistered tool rejected")
else:
raise AssertionError("unsafe tool accepted")
A real word-count contract must specify language and tokenization. Whitespace splitting is sufficient for this demonstration, not a claim of accurate counting for every language.
MCP connects systems; applications authorize actions
Check the current protocol version, capability declarations, tool list, and input and output schemas. A tutorial fragment is not a complete protocol message. Real requests also need the metadata required by the selected version.
Tool results can contain untrusted instructions too. They remain external data rather than system rules. Easier connections make clear resource boundaries more necessary, not less.
Try it next
Begin with one read-only tool and write its input, result, and error contracts. Add cases for unknown fields, oversized input, and an unauthorized tool.
Only then add a side-effecting operation with an identifier, confirmation rule, and recovery flow. The next guide addresses continuing a task after interruption without blindly repeating an action.
Sources & further reading
Sources checked on 2026-11-10. Original engineering explanation; examples are teaching data, not production measurements.