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 })
],
});
- Existing users (the app's own sign-in): add
teamIdpAuth, mapping their token's claims withtoPrincipal, so both kinds of caller share one API. If the existing auth isn't JWT-based, keep its middleware for its own routes and useauthenticateon the routes Blu calls. - Other backends that Blu grants access:
bluServiceAuth. Outside systems:apiKeyAuth(hashed keys, never from browsers). Only if needed. - Turning entry points on per deployment:
enabledModes(process.env.AUTH_MODES).
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
- In handlers, use
req.principal:clientId(the active client, from the token),subject,displayName,email,roles,permissions, andkey(a stable id for the person). - Scope every query by
req.principal.clientId. If a request names another client,authorizealready refuses it (client_mismatch); don't add code that trusts a client id from the request. - Remember Blu users (no account needed): key your records on
principal.key.createPrincipalDirectory()shows the pattern. - Web-standard handlers (Hono, workers, Next.js route handlers):
const caller = await verifyRequest(request), which throws anAuthError(status,code); thenallows(policy, caller).
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 }));
- The manifest is required (
manifest): the endpoint refuses to start if the tools don't match the manifest exactly. - Web-standard handlers use
createMcpWebHandler({ name, version, verify: verifyRequest, tools, manifest }). - Writing descriptions: each says which team it's from and when to use the tool, for the model's benefit.
- Read-only for the Concierge. The Blu Concierge only answers questions: it never uses a write tool (the SDK doesn't offer one to a caller acting for a user). Add write tools only if the team's own callers need them, with
readOnly: falseand the matching write permission.
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.