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

The module's API: @blu/sdk/server

Full reference: the "Your API" section of node_modules/@blu/sdk/README.md. This page is the integration recipe.

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

import { bluPlatformAuth, corsAllowlist, createModuleAuth } from "@blu/sdk/server";

const BLU_ISSUER = process.env.BLU_ISSUER;            // this environment's Blu token issuer
const { authenticate, authorize, verifyRequest } = createModuleAuth({
  apiUrl: process.env.PUBLIC_URL,                     // this API's own public URL
  strategies: [
    bluPlatformAuth({
      issuer: BLU_ISSUER,
      jwksUrl: `${BLU_ISSUER}/.well-known/jwks.json`,
      moduleId: "pacing",                             // = blu.module.json "id"
      roleMap: {                                      // 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, jwksUrl, audience, toPrincipal })
  ],
});

2. Per-route policies

app.get("/api/campaigns", authenticate, authorize({ blu: ["pacing:read"], team: ["pacing:read"] }), handler);
app.post("/api/admin/recompute", authenticate, authorize({ team: ["pacing:admin"] }), handler);   // never Blu

3. CORS

app.use(corsAllowlist({ origins: [process.env.BLU_SHELL_ORIGIN /*, the app's own front-end origin */] }));

Inside Blu, the browser calls the API from the Blu shell's origin, so list it.

Blu sends credentials as headers (Bearer), never cookies.

4. Concierge tools (optional)

Wrap existing read endpoints. Each tool runs with the user's own permissions and client.

import { z } from "zod";
import { createMcpEndpoint, link } from "@blu/sdk/server";
import manifest from "../blu.module.json" with { type: "json" };

const tools = [{
  name: "list_campaigns",                       // = manifest mcp.tools[].name
  title: "Campaigns with pacing",
  description: "Pacing team: the current client's campaigns with spend vs budget. Use for 'which campaigns are over pacing'.",
  input: { status: z.enum(["over", "under"]).optional() },
  policy: { blu: ["pacing:read"] },             // = manifest requires
  annotations: { readOnlyHint: true },          // = manifest readOnly
  run: async (principal, { status }) => {
    const rows = await campaignsFor(principal.clientId, { status });
    return {
      summary: rows.map((c) => `${c.name}: ${c.pacing}%`).join("\n"),
      data: rows,
      links: rows.map((c) => link("campaign", { campaignId: c.id }, c.name)),   // routes from the manifest only
    };
  },
}];

app.post("/mcp", express.json(), ...createMcpEndpoint({ name: "pacing", version: "1.0.0", authenticate, tools, manifest }));

Configuration per environment

Env var (suggested) Value
BLU_ISSUER this environment's Blu issuer
BLU_SHELL_ORIGIN the Blu shell's origin (for CORS)
PUBLIC_URL this API's own URL (apiUrl; also the manifest's apiBase)
AUTH_MODES e.g. blu,team

Never hard-code a Blu environment. Never accept test tokens in deployed environments; createModuleAuth already refuses them unless apiUrl is a local address.