Skip to main content

Build MCP (Model Context Protocol) servers on django-bolt with native Streamable HTTP transport

Project description

bolt-mcp

Build MCP (Model Context Protocol) servers on top of django-bolt, served natively over the MCP Streamable HTTP transport by django-bolt's Rust pipeline — no Starlette/mcp-SDK stack.

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 implicitlyapi.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 / v1 scope

Implemented: initialize/ping, tools/{list,call}, resources/{list,read,templates/list}, prompts/{list,get}, Streamable HTTP (POST/GET/DELETE), sessions, both auth tiers, auto-expose, and streaming tools (progress/logging/sampling/elicitation) via a tool Context.

Not yet (v2): completion/complete, logging/setLevel, resumability (Last-Event-ID), and Host/Origin DNS-rebinding protection.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

bolt_mcp-0.1.0.tar.gz (57.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

bolt_mcp-0.1.0-py3-none-any.whl (43.8 kB view details)

Uploaded Python 3

File details

Details for the file bolt_mcp-0.1.0.tar.gz.

File metadata

  • Download URL: bolt_mcp-0.1.0.tar.gz
  • Upload date:
  • Size: 57.6 kB
  • Tags: Source
  • Uploaded using 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}

File hashes

Hashes for bolt_mcp-0.1.0.tar.gz
Algorithm Hash digest
SHA256 3439e06d4f90e797cf48ebc258c594cdaf701892eb3d0ce0f3a83a323deef1f7
MD5 58140988f4d581e78078673126f079f2
BLAKE2b-256 0f24ec23296f92787b1c0a57ed0a9d3316afcfd050555c466697360d8b3a6523

See more details on using hashes here.

File details

Details for the file bolt_mcp-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: bolt_mcp-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 43.8 kB
  • Tags: Python 3
  • Uploaded using 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}

File hashes

Hashes for bolt_mcp-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3212b2fd2d4a588ce6753503a0c0e610df8c9da9a2cffc3acaa9715cfda48ca0
MD5 c49377edc0f11ce906e93e6d64b3726c
BLAKE2b-256 eb4274d7da09190e8096598be64d6c2f1ddd72795c8ea1db20d45b19b00f6b81

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page