Skip to main content

bolt-mcp

Build MCP (Model Context Protocol) servers on top of django-bolt, served over the MCP Streamable HTTP transport by the official MCP Rust SDK (rmcp) embedded in django-bolt's Rust core — no Starlette or Python-SDK stack in the request path. Dual-era: speaks the 2026-07-28 revision (stateless, MRTR elicitation, server/discover) and the earlier session-based revisions on the same endpoint.

from django_bolt import BoltAPI
from bolt_mcp import MCP

api = BoltAPI()
mcp = MCP("my-server", "1.0.0")


@mcp.tool
async def greet(name: str) -> dict:
    """Greet someone by name."""
    return {"greeting": f"Hello, {name}!"}


@mcp.resource("config://app", mime_type="application/json")
async def app_config() -> str:
    return '{"env": "prod"}'


@mcp.prompt
async def summarize(topic: str) -> str:
    return f"Please summarize: {topic}"


api.mount_mcp(mcp)  # MCP endpoint mounted at /mcp

Point an MCP client (Claude Desktop, MCP Inspector) at http://<host>/mcp.

Transport

mount_mcp registers POST/GET/DELETE on /mcp:

  • POST — JSON-RPC requests. By default every request response is streamed as a finite text/event-stream message (MCP-SDK-faithful). Use MCP(json_response=True) to return a single application/json object instead — the multi-process-friendly mode.
  • GET — opens the long-lived SSE listen channel for server→client messages (one per session).
  • DELETE — terminates the session.

Sessions are tracked in-process via Mcp-Session-Id. Stateful mode requires a single worker (runbolt --processes 1) or sticky sessions; for multiple workers use MCP(stateless=True) (no GET channel, each POST self-contained).

Streaming tools: progress, logging, sampling, elicitation

A tool that takes a Context can stream while it runs: call ctx.report_progress/ctx.info as work advances (those become live notifications on the POST SSE stream), then return the final result.

from bolt_mcp import Context

@mcp.tool
async def crunch(n: int, ctx: Context) -> dict:
    for i in range(n):
        await ctx.report_progress(i + 1, n)   # → notifications/progress (if client sent a progressToken)
        await ctx.info("working")             # → notifications/message
    return {"done": n}

ctx is injected by type annotation (excluded from the tool's input schema, like request). Beyond report_progress/debug/info/warning/error and read_resource (one-way / local), the Context can call back into the client and await a reply:

@mcp.tool
async def assist(text: str, ctx: Context) -> dict:
    summary = await ctx.sample(text)                 # ask the client's LLM (sampling/createMessage)
    ok = await ctx.elicit("Save this summary?")      # ask the user (elicitation/create)
    return {"summary": summary["content"]["text"], "saved": ok["action"] == "accept"}

sample/elicit are bidirectional: the server sends a request on the POST SSE stream and the client replies on a separate POST (correlated by id). They therefore require stateful streaming (MCP(stateless=False, json_response=False), single worker) and a client that advertises those capabilities — otherwise they raise (surfaced as an in-band tool error). report_progress/logging work in stateless mode too.

Expose existing endpoints as tools

Existing REST routes are never exposed implicitly — api.mount_mcp(mcp) serves only native @mcp.tool/@mcp.resource/@mcp.prompt components. To expose REST routes, list their handlers explicitly:

@api.get("/items/{item_id}")
async def get_item(item_id: int) -> dict:
    """Fetch an item by id."""
    return {"id": item_id}


api.mount_mcp(mcp, expose=[get_item])  # tool name "get_item", description from the docstring

The tool's name comes from the function name and its description from the route's description/docstring — no extra decorator needed. Use @expose_as_tool(name=..., description=...) only to override those. A handler that isn't a route on api, that takes file/form parameters, or whose name collides with another tool raises ValueError rather than being silently dropped or shadowed.

Exposure is per-handler by design: there is no "expose everything" switch, because a marker scattered across the codebase must never silently turn a route into an AI-callable tool. For deliberate bulk selection, call expose_routes(mcp, api, include=[...], methods=(...)) explicitly before mounting.

Authentication

Tier 1 — reuse django-bolt auth (validated in Rust before the handler):

from django_bolt import JWTAuthentication, IsAuthenticated

api.mount_mcp(mcp, auth=[JWTAuthentication(secret=...)], guards=[IsAuthenticated()])

Per-tool guards: @mcp.tool(guards=[HasPermission("x")]) — failing tools are filtered from tools/list and rejected on tools/call. Tools may declare request: Request to read request.context (the authenticated principal).

Tier 2 — OAuth 2.1 Resource Server (RFC 9728 metadata + WWW-Authenticate challenge):

from bolt_mcp import ProtectedResource

api.mount_mcp(mcp, oauth=ProtectedResource(
    resource_url="https://api.example.com/mcp",
    authorization_servers=["https://idp.example.com"],
    token_verifier=my_verifier,  # (token: str) -> claims | None
))

Development

This package is a uv-workspace member of the django-bolt repo.

uv sync                                                  # install workspace (editable)
uv run pytest python/bolt-mcp/tests -s -vv        # full suite (incl. subprocess integration)

Status / scope (0.2)

The protocol core is the official MCP Rust SDK (rmcp), embedded in django-bolt >= 0.10 — protocol revisions 2024-11-05 through 2026-07-28 are served dual-era. Implemented on top of it: tools/resources/prompts with msgspec schemas, MRTR elicitation with replay and signed requestState, per-request log gating, SEP-2549 ttlMs/cacheScope hints, SEP-2243 routing-header validation, Rust-evaluated per-tool guards, all three auth tiers (with RFC 9207 iss and DCR application_type), auto-expose, localhost-only Host validation by default, and configurable Host/Origin DNS-rebinding protection (mount_mcp(allowed_hosts=..., allowed_origins=...)).

Not yet: completion/complete, the tasks extension (io.modelcontextprotocol/tasks), list-changed notifications (catalogs are static), and CIMD client registration in the built-in Authorization Server.

Release files for bolt-mcp 0.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for bolt-mcp 0.2.1
File Size Uploaded
bolt_mcp-0.2.1.tar.gz 64.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for bolt-mcp 0.2.1
File Interpreter ABI Platform
bolt_mcp-0.2.1-py3-none-any.whl Python 3 none any Details

Total release size: 109.0 kB

Release files / bolt_mcp-0.2.1.tar.gz

Download URL bolt_mcp-0.2.1.tar.gz
Size 64.1 kB
Tags Source
SHA-256 checksum
How to use checksums
d3ee917562b17d35cbc1aef108dc97d310e50e78a05f21748f5a2c5016edcfce
BLAKE2b-256 checksum
How to use checksums
4473987e7e3bb7ccceaf963700011d7d17838481dcb20c70402adbdda988fa88
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.10.3 {"installer":{"name":"uv","version":"0.10.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / bolt_mcp-0.2.1-py3-none-any.whl

Download URL bolt_mcp-0.2.1-py3-none-any.whl
Size 45.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f3f65bde11aad446e72b18a9ae35aee2f8c3bfdd72ca57f09e976f5b7a20948c
BLAKE2b-256 checksum
How to use checksums
252b65416683cb25f5aac27821ff198055aeb936f10fdc74d6748d88fcb6445a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.10.3 {"installer":{"name":"uv","version":"0.10.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

0.2.3

2 release files

0.2.2

2 release files

This release

0.2.1 This release

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page