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
- Existing users (the app's own sign-in): add
TeamIdpAuth, mapping their token's claims withto_principal, so both kinds of caller share one API. If the existing auth isn't JWT-based (Django sessions, for example), keep it for its own routes and userequire(...)on the routes Blu calls. - Other ways in:
BluServiceAuthfor other backends that Blu grants access, andApiKeyAuthfor outside systems (hashed keys, never from browsers). Add them only if needed.
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
...
- Flask:
@blu.require({...})under the route decorator, thencurrent_principal(). - Django:
@require({...})fromblu_sdk.django, afterconfigure(ModuleAuth(...))at start-up; the caller isrequest.blu_principal. - Scope every query by
p.client_id. If a request names another client,requirealready refuses it (client_mismatch). Never trust a client id from the request. - Remember Blu users (no account needed): key your records on
p.key.
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
- The manifest is required: the endpoint refuses to start if the tools don't match it exactly.
- Flask and Django serve
blu_sdk.mcp.McpEndpointfrom their ASGI side. - Descriptions say which team the tool is from and when to use it. Start read-only.
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:
- the imports (never
blu_sdk._*or anything from Blu's platform); - that horizonmedia-blu-sdk is declared (and not installed from Git);
- that
module_idmatches the manifest; - that the MCP endpoint gets the manifest and defines every declared tool;
- that links go only to declared routes.
Then run the project's own tests.