PYTHON / FASTMCP / LOCAL STDIO
Gate a FastMCP tool. Prove a denied call leaves its effect untouched.
If you own a Python MCP server, you can put the permission check immediately before its actual file, database or API operation. FastMCP handles the tool schema and transport. DeadBolt admits the enclosed operation only when the configured run and policy allow it.
This walkthrough runs a separate MCP server process and checks a temporary note file after each call. It needs no model key or real account. An error response alone is not the test: the denied call must leave the file's bytes unchanged.
Authorship disclosure: an AI coding assistant wrote this guide for NextEleven and checked its commands against the source and actual local tool effects. The linked automated tests are evidence about this integration, not independent certification or customer adoption.
1. Run it in a new local lab
The source-build route below requires Git, Python 3.10+ (3.12 recommended), Rust 1.85+ and a native C compiler/linker for bundled SQLite. On Windows, use Rust's MSVC toolchain, C++ Build Tools and Windows SDK. The first build needs Internet access and can take several minutes. No system-wide DeadBolt installation is replaced.
The commands pin a reviewed source revision, install a pinned dependency set with the published Python client, and copy only the two recipe files out of the checkout. This makes the demonstration use the installed client rather than the adjacent source client.
# macOS / Linux: use a folder name that does not already exist.
mkdir deadbolt-fastmcp-lab
cd deadbolt-fastmcp-lab
git clone https://github.com/seanebones-lang/deadbolt.git source
git -C source checkout --detach 97bfe6ba5038279d671132c7289577f91ec4b56c
cargo build --manifest-path source/Cargo.toml --locked --bin deadbolt
python3 -m venv .venv
.venv/bin/python -m pip install -r source/tests/fastmcp-requirements.txt n11-deadbolt-client==1.1.0rc1
mkdir consumer
cp source/examples/fastmcp_demo.py source/examples/fastmcp_server.py consumer/
.venv/bin/python consumer/fastmcp_demo.py --binary source/target/debug/deadbolt
Windows PowerShell commands
mkdir deadbolt-fastmcp-lab
cd deadbolt-fastmcp-lab
git clone https://github.com/seanebones-lang/deadbolt.git source
git -C source checkout --detach 97bfe6ba5038279d671132c7289577f91ec4b56c
cargo build --manifest-path source/Cargo.toml --locked --bin deadbolt
py -3 -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r source/tests/fastmcp-requirements.txt n11-deadbolt-client==1.1.0rc1
mkdir consumer
Copy-Item source/examples/fastmcp_demo.py,source/examples/fastmcp_server.py consumer/
.\.venv\Scripts\python.exe consumer/fastmcp_demo.py --binary source/target/debug/deadbolt.exeConfirm py -3 --version selects Python 3.10 or newer before creating the environment. These direct interpreter commands do not require changing PowerShell's execution policy.
Already have the matching native engine? Download its ZIP and matching checksum from the 1.1.0-rc.1 release, verify it using the native installation guide, and skip the Cargo build. Replace the final --binary value with the actual executable path. The current source clone is still needed for the new recipe files.
The demo creates private temporary state, starts a loopback sidecar and a separate stdio MCP child, checks the note's bytes, then removes its temporary state and stops its processes. The lab folder and virtual environment remain for your inspection. After it finishes, you can delete this disposable lab folder.
2. Put the effect inside the callback
The complete server source selects its output path and run identity in trusted startup code. Its MCP input contains only a bounded note string. This is the relevant function, inside the server factory:
@server.tool(annotations={"readOnlyHint": False, "destructiveHint": True,
"idempotentHint": True, "openWorldHint": False})
async def demo_write_note(
content: Annotated[str, Field(min_length=1, max_length=4096)],
) -> str:
async def effect() -> str:
await asyncio.to_thread(output.write_text, content, encoding="utf-8")
return "written"
outcome = await dispatch_async(agent_id, "demo_write_note", effect)
if not outcome["executed"]:
code = outcome["decision"].get("code", "unknown")
raise ToolError(f"deadbolt:{code}; the note was not written. Contact the operator.")
return outcome["result"]
Imports and startup configuration are in the complete file. dispatch_async performs admission I/O in a worker thread and invokes the callback only after explicit allow. Every protected effect belongs inside that callback. A write before it, a background task launched before it, or a second ungated tool defeats that route's boundary. Do not cache admission decisions.
FastMCP's tool annotations describe behavior for clients; they do not enforce permission. The MCP tool name must match the name checked by the gate and the operator's policy.
3. Read the evidence, including the failures
The final JSON should include "passed": true, "count": 17, "provider_requests": 0 and "engine": "deadbolt 1.1.0-rc.1". The case names describe what actually ran or stayed untouched:
| Situation | Required observation |
|---|---|
| Allowed call | The note changes to the requested content. |
| Off-policy, invalid arguments, paused or clipped run | The previous note bytes remain unchanged. |
| Approval required | No write until the operator grants one approval; the next replay requires approval again. |
| Invalid, wrong-run, revoked or expired credential | The effect stays untouched. |
| Killed run, including a new MCP connection | Reconnecting does not revive the run or allow another write. |
| Unavailable sidecar | The server withholds the effect rather than continuing without admission. |
The 20-case acceptance script adds three startup refusals: missing sidecar address, missing admission key and an operator key present in the workload environment. The recipe CI run passed on Linux, macOS and Windows using an installed client wheel outside the checkout. The 17-case demo uses the MCP protocol; it does not require a model to choose tool calls.
If the lab does not finish successfully
- Wrong engine version: run the chosen executable with
--version. This recipe requires 1.1.0-rc.1, not stable 1.0.3 or the MCP bundle's engine. - Import or framework errors: use the lab's virtual-environment interpreter and its pinned requirements. Confirm Python 3.10+; a system Python 3.9 cannot run FastMCP 4.1.0.
- Build errors: check Rust and native compiler prerequisites, or use the checksum-verified matching native archive.
- Failed effect check: stop and inspect the failure. Do not loosen policy, pass the operator key to the tool process or turn a failure into automatic approval.
Report versions, OS and the failing case with dummy data in a GitHub issue. Keep credentials and real message content out of reports.
4. Adapt one real route deliberately
- Pick one bounded action and place its trusted validation and actual effect inside the callback. Test allowed and denied calls against the real executor.
- Assign a fresh run in trusted application code. Configure tool policy and issue a scoped admission credential through a separate operator process.
- For network routes, derive the destination from trusted execution parameters and enforce the actual network path separately. Review the current-source destination policy and migration guide; the pinned lab revision uses the earlier engine rule.
- Give the MCP workload only
DEADBOLT_SOCKandDEADBOLT_ADMISSION_TOKEN. Keep the operator key and durable state outside that process. - Display tool denials or request operator intervention. An MCP error does not stop the whole agent; avoid blind retry loops.
- For a public or multi-user server, authenticate and assign each run separately. This fixed-run local demo is not a shared-user operator service.
A live run has a sliding lease; admission renews it. An expired or killed identity needs a fresh operator-created run. The server must not call ensure to revive itself.
The demo's approval is one use for an agent/tool pair. It is not bound to exact content, arguments or recipients. If reviewed content must match the executed action, evaluate the separate exact-action protocol. Additional process/resource isolation is required for untrusted workload code.
Help make the next integration easier
A useful first developer report covers one route: time to first successful run, the confusing step, one allowed effect, one denied effect that stayed untouched, and one revoked run. Share a dummy-data reproduction with the integration report form. That feedback helps improve installation and examples without making claims about untested deployments.
Full integration recipe · Bolt on to an existing stdio server · Captioned overview and walkthrough