Your MCP server runs whatever a client calls. MCPServerGuard checks each call first, and a refused call never runs.
MCP server security in one line: MCPServerGuard checks every tool call before it runs. add is approved, an undeclared divide is refused.
The MCPServerGuard class checks every tool call your MCP server receives with AgentTrust before the tool runs. Guard the server once, then run a small calculator server with and without AgentTrust and watch a call get refused.
The client asks. The gateway answers. Only then does the tool run.
An MCP client, driven by a model, picks the tool and its arguments. The server runs it. In this guide's calculator server, add and multiply are the intended tools, yet a client can also call divide. Run the server with no check and divide runs: nothing stopped it. For a calculator that is harmless. Swap divide for process_payment or delete_file and it would run just the same.
One line after the server is created. MCPServerGuard wraps the server's tools so every call goes to AgentTrust first. By the end of this guide:
Blocked by AgentTrust reason, and the server keeps running.Prerequisites. Python 3.10 or later and the mcp package: version 1.x (FastMCP) or 2.x (MCPServer). The walkthrough needs no model and no key, because a client built into the example calls the tools. The local gateway needs no account; the console route does.
Package name. Install agentrust-py and import agentrust_sdk. The PyPI package agentrust-sdk belongs to another company.
The guard sits on the server, not in the client, so it covers every client that connects: the MCP Inspector, Claude Desktop, or your own agent.
ToolError, so the client receives an error result (isError true) carrying the reason.A tool with a typed return value, such as -> int, publishes an output schema, and a plain refusal string would fail it. An error result carries the reason for any tool. We confirmed the isError result with a real stdio client on both mcp versions.
The guard governs every tool already registered and every tool you register later. Give each server its own agent_id: the audit trail says who acted, and the manifest is matched by tool name, so two servers with the same tool name share one entry. An agent with no declared tools is not restricted, so every tool is allowed.
The helper works with FastMCP from mcp 1.x and MCPServer from mcp 2.x. These steps connect to your console. With no account, use the local gateway below instead.
pip install "agentrust-py[mcp]"
It accepts mcp>=1.2,<3.0.
In your console, select New agent and copy its API Key and Agent ID. The key starts with at_live_ and is shown once. Select the agent, then under Manifest & fingerprint select Declare and add the tools the server may run, with an optional limit for a tool that moves money, such as process_payment up to 100.
An agent with no declared tools is not restricted: every tool is allowed.
export AGENTRUST_GATEWAY_URL=https://console.agent-trust.tech
export AGENTRUST_KEY=at_live_...
export AGENTRUST_AGENT_ID=payments-mcp
Where: once, after the server is created. Your tools do not change.
+ from agentrust_sdk.adapters.mcp import MCPServerGuard
mcp = FastMCP("payments") # mcp 2.x: MCPServer
@mcp.tool()
async def process_payment(amount: float, account: str) -> dict: ...
+ MCPServerGuard(mcp, agent_id=os.environ["AGENTRUST_AGENT_ID"])
To guard a single tool instead, put @mcp_tool_guard(agent_id=...) under @mcp.tool(). The guarded function keeps its name, docstring and signature, so the client sees the same tool schema.
When a client calls a tool the manifest does not allow, or one over its limit, the tool does not run. The client gets an error result and the model can explain or try something else:
Error executing tool process_payment: Blocked by AgentTrust: Tool-trust hard gate: tool_trust: 'process_payment' amount 500.0 exceeds declared max_amount 100.0 (envelope …)
That line is an example of the wording, not a run from this guide. To raise BlockedError instead, pass on_block="raise".
Prefer async tools. The check for a sync tool blocks the server's event loop while the gateway answers. An async tool does not.
Skip the console steps and start a gateway inside your program before the first tool call:
pip install "agentrust-py[mcp,embedded]"
from agentrust_sdk.embedded import embed_gateway
gateway = embed_gateway()
gateway.register_manifest("payments-mcp", {
"declared_tools": [
{"name": "lookup_account"},
{"name": "process_payment", "max_amount": 100},
],
})
Set AGENTRUST_AGENT_ID=payments-mcp (or pass the ID straight to MCPServerGuard). Decisions last only while the program runs, so use this to try AgentTrust, in tests and in CI.
In agentrust-py 0.0.9, MCPServerGuard wraps the server's tools, where before it wrapped nothing. mcp_tool_guard now checks before the tool runs. Its block_on_block and framework arguments are gone: use on_block="raise".
The same steps, applied to a small calculator server. You run it three ways and watch a tool call get refused. About 10 minutes. Steps 1 to 4 need no model and no key. Steps 5 to 7 use an AgentTrust console account.
mkdir agentrust-quickstart && cd agentrust-quickstart
python -m venv .venv && source .venv/bin/activate # on Windows: .venv\Scripts\activate
pip install "agentrust-py[mcp,embedded]"
Save the program below as mcp_quickstart.py. It is the example examples/mcp_quickstart.py from the AgentTrust SDK repository.
"""
mcp_quickstart.py — an MCP server whose tool calls AgentTrust checks.
The server is a calculator: an MCP server with add, multiply and divide tools.
A client (Claude Desktop, the MCP Inspector, your own agent) calls them.
AgentTrust is one line (marked "AgentTrust" below): MCPServerGuard wraps the
server's tools. Every call is checked before it runs, and its result after. A
tool the agent may not use is refused before its code runs; the client gets an
error result that carries the reason, and the server keeps running. The agent
may use add and multiply, not divide.
RUN IT (in the folder that holds this file)
pip install "agentrust-py[mcp,embedded]"
python mcp_quickstart.py --without-agentrust # AgentTrust off: every tool runs
python mcp_quickstart.py --local # a gateway starts inside this program; no account
python mcp_quickstart.py --console # your AgentTrust console keeps every decision
python mcp_quickstart.py --serve --console # serve over stdio, for a real MCP client
Without --serve, a client built into this file calls the tools, so you see
the decisions with no model and no key.
--console needs three settings, from the agent you create in the console:
export AGENTRUST_GATEWAY_URL=https://console.agent-trust.tech
export AGENTRUST_KEY=at_live_...
export AGENTRUST_AGENT_ID=...
"""
from __future__ import annotations
import argparse
import asyncio
import os
import sys
import tempfile
try:
from mcp.server.fastmcp import FastMCP as Server # mcp 1.x
except ImportError:
from mcp.server.mcpserver import MCPServer as Server # mcp 2.x
from agentrust_sdk.adapters.mcp import MCPServerGuard
AGENT_ID = os.environ.get("AGENTRUST_AGENT_ID", "calculator-agent")
# The tools this agent may call. With --local they are set here; with --console
# you declare the same list on the agent in the console. divide is left out on purpose.
MANIFEST = {"declared_tools": [{"name": "add"}, {"name": "multiply"}]}
server = Server("calculator")
@server.tool()
def multiply(a: int, b: int) -> int:
"""Multiply `a` and `b`."""
return a * b
@server.tool()
def add(a: int, b: int) -> int:
"""Add `a` and `b`."""
return a + b
@server.tool()
async def divide(a: int, b: int) -> float:
"""Divide `a` by `b`."""
return a / b
# AgentTrust: every call to these tools is checked before it runs and after it
# returns. Tools registered after this line are checked too.
MCPServerGuard(server, agent_id=AGENT_ID)
CALLS = [("add", {"a": 3, "b": 4}), ("divide", {"a": 10, "b": 2})]
def _text(result) -> str:
"""The text a client reads from a call_tool result (mcp 1.x and 2.x shapes)."""
if hasattr(result, "content"):
return result.content[0].text
blocks = result[0] if isinstance(result, tuple) else result
return blocks[0].text
def call_tools(where: str) -> int:
"""Play a client: call each tool and print what comes back."""
print(f"AgentTrust: {where} · agent {AGENT_ID}")
for name, args in CALLS:
print(f"\n=== {name}({', '.join(f'{k}={v}' for k, v in args.items())}) ===")
try:
print(f"\nResult: {_text(asyncio.run(server.call_tool(name, args)))}")
except Exception as exc: # noqa: BLE001 - a refused call arrives as an error result
print(f"\nError result: {exc}")
return 0
def serve(where: str) -> int:
print(f"AgentTrust: {where} · agent {AGENT_ID} · serving over stdio", file=sys.stderr)
server.run()
return 0
# --without-agentrust: every tool runs. For comparison.
def run_without_agentrust(act) -> int:
os.environ["AGENTRUST_ENABLED"] = "false" # the SDK's off switch: wrapped tools pass every call through
return act("off (every tool runs)")
# --local: a gateway runs inside this program; no account needed.
def run_local(act) -> int:
from agentrust_sdk.embedded import embed_gateway
# SQLite in a temp dir, gone after the run. embed_gateway sets
# AGENTRUST_GATEWAY_URL and AGENTRUST_KEY, which MCPServerGuard reads.
gateway = embed_gateway(db_path=os.path.join(tempfile.mkdtemp(), "gateway.db"))
gateway.register_manifest(AGENT_ID, MANIFEST)
try:
return act(f"local gateway {gateway.url} (no account, nothing kept)")
finally:
gateway.stop()
# --console: the SDK sends every check to your console, which keeps the decisions.
def run_with_console(act) -> int:
missing = [v for v in ("AGENTRUST_GATEWAY_URL", "AGENTRUST_KEY", "AGENTRUST_AGENT_ID") if not os.environ.get(v)]
if missing:
print(f"Set {', '.join(missing)} (from the agent you created in the console), or use --local.",
file=sys.stderr)
return 2
console = os.environ["AGENTRUST_GATEWAY_URL"]
code = act(f"console {console}")
print(f"\nOpen Events in the console ({console}) and filter by agent ID {AGENT_ID}.", file=sys.stderr if act is serve else sys.stdout)
return code
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="The MCP calculator server, governed by AgentTrust.")
where = parser.add_mutually_exclusive_group()
where.add_argument("--local", action="store_true", help="run a gateway inside this program, no account (the default)")
where.add_argument("--console", action="store_true", help="send every check to your AgentTrust console")
where.add_argument("--without-agentrust", action="store_true", help="for comparison: AgentTrust off, every tool runs")
parser.add_argument("--serve", action="store_true", help="serve over stdio for a real MCP client instead of calling the tools here")
args = parser.parse_args(argv)
act = serve if args.serve else call_tools
if args.without_agentrust:
return run_without_agentrust(act)
return run_with_console(act) if args.console else run_local(act)
if __name__ == "__main__":
sys.exit(main())
First, see what the server does with no checks. A client built into the program calls add, then divide:
python mcp_quickstart.py --without-agentrust
AgentTrust: off (every tool runs) · agent calculator-agent
=== add(a=3, b=4) ===
Result: 7
=== divide(a=10, b=2) ===
Result: 5.0
divide ran and nothing stopped it.
This is the only change AgentTrust needs, and it is already in your file. Find the MCPServerGuard line after the tools are defined. On your own server it looks like this:
+ from agentrust_sdk.adapters.mcp import MCPServerGuard
server = Server("calculator")
# ... the @server.tool() functions ...
+ MCPServerGuard(server, agent_id=AGENT_ID)
Names, descriptions and argument types stay the same, so clients see what they saw before. Allowed calls run and their results are checked. Refused calls never run.
python mcp_quickstart.py --local
The program starts a small AgentTrust gateway inside itself and allows only the tools in MANIFEST: add and multiply. divide is not on the list. No account needed.
AgentTrust: local gateway http://127.0.0.1:8765 (no account, nothing kept) · agent calculator-agent
=== add(a=3, b=4) ===
Result: 7
=== divide(a=10, b=2) ===
Error result: Error executing tool divide: Blocked by AgentTrust: Tool-trust hard gate: action 'divide' is
not in this agent's declared_tools (envelope cde0c0da-14e8-46f3-b5e6-ce563e927557)
add returned 7. divide was refused before its code ran, and the client got an error result with the reason. Nothing is kept after the program exits.
To try it with a real client, serve it over stdio with python mcp_quickstart.py --serve --local and point the MCP Inspector or Claude Desktop at that command. We did not run a model or Claude Desktop against it.
To keep decisions and manage the agent from the console, register it there:
mcp-quickstart, and select Create.at_live_ and is shown only once.add and multiply, and select Save manifest.Step 3 is the same list MANIFEST holds in the program, now kept in the console. An agent with no declared tools is not restricted, so divide would run.
export AGENTRUST_GATEWAY_URL=https://console.agent-trust.tech # your console's address
export AGENTRUST_KEY=at_live_... # the API key
export AGENTRUST_AGENT_ID=mcp-quickstart # the Agent ID
python mcp_quickstart.py --console
The first line names the console, and the run should match the local one: add returns 7 and divide is refused with the same Blocked by AgentTrust reason.
Open Events in the console and type your Agent ID into Filter by Agent ID. You will see one row per decision: add approved before and after it ran, and divide blocked.

