DEADBOLT

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.

Evaluation versions: FastMCP 4.1.0, Python client 1.1.0rc1 and engine 1.1.0-rc.1. The DeadBolt versions are prereleases. The recipe lives in current source and was added after those releases; it is not in the original release archives or Python wheel. The stable 1.0.3 engine in MCP bundle 1.1.0 cannot run this recipe.

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.exe

Confirm 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:

SituationRequired observation
Allowed callThe note changes to the requested content.
Off-policy, invalid arguments, paused or clipped runThe previous note bytes remain unchanged.
Approval requiredNo write until the operator grants one approval; the next replay requires approval again.
Invalid, wrong-run, revoked or expired credentialThe effect stays untouched.
Killed run, including a new MCP connectionReconnecting does not revive the run or allow another write.
Unavailable sidecarThe 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

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

  1. Pick one bounded action and place its trusted validation and actual effect inside the callback. Test allowed and denied calls against the real executor.
  2. Assign a fresh run in trusted application code. Configure tool policy and issue a scoped admission credential through a separate operator process.
  3. 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.
  4. Give the MCP workload only DEADBOLT_SOCK and DEADBOLT_ADMISSION_TOKEN. Keep the operator key and durable state outside that process.
  5. Display tool denials or request operator intervention. An MCP error does not stop the whole agent; avoid blind retry loops.
  6. 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.

Know the boundary. This recipe gates only the enclosed callback. Resources, prompts, startup, middleware, other tools, background tasks and arbitrary OS/network access remain outside it. Same-user processes are not an OS sandbox. Admission does not cancel an already-running operation or make permission and execution atomic. Cancelling an async call cannot stop a file write already running in a worker thread.

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