> ## Documentation Index
> Fetch the complete documentation index at: https://agenticadvertisingorg-fix-release-bump-classification.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Building a brand agent

> Build an AdCP brand agent as an MCP server. Serve brand identity via get_brand_identity, license talent rights via get_rights and acquire_rights, with public and authorized data tiers.

A brand agent is an MCP server that implements brand protocol tasks. DAMs, talent agencies, and brand portals build brand agents to make their data available to buyer agents over AdCP.

The agent declares `supported_protocols: ["brand"]` in [`get_adcp_capabilities`](/dist/docs/3.0.19/protocol/get_adcp_capabilities). The specific tasks it implements define its role:

| Role              | Tasks                           | Example                                      |
| ----------------- | ------------------------------- | -------------------------------------------- |
| Identity provider | `get_brand_identity`            | Acme DAM serving brand assets and guidelines |
| Rights manager    | `get_rights` + `acquire_rights` | Pinnacle Agency licensing talent             |
| Both              | All three                       | Nova Talent managing identity and rights     |

## Server setup

Every brand agent starts with an MCP server that registers AdCP tasks as tools.

```typescript theme={null}
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { z } from "zod";

const server = new McpServer({
  name: "acme-brand-agent",
  version: "1.0.0",
});
```

Register `get_adcp_capabilities` so buyer agents can discover your supported protocols:

```typescript theme={null}
server.tool("get_adcp_capabilities", {}, async () => ({
  content: [{
    type: "text",
    text: JSON.stringify({
      supported_protocols: ["brand"],
      supported_tasks: ["get_brand_identity"],
    }),
  }],
}));
```

## Transport and HTTP setup

Wire the MCP server to an HTTP endpoint so buyer agents can reach it over the network:

```typescript theme={null}
import express from "express";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";

const app = express();
app.use(express.json());

app.post("/mcp", async (req, res) => {
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: undefined,
  });
  res.on("close", () => transport.close());
  await server.connect(transport);
  await transport.handleRequest(req, res, req.body);
});

app.listen(3000, () => console.log("Brand agent listening on port 3000"));
```

This gives you a stateless HTTP endpoint at `/mcp`. For production, add authentication middleware and CORS headers.

## Tier 1: identity only

Implement `get_brand_identity` to serve brand data from your DAM or brand portal.

```typescript theme={null}
const FIELDS_ENUM = [
  "description", "industries", "keller_type", "logos", "colors",
  "fonts", "visual_guidelines", "tone", "tagline",
  "voice_synthesis", "assets", "rights",
] as const;

server.tool(
  "get_brand_identity",
  "Returns brand identity data. Core fields are always public.",
  {
    brand_id: z.string().describe("Brand identifier"),
    fields: z.array(z.enum(FIELDS_ENUM)).optional()
      .describe("Sections to include. Omit for all authorized sections."),
    use_case: z.string().optional()
      .describe("Intended use case — agent tailors content accordingly"),
  },
  async ({ brand_id, fields, use_case }, extra) => {
    const brand = await loadBrand(brand_id);
    if (!brand) {
      return {
        content: [{ type: "text", text: JSON.stringify({
          errors: [{ code: "brand_not_found", message: `No brand with id '${brand_id}'` }],
        }) }],
        isError: true,
      };
    }

    const isAuthorized = await checkLinkedAccount(extra);
    const response = buildIdentityResponse(brand, { fields, use_case, isAuthorized });

    return { content: [{ type: "text", text: JSON.stringify(response) }] };
  }
);
```

## Public vs authorized data

Every `get_brand_identity` response includes the public baseline: `brand_id`, `house`, `names`, `description`, `industries`, `keller_type`, basic `logos`, and `tagline`. No authentication required.

Authorized callers — linked via [`sync_accounts`](/dist/docs/3.0.19/accounts/tasks/sync_accounts) — get deeper data on top of that baseline: high-res assets, voice synthesis configs, tone guidelines, and rights availability.