langgraph-quickstart. It is not a capture of the MCP run.We also ran claude-haiku-4-5 as the agent, text only, through the logged-in claude CLI, against the same guarded tools. It is not a real MCP client driven by a model. add ran. divide was refused and Haiku told the user it was blocked. A $40 refund ran. A $5,000 refund was refused and Haiku explained the $100 limit.
Guard the server once. Tools you add later are checked too.
The same server, without and with the one change: guarding it.
| Your server can… | Without | With |
|---|---|---|
| Be limited to the tools you allow (you ran this) | ✗ | ✓ |
| Have a call stopped before it runs (you ran this) | ✗ | ✓ |
| Tell the client why a call was refused, and keep running (you ran this) | ✗ | ✓ |
| Leave a record of every decision, with scores | ✗ | ✓ |
| Have a tool's result checked before it is returned | ✗ | ✓ |
| Cap risky amounts, such as a refund over 100 | ✗ | ✓ |
| Hold a risky call until a person approves it | ✗ | ✓ |
| Be stopped instantly with a kill switch | ✗ | ✓ |
| Refuse every call if AgentTrust can't be reached (optional) | ✗ | ✓ |
| Have tools registered after the guard checked too | ✗ | ✓ |
✗ means the MCP SDK does not provide it, so you would build it yourself. The first three rows are what you just ran. The others are covered by the SDK's tests (28 per mcp version, against a real embedded gateway) and the adapter guide (docs/framework-adapters/mcp.md). They were not run in this guide.
MCPServerGuard relies on the server's private _tool_manager and raises if it is not found.| Mechanism | What it checks |
|---|---|
MCPServerGuard | Every tool on the server, including tools registered later |
mcp_tool_guard | One tool. Put it under @mcp.tool(). |
Hold risky calls for a person. Set the agent's mode to Human Review. Rejected or timed-out calls do not run. See the MCP integration docs.
Serve it to a real client. Run python mcp_quickstart.py --serve --console and add the command to the MCP Inspector or Claude Desktop.
Calling Claude yourself? Claude Agent Guardrails: Block Unsafe Tool Calls Before They Run does the same for a tool loop. Or build a full governed bot in Guide: Stop Your AI Support Bot from Issuing a Bad Refund.
mcp>=1.2,<3.0: FastMCP from mcp 1.x and MCPServer from mcp 2.x.
A typed tool return, such as -> int, publishes an output schema, and a plain refusal string would fail it. The guard raises a ToolError, so the client gets an error result (isError true) carrying the reason.
Yes. The guard governs every tool already registered and every tool you register later.
MCPServerGuard used to wrap nothing; it now wraps the server's tools. mcp_tool_guard now checks before the tool runs, and its block_on_block and framework arguments are gone. Use on_block="raise".
Async. The check for a sync tool blocks the server's event loop while the gateway answers. An async tool does not.
No model and no key: a client built into the example calls the tools. The local gateway needs no account. The console route needs an AgentTrust console account.
Check every tool call before it runs, and keep a record of every decision.
See Pricing & Start Free →