MCP Integration
MCPServerGuard checks every tool call on your MCP server with AgentTrust before the tool runs.
Guide · Python · MCP (Model Context Protocol)
Wrap your MCP server with one line and every tool call a client makes is checked before it runs. Then run a small calculator server with and without AgentTrust and watch a call get refused.
Tier: Developer+
What you will build
An MCP server lists tools that any client, or any model behind it, can call, and nothing stops a call you did not intend. The AgentTrust class MCPServerGuard wraps the server's tools so each call goes to AgentTrust first. By the end of this guide:
- Every tool call is checked before it runs, and its result after.
- A refused call never runs. The client gets an error result that carries the
Blocked by AgentTrustreason, so the model can read it, and the server keeps running. - Every decision, allowed or refused, shows under Events in your console.
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 needs an AgentTrust console account.
Package name. Install agentrust-py and import agentrust_sdk. The PyPI package agentrust-sdk belongs to another company.
Connect AgentTrust in 4 steps
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 instead.
Install the adapter
pip install "agentrust-py[mcp]"It accepts mcp>=1.2,<3.0.
Register the agent
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.
Do not skip the manifest. An agent with no declared tools is not restricted: every tool is allowed.
Point your server at the console
export AGENTRUST_GATEWAY_URL=https://console.agent-trust.tech
export AGENTRUST_KEY=at_live_...
export AGENTRUST_AGENT_ID=payments-mcpGuard the server
Where: once, after the server is created. The guard governs every tool already registered and every tool you register later. Your tools do not change.
+ import os
+ 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"])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.
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. The wording comes from the gateway; this one is an example, not a run from this page:
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 …)Why an error result and not text. A tool with a typed return value, such as -> int, publishes an output schema, and a plain refusal string would fail it. The guard raises a ToolError, so the client receives an error result (isError true) that carries the reason for any tool. 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.
MCPServerGuard reads the server's tool registry (_tool_manager). If it cannot find one it raises RuntimeError instead of leaving the tools unguarded; use @mcp_tool_guard on each tool then.
No account? Connect to a local gateway
Skip steps 2 and 3 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) and go to step 4. Decisions last only while the program runs, so use this to try AgentTrust, in tests and in CI.
Upgrading from before 0.0.9? MCPServerGuard used to wrap nothing; it now wraps the server's tools. mcp_tool_guard now checks before the tool runs. Its block_on_block and framework arguments are gone: use on_block="raise".
Calling Claude yourself? See the Claude quickstart. Not on MCP? Wrap any Python function with @harness: see the @harness quickstart.
See it in action
The same four steps, applied to a small calculator server. You run it three ways and watch a tool call get refused. About 10 minutes. Each step has one thing to do and one thing you should see.
Before you start. Python 3.10 or later. Steps 1 to 4 need nothing else: no model and no key. Steps 5 to 7 use an AgentTrust console account.
Create the project
Make a folder with its own Python environment and install what the program needs:
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]"Create a file named mcp_quickstart.py in that folder, paste the program below and save. It is the example examples/mcp_quickstart.py from the AgentTrust SDK repository. The tools (add, multiply, divide) are at the top, AgentTrust is one line (MCPServerGuard, step 3), a small client built into the file calls the tools so you need no model, and the ways to run it are at the bottom.
MCPServerGuard asks the AgentTrust gateway before the tool runs; the gateway records every check as an event.Here is what happens when the client calls add, then divide. The tool names come from the program you are about to create.
- A client calls a tool. The built-in client calls
add(ordivide) on the server. A real client such as the MCP Inspector would do the same over stdio. - The guard asks AgentTrust first.
MCPServerGuardsends the tool name and its arguments to the gateway. This is the pre-check, and your tool's code has not run. - The gateway decides and records it. It compares the call with the agent's declared tools:
addis declared,divideis not. Either way it writes an event. - Allowed: the tool runs. Refused: it never does.
add(3, 4)runs and its result goes back for a post-check, a second event.divideis raised as aToolErrorcarryingBlocked by AgentTrust. - The client gets a reply either way. It reads the result, or an error result (
isErrortrue) with the reason, and the server keeps running.
So an allowed call leaves two events (before and after it ran) and a refused call leaves one. With --console they appear under Events in step 7; with --local the gateway lives inside the program and forgets them when it exits.
See the whole program
mcp_quickstart.py
"""
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())Check that the file works:
python mcp_quickstart.py --helpRun it without AgentTrust
First, see what the server does with no checks. A client built into the program calls add, then divide:
python mcp_quickstart.py --without-agentrustAgentTrust: off (every tool runs) · agent calculator-agent
=== add(a=3, b=4) ===
Result: 7
=== divide(a=10, b=2) ===
Result: 5.0divide ran and nothing stopped it. For a calculator that is harmless. Swap divide for process_payment or delete_file and it would run just the same.
Guard the server
This is the only change AgentTrust needs, and it is already in your file. Nothing to edit. 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.
Run it locally
Run the same server with AgentTrust on:
python mcp_quickstart.py --localThe 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 ran. 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. This page does not claim that run: a stdio client called the tools directly, with no model.
Create the agent in your console
To keep every decision and manage the agent from the console, register it there:
- Open Agents, select New agent, name it
mcp-quickstart, and select Create. - Copy the API Key and the Agent ID before you select Done. The key starts with
at_live_and is shown only once. Never paste it in a chat, a ticket or a commit. - Select the agent. Under Manifest & fingerprint, select Declare. Under Declared tools, select + Add tool twice, type
addandmultiply, and select Save manifest.
That list is the same one MANIFEST holds in the program.
Do not skip the manifest. An agent with no declared tools is not restricted: every tool is allowed, so divide would run.
Run it against the console
Give the program the three values from step 5, then run it with --console:
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 --consoleThe first line names the console, and the run should match step 4: add returns 7 and divide is refused with the same Blocked by AgentTrust reason. The last line tells you where to look.
See the decisions in the console
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. Select a row to see the scores behind the decision. The Events view looks the same as in the LangGraph quickstart; this page has no captures of its own yet.
You are done. Your server's tool calls are now checked, refused when they should not run, and recorded.
What AgentTrust adds
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 the client sees it | ✗ | ✓ |
| Cap risky amounts, such as a payment 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) | ✗ | ✓ |
| Govern tools registered after the guard | ✗ | ✓ |
✗ means the MCP SDK does not provide it, so you would build it yourself. The first three rows are what you ran. The others are covered by the SDK's tests (28 per mcp version, against a real embedded gateway) and the MCP adapter guide (docs/framework-adapters/mcp.md), not run here.
Options
| Option | Default | Effect |
|---|---|---|
on_block | "refuse" | "raise" raises BlockedError instead of an error result |
review_timeout_s | 300 | how long a held call waits for a person before it is refused |
AGENTRUST_FAILURE_MODE | open | closed refuses every call when the gateway cannot be reached |
AGENTRUST_ENABLED=false | on | kill switch: tools run plainly, nothing is checked |
Best practices
- Guard at the server so every tool is covered, including ones added later
- Give each server its own
agent_id - Prefer async tools: the check for a sync tool blocks the server's event loop while the gateway answers
- Use tool trust rules for allowlists and amount limits
Troubleshooting
| Issue | Fix |
|---|---|
RuntimeError: MCP adapter requires Developer tier | Upgrade to Developer |
RuntimeError: ... could not find the tool registry | Use @mcp_tool_guard on each tool |
| A tool runs that should be refused | The agent has no declared tools, or the tool was never registered on the guarded server |
| The refusal is not the tool's return value | By design: it is an error result, because a typed return would fail its output schema on a plain string |
Next steps
Hold risky calls for a person. Set the agent's mode to Human Review. Rejected or timed-out calls do not run.
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? The same checks for a Claude tool loop: see the Claude quickstart.