```typescript theme={null}
function buildIdentityResponse(brand, { fields, use_case, isAuthorized }) {
  // Core fields are always returned
  const response = {
    brand_id: brand.id,
    house: brand.house,
    names: brand.names,
  };

  // Determine which sections to include
  const publicFields = ["description", "industries", "keller_type", "logos", "tagline"];
  const authorizedFields = ["colors", "fonts", "visual_guidelines", "tone",
                            "voice_synthesis", "assets", "rights"];

  const requested = fields ?? [...publicFields, ...authorizedFields];
  const withheld = [];

  for (const field of requested) {
    if (publicFields.includes(field)) {
      response[field] = brand[field];
    } else if (isAuthorized) {
      response[field] = brand[field];
    } else {
      withheld.push(field);
    }
  }

  // Signal what's behind auth
  if (withheld.length > 0) {
    response.available_fields = withheld;
  }

  return response;
}
```

When a public caller requests `fields: ["logos", "tone"]`, they get logos but not tone. The response includes `available_fields: ["tone"]` so the caller knows what linking their account would unlock.

## Tier 2: rights only

Add `get_rights` and `acquire_rights` for rights discovery and licensing. This is the path for talent agencies and music sync platforms.

```typescript theme={null}
server.tool(
  "get_rights",
  "Search for licensable rights with pricing",
  {
    query: z.string().describe("Natural language description of desired rights"),
    uses: z.array(z.string()).describe("Rights uses: likeness, voice, name, endorsement"),
    buyer_brand: z.object({
      domain: z.string(),
      brand_id: z.string().optional(),
    }).optional(),
    brand_id: z.string().optional(),
    include_excluded: z.boolean().optional(),
  },
  async ({ query, uses, buyer_brand, brand_id, include_excluded }) => {
    const matches = await searchRights({ query, uses, brand_id });

    // Filter by buyer compatibility when buyer_brand is provided
    const { rights, excluded } = buyer_brand
      ? await filterByBuyerCompatibility(matches, buyer_brand)
      : { rights: matches, excluded: [] };

    const response = { rights };
    if (include_excluded) response.excluded = excluded;

    return { content: [{ type: "text", text: JSON.stringify(response) }] };
  }
);
```

