MCP server and local agents
Bunnyland mounts Streamable HTTP MCP at /v1/mcp/ on the existing API listener. It does not open a second port or use an MCP-specific credential. HTTP, WebSocket, and MCP share the same opaque bearer principals and world:play / world:admin scopes.
Prerequisites
Complete the Discord guide or skip Discord intentionally. The server must already have a private token database and a loopback API. Your MCP client must support Streamable HTTP and an Authorization header stored outside its project files.
Install the extra and enable MCP:
uv sync --locked --extra server --extra mcp
uv run bunnyland serve --config bunnyland.yml \
--mcp \
--api-host 127.0.0.1 \
--api-port 8765 \
--auth-users-file data/auth/users.yml \
--token-db data/auth/tokens.sqlite3
Local endpoint:
http://127.0.0.1:8765/v1/mcp/
Hosted same-origin endpoint:
https://play.example.com/api/v1/mcp/
Keep the trailing slash. nginx should forward the bearer header and X-Bunnyland-Client-Id; it should not add Basic auth or inject a second shared secret.
Provision the smallest useful token
For normal play:
uv run bunnyland auth provision-token \
--db data/auth/tokens.sqlite3 \
--subject my-local-agent \
--scope world:play \
--expires-days 90
Add --scope world:admin only when that specific client must generate/edit worlds, inspect global projections, administer runtime state, or use other privileged MCP tools. Prefer a separate short-lived admin token over making the everyday play token an admin.
Capture the printed secret once. Configure the transport header with the client's protected credential facility:
Authorization: Bearer blt_<public-id>_<random-secret>
X-Bunnyland-Client-Id: my-stable-mcp-client
The client-ID header is required on authenticated restricted routes even when no allowlist is configured. Keep one stable non-empty value for the session. It is not a credential; only the bearer token belongs in protected secret storage.
The bearer token never belongs in an MCP tool schema, prompt, argument, URL, repository configuration, screenshot, or log. If a client cannot protect and send an HTTP header, do not give that client a production credential.
Understand bearer and claim credentials
The bearer token grants access to Bunnyland. A character claim ID and claim secret select the specific character that the client controls. They are separate on purpose:
- connect with a play-capable bearer principal;
- list claimable characters;
- claim one character and protect the returned claim secret;
- use perceived views and normal action tools with that claim;
- release the claim when finished.
Do not persist claim secrets in a world snapshot or promote them into long-lived account tokens. Follow the play loop in the MCP player guide.
Test least privilege
With a play token, verify that character listing/claim/play works and an admin-only tool is rejected with 403. With no or an invalid bearer token, the MCP transport should return an authentication failure. A valid token whose client ID is excluded by an optional allowlist must also be rejected.
For client-specific configuration syntax, use that client's documentation. The essential values are transport streamable-http, one of the endpoint URLs above, the protected Authorization header, and a stable X-Bunnyland-Client-Id header.
Rotate and revoke
uv run bunnyland auth list-tokens --db data/auth/tokens.sqlite3
uv run bunnyland auth replace-token \
--db data/auth/tokens.sqlite3 \
--token-id TOKEN_ID
uv run bunnyland auth revoke \
--db data/auth/tokens.sqlite3 \
--token-id TOKEN_ID
Update the client with the replacement secret before discarding it. A revoked or expired credential receives 401 with WWW-Authenticate: Bearer; a valid play-only principal receives 403 for an admin operation.
The shorter MCP authentication reference documents the same transport boundary for operators integrating custom clients.
Troubleshooting
The client receives 404
Confirm --mcp is enabled, the mcp extra is installed, and the URL ends in /v1/mcp/. Hosted clients need the public /api/ prefix.
The client receives 401
Check that it is sending Authorization: Bearer ... on the HTTP transport, then inspect token expiry/revocation metadata. Never move the token into tool arguments as a workaround.
The client receives 403
The principal is authenticated but lacks a required scope, omitted the required stable X-Bunnyland-Client-Id, or fails a configured client-ID allowlist. Use world:play for character operation and reserve world:admin for privileged tools.
Local MCP works but hosted MCP fails
Verify nginx forwards Authorization, does not buffer the stream, preserves the request path/trailing slash, and has a sufficiently long read timeout. Use HTTPS for every non-local client.
A claim becomes stale
Claims can expire, be released, or change controller generation. List/claim again rather than reusing old claim secrets or queued commands.