Skip to content

Quickstart

End-to-end in five steps. No real Blender required — a stub peer stands in for the addon so you can see the round-trip before installing anything in Blender.

  • Python 3.10+
  • uv installed
  • A clone of the repo
  1. Start the server. Pick one:

    Terminal window
    cd blender-mcp
    uv sync
    export ADMIN_PASSWORD=adminpw # required; server refuses to start without it
    export DEMO_PASSWORD=demopw # optional; creates a `demo` user
    uv run blender-mcp

    Expected output:

    INFO oauth_server /auth/login mounted
    INFO oauth_server /mcp mounted (Streamable HTTP)
    INFO uvicorn Uvicorn running on http://0.0.0.0:8000

    For the next steps, set MCP=http://localhost:8000.

  2. Get a JWT.

    Terminal window
    curl -s -X POST "$MCP/auth/login" \
    -H 'content-type: application/json' \
    -d '{"username":"demo","password":"demopw"}' | jq -r .access_token

    Save the token as TOKEN=... for the next two terminals. (Replace demopw with whatever you set DEMO_PASSWORD to.)

  3. Run a fake Blender peer in terminal 2. This registers as a persistent blender client and responds to both command_dispatch and job_dispatch payloads.

    Terminal window
    uv run python examples/fake_blender_peer.py \
    --server http://localhost:8000/mcp \
    --token "$TOKEN" \
    --uuid blender-demo01

    Expected output:

    [peer] Connected as blender-demo01
    [peer] set_logging_level=debug
    [peer] register_client ok
    [peer] waiting for jobs...
  4. Call a dispatch tool in terminal 3 — the simplest happy path. No job_id, no subscription, no notification filtering. The server hides the bus round-trip.

    Terminal window
    curl -s -X POST http://localhost:8000/mcp \
    -H "authorization: Bearer $TOKEN" \
    -H 'content-type: application/json' \
    -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
    "params":{"name":"blender_get_scene_info","arguments":{}}}'

    Expected output (single MCP tool result, JSON-encoded):

    {"status":"completed","command":"get_scene_info",
    "target_uuid":"blender-demo01","job_id":"j-7a3f2c1e9b04",
    "result":"{\"name\":\"Scene\",\"object_count\":3,...}","error":""}

    The server auto-picked blender-demo01 because it’s the only Blender peer on the user’s bus. Pass target_uuid in arguments to override.

  5. Inspect what’s connected. In a fourth terminal:

    Terminal window
    curl -s -X POST http://localhost:8000/mcp \
    -H "authorization: Bearer $TOKEN" \
    -H 'content-type: application/json' \
    -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
    "params":{"name":"blender_list_available_clients","arguments":{}}}'

    Returns the persistent + ephemeral client lists for the demo user’s bus.

Terminal 2 registered as a persistent client (is_persistent=true) of type blender. Terminal 3 called blender_get_scene_info — a flat MCP tool. The server’s dispatch component:

  1. Auto-picked the lone blender client (blender-demo01) as the target.
  2. Registered a Future via JobWaiter keyed on (demo, j-...).
  3. Routed a command_dispatch payload through the bus.
  4. The forwarding handler pushed an MCP notification to terminal 2’s session.
  5. Terminal 2’s drainer dispatched get_scene_info to its executor (the stub returns a canned scene snapshot), then called blender_job_update.
  6. The server’s job_update handler called job_waiter.deliver(...). The Future resolved.
  7. The dispatch tool’s await asyncio.wait_for(future) returned, the JSON wire body went back to terminal 3.

The same flow works against a real Blender addon — the stub just shortcuts the actual bpy call.

For multi-step Python or anything outside the dispatch tool surface, send a job_dispatch payload via blender_send_message. This is the original path; it still works.

Terminal window
uv run python examples/llm_client_example.py \
--server http://localhost:8000/mcp \
--token "$TOKEN" \
--target blender-demo01 \
--script 'import bpy; print(len(bpy.data.objects))'

Expected output:

[llm] send_message -> job_id=job-9c2f...
[llm] received job_update status=completed
[llm] result: 3

The example client maintains its own _message_bus subscription, a JobWaiter, and a notification filter. See Write your own LLM client for both styles in detail.