`acquire_rights` follows the same pattern — accept a `rights_id` and `pricing_option_id` from `get_rights`, clear against existing contracts, and return terms with generation credentials. The response includes an authenticated `approval_webhook` (using [`push-notification-config`](https://adcontextprotocol.org/schemas/3.0.19/core/push-notification-config.json)) so buyers can submit creatives for review. See the [acquire\_rights task reference](/dist/docs/3.0.19/brand-protocol/tasks/acquire_rights) for the full schema.

## Confidential brand rules

Brands often have rules they cannot disclose — public figure policies, internal exclusion lists, legal restrictions. Your agent evaluates these internally and returns a sanitized reason without revealing the rule itself.

The protocol supports this through a simple convention: if the rejection includes `suggestions`, the buyer can fix the problem. If it doesn't, the rejection is final and the buyer should move on.

```typescript theme={null}
async function evaluateAcquisition(request, talent) {
  // Confidential rules — buyer never sees these
  const confidentialResult = await evaluateConfidentialRules(request, talent);
  if (confidentialResult.blocked) {
    return {
      status: "rejected",
      reason: confidentialResult.sanitized_reason,
      // No suggestions — this is final, nothing the buyer can change
    };
  }

  // Actionable rejection — buyer can adjust their request
  const exclusivityConflict = await checkExclusivity(request, talent);
  if (exclusivityConflict) {
    return {
      status: "rejected",
      reason: `Exclusive conflict in ${exclusivityConflict.country} through ${exclusivityConflict.end_date}`,
      suggestions: [
        `Available in ${exclusivityConflict.alternative_countries.join(", ")}`,
        `Available after ${exclusivityConflict.end_date}`,
      ],
    };
  }

  // Approved — proceed with terms
  return { status: "acquired", /* ... */ };
}
```

The same pattern applies to `get_rights` exclusions: include `suggestions` on excluded results when the buyer can adjust their query (different market, different dates), omit them when the exclusion is non-negotiable.

### Defending against probing

A determined buyer agent could call `get_rights` with slight variations — different brands, industries, countries — to map out your confidential rules through the pattern of rejections. Mitigate this by:

* **Using consistent generic language** across similar confidential rejections. If three different rules all produce "This conflicts with our talent lifestyle guidelines," the buyer learns nothing from repeated attempts.
* **Returning the same reason regardless of which specific rule triggered it.** Don't vary the wording based on the rule — that creates a side channel.
* **Rate limiting discovery calls** per buyer. Track query volume per `buyer_brand` and return progressively less specific reasons after a threshold.

The `exclusivity_status.existing_exclusives` field in `get_rights` responses deserves special care. Populating it with specific deal terms ("exclusive with Acme Sports in NL through Q3") reveals competitive intelligence. Use vague descriptions ("exclusive commitment in this category") or omit the field entirely when confidentiality is a concern.

## Field selection and use case

The `fields` parameter lets callers request only the sections they need. Implement this efficiently — avoid loading expensive data (asset catalogs, voice configs) when not requested:

```typescript theme={null}
async function loadBrandData(brand_id, fields) {
  const brand = await db.getBrandCore(brand_id);
  if (!fields || fields.includes("assets")) {
    brand.assets = await db.getBrandAssets(brand_id);
  }
  if (!fields || fields.includes("voice_synthesis")) {
    brand.voice_synthesis = await voiceProvider.getConfig(brand_id);
  }
  return brand;
}
```

The `use_case` parameter is advisory — it tailors content within returned sections but does not override `fields`. A `"likeness"` use case prioritizes action photos in the `logos` section; a `"creative_production"` use case prioritizes vector logos and brand marks.

## Multi-tenancy

A single MCP endpoint can serve multiple brands. The `brand_id` parameter in every request disambiguates which brand the caller is asking about.

```typescript theme={null}
// One agent, many brands
const brands = {
  "emma_torres": { house: { domain: "pinnacleagency.com", name: "Pinnacle Agency" }, ... },
  "kai_nakamura": { house: { domain: "pinnacleagency.com", name: "Pinnacle Agency" }, ... },
};

async function loadBrand(brand_id) {
  return brands[brand_id] ?? null;
}
```

Each brand in your roster should also appear in your `brand.json` file's `brands` array so buyer agents can discover them before making MCP calls.

## Account linking

Buyers establish authorization by calling [`sync_accounts`](/dist/docs/3.0.19/accounts/tasks/sync_accounts) on your agent. After linking, their subsequent `get_brand_identity` requests are recognized as authorized.

Implement the [accounts protocol](/dist/docs/3.0.19/accounts/overview) to support this. The linked account is identified by the caller's credentials in the MCP transport — you do not need to pass account IDs in brand protocol requests.

### Extracting caller identity

```typescript theme={null}
async function checkLinkedAccount(extra: any): Promise<boolean> {
  // The caller's identity comes from your auth middleware.
  // After sync_accounts links a buyer, store their credentials
  // and check them on subsequent requests.
  const sessionId = extra?.sessionId;
  if (!sessionId) return false;
  return await db.isLinkedAccount(sessionId);
}
```

How you identify callers depends on your authentication setup. The MCP transport provides session information; your auth middleware maps that to a linked account. See the [authentication guide](/dist/docs/3.0.19/building/by-layer/L2/authentication) for patterns.

## Rights and creative integration

After a buyer acquires rights through `acquire_rights`, they receive `generation_credentials` and a `rights_constraint`. These connect the rights grant to creative production.

### From the brand agent's perspective

When implementing `acquire_rights`, return both pieces in the response:

```typescript theme={null}
// In your acquire_rights handler, after approval:
const response = {
  status: "acquired",
  rights_id: "rgt_dj_001",
  terms: { /* ... pricing, dates, restrictions */ },
  generation_credentials: [
    {
      provider: "midjourney",
      rights_key: "rk_dj_likeness_2026_abc",
      uses: ["likeness"],
      expires_at: "2026-06-15T00:00:00Z",
    },
  ],
  rights_constraint: {
    rights_id: "rgt_dj_001",
    rights_agent: { url: "https://rights.lotientertainment.com/mcp", id: "loti_entertainment" },
    valid_from: "2026-03-15T00:00:00Z",
    valid_until: "2026-06-15T23:59:59Z",
    uses: ["likeness"],
    countries: ["NL"],
    impression_cap: 100000,
    approval_status: "approved",
  },
};
```

### How buyers use these

The buyer's orchestrator passes `generation_credentials` to their creative agent, which uses them with the AI provider. The `rights_constraint` is embedded in the creative manifest's `rights` array — it travels with the creative through the supply chain so every system in the chain knows the usage terms.

```typescript theme={null}
// Buyer-side: passing rights to a creative agent
const creative = await creativeAgent.callTool({
  name: "build_creative",
  arguments: {
    brand: { domain: "bistro-oranje.nl" },
    format_id: { agent_url: "https://ads.example.com", id: "video_social_1080x1920" },
    brief: "15-second vertical video featuring Daan Janssen endorsing Bistro Oranje",
    generation_credentials: acquireResponse.generation_credentials,
    rights: [acquireResponse.rights_constraint],
  },
});
```

The creative agent uses the `generation_credentials` to authenticate with the AI provider (Midjourney, ElevenLabs, etc.) and produces the asset. The `rights` array becomes part of the creative manifest metadata — downstream systems (ad servers, verification vendors) can inspect it to confirm the creative is properly licensed.

For the full creative manifest specification, see [creative manifests](/dist/docs/3.0.19/creative/creative-manifests).

## Testing

Use the `validate_brand_agent` MCP tool to verify your agent is reachable and responding correctly. For automated testing during development, use the MCP SDK's in-memory transport:

```typescript theme={null}
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";

const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);

const result = await client.callTool({
  name: "get_brand_identity",
  arguments: { brand_id: "emma_torres" },
});
```

Key things to verify: core fields returned for public callers, deeper data for authorized callers, `available_fields` lists withheld sections, and `brand_not_found` errors for invalid IDs.

## Deployment checklist

* [ ] `brand.json` hosted at `/.well-known/brand.json` with `brand_agent.url` pointing to your MCP endpoint
* [ ] `get_adcp_capabilities` returns `supported_protocols: ["brand"]`
* [ ] `get_brand_identity` returns core fields for public callers
* [ ] `get_brand_identity` returns deeper data for authorized callers
* [ ] `available_fields` correctly lists withheld sections
* [ ] Error responses use the `errors` array format
* [ ] If implementing rights: `get_rights` returns pricing options and `acquire_rights` returns terms

## Related

* [Brand protocol overview](/dist/docs/3.0.19/brand-protocol/index) — How brand discovery works
* [brand.json spec](/dist/docs/3.0.19/brand-protocol/brand-json) — File format for brand declaration
* [get\_brand\_identity](/dist/docs/3.0.19/brand-protocol/tasks/get_brand_identity) — Identity task reference
* [get\_rights](/dist/docs/3.0.19/brand-protocol/tasks/get_rights) — Rights discovery task reference
* [acquire\_rights](/dist/docs/3.0.19/brand-protocol/tasks/acquire_rights) — Rights acquisition task reference
* [Accounts overview](/dist/docs/3.0.19/accounts/overview) — How account linking works
