Agent Systems / Tool Protocol
MCP and Tool Calling: Constraining Model Output into Verifiable External Actions
MCP does not merely make a model smarter. It creates discoverable, validated, logged protocol boundaries between models and tools.
Mechanism Lab
Animation: how MCP turns tool calls into protocol contracts
The animation shows a host/model discovering a server’s tools, resources, and prompts; generating a tool call; passing schema validation; executing inside the server; and returning typed results or errors.
Step 1 / 5
Discover
The host uses an MCP client to discover server tools, resources, and prompts.
discover(server)Animation Control
Reduced-motion users receive the same step states without continuous motion.
01 / Intuition
Core Intuition
Without a tool protocol, the model can only emit free text. With a tool protocol, the model can select declared tools and produce schema-constrained arguments.
MCP separates host, client, server, tools, resources, and prompts. The model application does not hard-code every tool; it discovers server capabilities.
The important object is the contract: name, description, input schema, permissions, execution environment, output format, and error semantics.
For empirical research, MCP lets agents read files, query databases, run statistical scripts, and render tables while leaving structured traces.
02 / Math
Protocol contracts and tool-call state semantics
01 / Capability discovery
The host connects through an MCP client to a server and reads available tools, resources, and prompts.
C = discover(server) = {tools,resources,prompts}02 / Schema constraints
Every tool has an input schema. Model-generated arguments must satisfy types, required fields, and enumerations.
valid(args, schema_tool) = true03 / Action generation
A tool call maps model context into a structured executable action, not a natural-language promise.
a_t = f_theta(C,s_t) = {name,args}04 / Execution boundary
The server executes tools within its own permissions and runtime. Host apps should not execute model text as shell commands.
o_t = server.execute(a_t)05 / Result return
Results may be data, text, artifact references, or errors. Errors must return to the model and trace.
r_t in {data,text,artifact,error}06 / Audit log
Auditable systems record prior state, tool name, arguments, result, error, permission, and timestamp.
trace_t=(s_t,name,args,r_t,permission,time)03 / Code
Python demo: validating an MCP-style tool call with a schema
Real MCP is transported through the protocol. This pure-Python demo shows the same core idea: declare the tool contract, validate arguments, execute the tool, and return structured results.
from dataclasses import dataclass
from typing import Any, Callable
@dataclass
class Tool:
name: str
description: str
required: set[str]
types: dict[str, type]
handler: Callable[[dict[str, Any]], dict[str, Any]]
def run_regression(args: dict[str, Any]) -> dict[str, Any]:
outcome = args["outcome"]
treatment = args["treatment"]
controls = args.get("controls", [])
return {
"ok": True,
"table": "outputs/regression.csv",
"formula": f"{outcome} ~ {treatment} + {' + '.join(controls)}",
}
REGISTRY = {
"run_regression": Tool(
name="run_regression",
description="Estimate a simple regression and return a table path.",
required={"outcome", "treatment"},
types={"outcome": str, "treatment": str, "controls": list},
handler=run_regression,
)
}
def validate(tool: Tool, args: dict[str, Any]) -> None:
missing = tool.required - set(args)
if missing:
raise ValueError(f"missing required fields: {sorted(missing)}")
for key, expected_type in tool.types.items():
if key in args and not isinstance(args[key], expected_type):
raise TypeError(f"{key} must be {expected_type.__name__}")
def execute_tool_call(call: dict[str, Any]) -> dict[str, Any]:
tool = REGISTRY.get(call.get("name"))
if tool is None:
return {"ok": False, "error": "unknown tool"}
args = call.get("arguments", {})
try:
validate(tool, args)
result = tool.handler(args)
return {"ok": True, "tool": tool.name, "result": result}
except Exception as exc:
return {"ok": False, "tool": tool.name, "error": str(exc)}
call = {
"name": "run_regression",
"arguments": {
"outcome": "wage",
"treatment": "training",
"controls": ["age", "education"],
},
}
response = execute_tool_call(call)
print(response)04 / Case
Case: letting a research agent call statistical tools safely
- In StatsPAI, an agent may call run_stata, read_table, estimate_did, render_regression_table, or search_literature.
- Without a protocol boundary, the model may emit plausible shell commands while paths, arguments, permissions, and error handling remain uncontrolled.
- An MCP-style design first exposes tool lists and schemas. The model can only select existing tools and provide schema-valid arguments. The server executes; the host writes results and errors back into context.
- This makes auditing possible: which tool was called, with which arguments, what file came back, why it failed, and whether human confirmation was required.
05 / Risks
Common Pitfalls
References
- Model Context Protocol Documentationhttps://modelcontextprotocol.io/docs
- Model Context Protocol Specificationhttps://modelcontextprotocol.io/specification
- JSON Schema Documentationhttps://json-schema.org/learn/getting-started-step-by-step
- OpenAI Function Calling Guidehttps://platform.openai.com/docs/guides/function-calling