Blu for developers

Claude Code plugin · v1.2.1

/plugin marketplace add https://developers.blu-ce.horizonmedia.com/plugins/claude-code/marketplace.json
/plugin install blu@blu

Download blu-1.2.1.zip

A Python API: horizonmedia-blu-sdk (blu_sdk)

Install it from PyPI (install.md, "Python APIs"). The full reference is the SDK's README, on its PyPI page (pypi.org/project/horizonmedia-blu-sdk) and in the installed package's metadata. This page is the integration recipe. It mirrors server.md for JS: the same rules, in Python names.

1. Who may call (per environment, from config)

import os
from blu_sdk import BluPlatformAuth, ModuleAuth, PrincipalDirectory, enabled_modes
from blu_sdk.fastapi import BluFastAPI          # or blu_sdk.flask.BluFlask / blu_sdk.django

BLU_ISSUER = os.environ["BLU_ISSUER"]          # this environment's Blu token issuer
modes = enabled_modes(os.environ.get("AUTH_MODES"))
strategies = []
if "blu" in modes:
    strategies.append(BluPlatformAuth(
        issuer=BLU_ISSUER,
        module_id="pacing",                     # = blu.module.json "id"
        role_map={                              # THE TEAM'S decision: Blu role → this API's permissions
            "viewer": ["pacing:read"],
            "analyst": ["pacing:read", "pacing:write"],
            "owner": ["pacing:read", "pacing:write"],
        },
    ))
# keep the app's own users working: TeamIdpAuth(issuer=..., audience=..., jwks=..., to_principal=...)

blu = BluFastAPI(ModuleAuth(strategies, api_url=os.environ["PUBLIC_URL"]), directory=PrincipalDirectory())
blu.install(app)                                # refusals answer {"error", "detail"} with their status

2. Per-route policies

@app.get("/api/campaigns")
async def campaigns(p: Principal = Depends(blu.require({"blu": ["pacing:read"], "team": ["pacing:read"]}))):
    return campaigns_for(p.client_id)

@app.post("/api/admin/recompute")
async def recompute(p: Principal = Depends(blu.require({"team": ["pacing:admin"]}))):   # never Blu
    ...

3. CORS

FastAPI and Starlette: app.add_middleware(CorsAllowlist, origins=[os.environ["BLU_SHELL_ORIGIN"], ...]) from blu_sdk.asgi. Flask and Django: use the framework's CORS package with the same explicit list, no wildcards, and the Authorization header allowed.

4. Concierge tools (optional)

from pydantic import BaseModel
from blu_sdk import McpTool, ToolResult, link

class Args(BaseModel):
    status: str | None = None

async def list_campaigns(p, args: Args) -> ToolResult:
    rows = campaigns_for(p.client_id, status=args.status)
    return ToolResult(summary="\n".join(f"{c.name}: {c.pacing}%" for c in rows), data=rows,
                      links=[link("campaign", {"campaignId": c.id}, c.name) for c in rows])   # manifest routes only

TOOLS = [McpTool(name="list_campaigns", title="Campaigns with pacing",           # = manifest mcp.tools[].name
                 description="Pacing team: the current client's campaigns with spend vs budget. Use for 'which campaigns are over pacing'.",
                 input=Args, policy={"blu": ["pacing:read"]}, read_only=True,       # = manifest requires / readOnly
                 run=list_campaigns)]

blu.mount_mcp(app, name="pacing", version="1.0.0", tools=TOOLS, manifest=MANIFEST)   # POST /mcp

Configuration per environment

The same as JS: BLU_ISSUER, BLU_SHELL_ORIGIN, PUBLIC_URL (the api_url, and the manifest's apiBase), AUTH_MODES. Never hard-code a Blu environment. Passwordless test tokens are refused unless api_url is a local address.

Verify

blu check reads Python too. Run it in the folder with blu.module.json. It checks:

Then run the project's own tests.