> ## Documentation Index
> Fetch the complete documentation index at: https://adcp-docs-ja.pier1.co.jp/llms.txt
> Use this file to discover all available pages before exploring further.

# ブランドエージェントの構築

> AdCP ブランドエージェントを MCP サーバーとして構築します。get_brand_identity でブランドアイデンティティを提供し、get_rights と acquire_rights でタレント権利をライセンスします。パブリックデータと認可データのティアを設ける。

ブランドエージェントはブランドプロトコルのタスクを実装する MCP サーバーです。DAM、タレント事務所、ブランドポータルはブランドエージェントを構築して、データを AdCP 経由でバイヤーエージェントに提供します。

エージェントは [`get_adcp_capabilities`](/docs/protocol/get_adcp_capabilities) で `supported_protocols: ["brand"]` を宣言します。実装する具体的なタスクがその役割を定義します:

| 役割             | タスク                             | 例                             |
| -------------- | ------------------------------- | ----------------------------- |
| アイデンティティプロバイダー | `get_brand_identity`            | ブランドアセットとガイドラインを提供する Acme DAM |
| 権利マネージャー       | `get_rights` + `acquire_rights` | タレントをライセンスする Pinnacle Agency  |
| 両方             | 3つすべて                           | アイデンティティと権利を管理する Nova Talent  |

## サーバーセットアップ

すべてのブランドエージェントは、AdCP タスクをツールとして登録する MCP サーバーから始まる。

```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",
});
```

バイヤーエージェントがサポートするプロトコルを発見できるよう `get_adcp_capabilities` を登録する:

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

## トランスポートと HTTP セットアップ

MCP サーバーを HTTP エンドポイントに接続し、バイヤーエージェントがネットワーク経由で到達できるようにする:

```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"));
```

これにより `/mcp` にステートレスな HTTP エンドポイントができます。本番環境では認証ミドルウェアと CORS ヘッダーを追加します。

## ティア1: アイデンティティのみ

`get_brand_identity` を実装して、DAM またはブランドポータルからブランドデータを提供します。

```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) }] };
  }
);
```

## パブリックデータと認可データ

すべての `get_brand_identity` レスポンスにはパブリックの基本情報が含まれます: `brand_id`、`house`、`names`、`description`、`industries`、`keller_type`、基本的な `logos`、`tagline`。認証は不要。

[`sync_accounts`](/docs/accounts/tasks/sync_accounts) でリンクされた認可済みの呼び出し元は、その基本情報に加えてより深いデータを取得できる: 高解像度アセット、音声合成設定、トーンガイドライン、権利の可用性。

```typescript theme={null}
function buildIdentityResponse(brand, { fields, use_case, isAuthorized }) {
  // コアフィールドは常に返される
  const response = {
    brand_id: brand.id,
    house: brand.house,
    names: brand.names,
  };

  // 含めるセクションを決定する
  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);
    }
  }

  // 認証の背後にあるものを通知する
  if (withheld.length > 0) {
    response.available_fields = withheld;
  }

  return response;
}
```

パブリックの呼び出し元が `fields: ["logos", "tone"]` をリクエストすると、logos は取得できるが tone は取得できません。レスポンスには `available_fields: ["tone"]` が含まれ、アカウントをリンクすることで何がアンロックされるかを呼び出し元が知ることができます。

## Adding verify\_brand\_claim

[`verify_brand_claim`](/docs/brand-protocol/tasks/verify_brand_claim) は、パートナーがブランドエージェントにそのアイデンティティについて権威ある yes/no の質問をできるようにします — 「この子会社はあなたのものか」「このプロパティはあなたのものか」「この商標はあなたのものか」。これはアイデンティティ層の上の階層化された機能です: 同じブランドデータに加え、静的な `brand.json` が表現できないよりリッチな状態（`pending_review`、`transferring`、`disputed`、`licensed_in`）です。

信頼モデルは方向によって非対称です。署名付きの拒否（`disputed` / `not_ours`）は一方的に権威を持ちます — ブランドは相互性なしに関連を拒否する立場を持ちます。署名付きのアサーション（`owned` / `pending_review` / `transferring` / `licensed_*`）は情報提供的ですが、単独では信頼を拡張しません。相互側が依然として確認しなければなりません。これが負荷を担う概念です — 完全な規範表については [`brand.json` § エージェント強化検証](/docs/brand-protocol/brand-json#agent-augmented-verification) を参照。

### Capability declaration

`get_adcp_capabilities` で `verify_brand_claim` をアドバタイズし、ツールごとの拡張を通じてどのクレームタイプを実装するかを宣言します。ブランドエージェントは、4 つすべてではなくスライス（例: クリエイティブクリアランス用の property のみ、またはガバナンス信頼拡張用の subsidiary+parent）を出荷してもかまいません（MAY）。

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

`supported_claim_types` が省略された場合、エージェントは 4 つすべてのサポートをアドバタイズします。コンシューマーは、特定のクレームタイプに依存する前に確認しなければなりません（MUST）。サポートされていないタイプは `UNSUPPORTED_CLAIM_TYPE` を返さなければなりません（MUST）。

### State model

エージェントは各クレームタイプに対応する内部データを必要とします。`brand.json` をこれらのストアの公開投影として扱います — エージェントは同じ事実に加え、よりリッチなライフサイクル状態を提供します。

| Store         | Backs                      | Mirrors in `brand.json`                               |
| ------------- | -------------------------- | ----------------------------------------------------- |
| 子会社ポートフォリオ    | `claim_type: "subsidiary"` | `brand_refs[]` とインライン `brands[]`                      |
| 親宣言           | `claim_type: "parent"`     | リーフの正準ドキュメント上の `house_domain`                         |
| プロパティレジストリ    | `claim_type: "property"`   | `properties[]`                                        |
| 商標レジストリ       | `claim_type: "trademark"`  | `trademarks[]` に加え内部のライセンシー側記録（今日 `brand.json` に現れない） |
| ペンディングクレームキュー | `pending_review` ライフサイクル   | 表現されない                                                |
| アーカイブ         | `archived` ステータス           | 表現されない                                                |

```typescript theme={null}
type SubsidiaryRecord = {
  subsidiary_brand_id: string;
  subsidiary_domain: string;
  status: "owned" | "pending_review" | "transferring" | "disputed" | "not_ours" | "archived";
  first_observed_by_house_at: string;
  expected_resolution_window_days?: number; // REQUIRED when status is "pending_review"
};

type PropertyRecord = {
  type: "website" | "mobile_app" | "ctv_app" | "desktop_app" | "dooh" | "podcast" | "radio" | "streaming_audio";
  identifier: string;
  brand_id: string;
  relationship: "owned" | "direct" | "delegated" | "ad_network";
  regions: string[]; // ISO 3166-1 alpha-2 or ["global"]
  status: "owned" | "transferring" | "disputed" | "not_ours" | "archived";
  use_case_authorization?: Record<string, boolean>;
};

type TrademarkRecord = {
  registry: string;
  number: string;
  mark: string;
  registration_status: "active" | "pending" | "expired" | "cancelled";
  countries: string[];
  nice_classes: number[];
  status: "owned" | "licensed_in" | "licensed_out" | "transferring" | "disputed" | "not_ours" | "archived";
  licensor_domain?: string; // when status is "licensed_in"
  use_case_authorization?: Record<string, boolean>;
};
```

### Tool registration and request validation

`verify_brand_claim` は `claim_type` で判別します。タイプごとに `claim` ペイロードを検証します — 必須フィールドは異なり、欠けているか不正な場合は `INVALID_INPUT` が正しいレスポンスです。

```typescript theme={null}
import { z } from "zod";

const SubsidiaryClaim = z.object({
  subsidiary_domain: z.string().min(1),
  subsidiary_brand_id: z.string().optional(),
  observed_at: z.string().datetime().optional(),
});

const ParentClaim = z.object({
  parent_domain: z.string().min(1),
  claimant_says: z.string().optional(),
  observed_at: z.string().datetime().optional(),
});

const PropertyClaim = z.object({
  property: z.object({
    type: z.enum(["website", "mobile_app", "ctv_app", "desktop_app", "dooh", "podcast", "radio", "streaming_audio"]),
    identifier: z.string().min(1),
    store: z.enum(["apple", "google", "amazon", "roku", "fire_tv", "samsung", "lg", "vizio", "other"]).optional(),
    region: z.string().optional(),
  }),
  use_case: z.string().optional(),
});

const TrademarkClaim = z.object({
  mark: z.string().min(1),
  registry: z.string().optional(),
  number: z.string().optional(),
  countries: z.array(z.string().length(2)).optional(),
});

server.tool(
  "verify_brand_claim",
  "Answer an authoritative yes/no about a facet of brand identity",
  {
    claim_type: z.enum(["subsidiary", "parent", "property", "trademark"]),
    claim: z.unknown(),
  },
  async ({ claim_type, claim }, extra) => {
    const isAuthorized = await checkLinkedAccount(extra);
    const callerId = await resolveCaller(extra);

    if (await rateLimited(callerId, claim_type, claim)) {
      return rateLimitedResponse(claim_type, callerId, claim);
    }

    switch (claim_type) {
      case "subsidiary": {
        const parsed = SubsidiaryClaim.safeParse(claim);
        if (!parsed.success) return invalidInput(parsed.error);
        return await answerSubsidiary(parsed.data, { isAuthorized });
      }
      case "parent": {
        const parsed = ParentClaim.safeParse(claim);
        if (!parsed.success) return invalidInput(parsed.error);
        return await answerParent(parsed.data, { isAuthorized });
      }
      case "property": {
        const parsed = PropertyClaim.safeParse(claim);
        if (!parsed.success) return invalidInput(parsed.error);
        return await answerProperty(parsed.data, { isAuthorized });
      }
      case "trademark": {
        const parsed = TrademarkClaim.safeParse(claim);
        if (!parsed.success) return invalidInput(parsed.error);
        return await answerTrademark(parsed.data, { isAuthorized });
      }
    }
  }
);
```

### Per-claim-type response shaping

`details` フィールドは `claim_type` によって異なります。内部記録から型付きレスポンスを構築し、呼び出し元がリンクされていない場合は認可専用フィールドを取り除きます。

```typescript theme={null}
async function answerSubsidiary(claim, { isAuthorized, requestContext }) {
  const record = await subsidiaries.findByDomain(claim.subsidiary_domain);

  if (!record) {
    return signedResponse({
      claim_type: "subsidiary",
      verification_status: "not_ours",
      context_note: "We have no record of this brand.",
    }, requestContext);
  }

  const details: Record<string, unknown> = {};

  // Public fields
  if (["owned", "pending_review", "transferring"].includes(record.status)) {
    details.brand_id = record.subsidiary_brand_id;
  }

  // Authorized-only fields
  if (isAuthorized) {
    details.first_observed_by_house_at = record.first_observed_by_house_at;
    if (record.expected_resolution_window_days != null) {
      details.expected_resolution_window_days = record.expected_resolution_window_days;
    }
  }

  // expected_resolution_window_days is REQUIRED when status is pending_review,
  // even for unauthorized callers — surface the bound so they can age the answer.
  if (record.status === "pending_review" && !isAuthorized) {
    details.expected_resolution_window_days = record.expected_resolution_window_days;
  }

  return signedResponse({
    claim_type: "subsidiary",
    verification_status: record.status,
    details,
  }, requestContext);
}
```

同じシェイピングパターンが `property`（パブリック呼び出し元には `details.use_case_authorization` を省略）と `trademark`（`matched_registration`、`licensor_domain`、`countries`、`nice_classes` はパブリックに保持し、`use_case_authorization` は認可の背後にゲートする）に適用されます。

### Public vs authorized field gating

`get_brand_identity` からのパブリック/認可の分割をミラーします。クレームタイプごとの分割:

| Public                                                                                                                                | Authorized-only                                                       |
| ------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `claim_type`、`verification_status`、`context_note`（常に）                                                                                 | `details.first_observed_by_house_at`                                  |
| `details.brand_id`、`details.relationship`、`details.matched_registration`、`details.countries`、`details.nice_classes`、`details.regions` | `details.expected_resolution_window_days`（`pending_review` で必須の場合を除く） |
| `details.licensor_domain`（`verification_status` が `licensed_in` の場合）                                                                  | `details.use_case_authorization`                                      |

キュー位置、チケット状態、チームルーティングは、いかなる層でも決して公開されません。

### Aging contract for pending\_review

エージェントは、`expected_resolution_window_days` が経過したら、`pending_review` 記録を終端ステータス（`owned`、`disputed`、`not_ours`、`transferring`、`archived`）または `unknown` に遷移させなければなりません（MUST）。コンシューマーは、古い `pending_review` レスポンスを `unknown` として扱い、クロールベースの検証にフォールバックすべきです（SHOULD）— しかしエージェントはいずれにせよ遷移を負っています。

cron 駆動のスイープが最もシンプルな実装です。

```typescript theme={null}
// Run hourly. Promotes timed-out pending_review records to unknown
// unless a human reviewer has acted in the meantime.
async function ageOutPendingClaims(): Promise<void> {
  const now = Date.now();
  const stale = await pendingClaims.findStale(now);

  for (const record of stale) {
    const elapsedDays = (now - Date.parse(record.first_observed_by_house_at)) / 86_400_000;
    if (elapsedDays >= record.expected_resolution_window_days) {
      await pendingClaims.update(record.id, {
        status: "unknown",
        aged_out_at: new Date(now).toISOString(),
      });
      logger.info("aged_out_pending_claim", { record_id: record.id, claim_type: record.claim_type });
    }
  }
}

setInterval(ageOutPendingClaims, 60 * 60 * 1000);
```

イベント駆動の実装も機能します — 記録作成時に `first_observed_by_house_at + expected_resolution_window_days` に遅延ジョブをスケジュールし、`unknown` に反転する前に人間のアクションが介入しなかったことをジョブで確認します。

### Signing setup

レスポンスはブランドの `adcp_use: "response-signing"` JWK の下で署名されます。**これは、エージェントが自身のアウトバウンド呼び出しに使う `request-signing` 鍵とは別個の鍵です** — 目的ごとの鍵の慣例に従い、受信者は JWK の `adcp_use` レベルで目的を強制します。目的をまたいで鍵を再利用することは仕様で禁止されています。

署名は、レスポンスボディの内部に運ばれる **JWS ペイロードエンベロープ**です（RFC 9421 §2.2.9 のトランスポートレスポンス署名ではありません — そのプリミティブは 3.x で未定義です）。`verify_brand_claim` と `verify_brand_claims` は、仕様の[指定タスクレスポンス署名リスト](/docs/building/by-layer/L1/security#designated-task-response-signing)にある唯一のタスクです — 閉じたリストのルールと入場基準はそこにあります。

エンベロープはレスポンスの `signed_response` フィールドに運ばれ、[`response-payload-jws-envelope.json`](https://adcontextprotocol.org/schemas/v3/core/response-payload-jws-envelope.json) に従います。`brand_domain` を呼び出し元の入力からではなく、サーバー側のテナント解決から投入します。共有マルチブランドフリートも、`brand_domain` ごとに別個のレスポンス署名鍵素材と別個の `kid` 値を必要とします。ブランドをまたいだ鍵の再利用はテナントバウンドのリプレイ分析を無効にし、`adcp_use: "response-signing"` について非準拠です。

AdCP 3.1 の適合性は、通常のタスクレスポンスに加えてネストされた `signed_response` を使います。JWS エンベロープをレスポンスボディ全体として返したプレリリースの例は 3.1 準拠ではありません。検証者は、プライベート移行中のみそのドラフト形状を受け入れてもかまいませんが（MAY）、それを準拠した指定タスクレスポンスとして扱ってはなりません（MUST NOT）。

エージェントの JWKS に JWK を公開し、`brand.json` の関連する `agents[]` エントリから参照します。

```json theme={null}
// /.well-known/jwks.json on the brand-agent origin
{
  "keys": [
    {
      "kty": "EC", "crv": "P-256",
      "kid": "brand-agent-response-2026-01",
      "x": "...", "y": "...",
      "use": "sig",
      "key_ops": ["verify"],
      "adcp_use": "response-signing"
    },
    {
      "kty": "EC", "crv": "P-256",
      "kid": "brand-agent-request-2026-01",
      "x": "...", "y": "...",
      "use": "sig",
      "key_ops": ["verify"],
      "adcp_use": "request-signing"
    }
  ]
}
```

```json theme={null}
// /.well-known/brand.json — agents[] entry
{
  "agents": [
    {
      "type": "brand",
      "url": "https://brand-agent.nikeinc.com/mcp",
      "id": "nikeinc_brand_agent",
      "jwks_uri": "https://brand-agent.nikeinc.com/.well-known/jwks.json"
    }
  ]
}
```

コードでは、返す前にレスポンスペイロードに署名します。署名なしの外側のフィールドは通常のタスクコンシューマーのために残ります。署名されたペイロードが正準の証明可能なオブジェクトです。

```typescript theme={null}
import { createHash } from "node:crypto";
import { jcsCanonicalize, signResponseEnvelope } from "./signing"; // your library of choice

function sha256Base64Url(input: string) {
  return createHash("sha256").update(input).digest("base64url");
}

function signedResponse(
  body: Record<string, unknown>,
  request: {
    task: "verify_brand_claim" | "verify_brand_claims";
    requestBody: unknown;
    callerIdentity: string | null;
    resolvedBrandDomain: string;
    agentUrl: string;
    maxAgeSeconds: number;
  }
) {
  const now = Math.floor(Date.now() / 1000);
  const requestBinding = {
    task: request.task,
    brand_domain: request.resolvedBrandDomain,
    agent_url: request.agentUrl,
    caller_identity: request.callerIdentity,
    request: request.requestBody,
  };
  const payload = {
    typ: "adcp-response-payload+jws",
    task: request.task,
    brand_domain: request.resolvedBrandDomain,
    agent_url: request.agentUrl,
    request_hash: `sha256:${sha256Base64Url(jcsCanonicalize(requestBinding))}`,
    iat: now,
    exp: now + request.maxAgeSeconds,
    response: body,
  };
  const signed = signResponseEnvelope(payload, {
    kid: "brand-agent-response-2026-01",
    alg: "ES256",
    typ: "adcp-response-payload+jws",
    adcp_use: "response-signing",
  });
  return { content: [{ type: "text", text: JSON.stringify({ ...body, signed_response: signed }) }] };
}
```

キーペア生成と JWKS 公開のパターンについては [request-signing](/docs/building/by-layer/L1/request-signing) を参照。レスポンス署名鍵は、異なる `adcp_use` タグを持つ同じ形状に従います。

**レスポンスボディミドルウェアの注意。** 署名されるオブジェクトは `signed_response.payload` で、署名検証前に RFC 8785/JCS で正規化されます。そのサブオブジェクトの外側の空白やキー順の変更は署名に影響しませんが、`signed_response.payload`、`signed_response.protected`、`signed_response.signature` を変更するミドルウェアは検証を壊します。外側の便宜フィールドが存在する場合、検証者は、それらが `signed_response.payload.response` と一致しないとき署名付きレスポンスを拒否しなければなりません（MUST）。

### Rate limiting

`{caller_identity, claim_type, claim-target}` ごとにレート制限します — 1 つの商標を叩き続けるバイヤーは多くのプロパティを調査するバイヤーとは異なり、それらを混同すると過剰または過小なブロックを招きます。制限時は `Retry-After` を返し、ハードな `RATE_LIMITED` エラーよりもキャッシュされた以前の回答を返すことを優先します。呼び出し元は古い `owned` に基づいて行動できます。`429` には基づいて行動できません。

```typescript theme={null}
const RATE_LIMIT_WINDOW_SEC = 60;
const RATE_LIMIT_MAX = 30;

function rateLimitKey(callerId: string, claimType: string, claim: unknown): string {
  const target = extractTarget(claimType, claim); // subsidiary_domain | property.identifier | mark+registry
  return `${callerId}::${claimType}::${target}`;
}

async function rateLimited(callerId: string, claimType: string, claim: unknown): Promise<boolean> {
  const key = rateLimitKey(callerId, claimType, claim);
  const count = await counter.increment(key, RATE_LIMIT_WINDOW_SEC);
  return count > RATE_LIMIT_MAX;
}

async function rateLimitedResponse(claimType: string, callerId: string, claim: unknown) {
  const cached = await responseCache.get(rateLimitKey(callerId, claimType, claim));
  if (cached) {
    return { content: [{ type: "text", text: JSON.stringify({ ...cached, _from_cache: true }) }] };
  }
  return {
    content: [{
      type: "text",
      text: JSON.stringify({
        errors: [{ code: "RATE_LIMITED", message: "Rate limit exceeded for this claim." }],
      }),
    }],
    _meta: { "retry-after": String(RATE_LIMIT_WINDOW_SEC) },
    isError: true,
  };
}
```

### Cache headers per status

レスポンスに `Cache-Control: max-age=N` を設定します。タスクページからの推奨値:

| Status                        | max-age       |
| ----------------------------- | ------------- |
| `owned`、`not_ours`、`disputed` | 24–72h        |
| `pending_review`              | ≤1h           |
| `transferring`                | ≤4h           |
| `licensed_in`、`licensed_out`  | 24h           |
| `unknown`                     | ≤1h           |
| `use_case_authorization` あり   | セッションごとに再チェック |

コンシューマーは下方にオーバーライドしてもかまいませんが（MAY）、エージェントが提供する `max-age` を超えるべきではありません（SHOULD NOT）。

### Notification loop for pending\_review

`verify_brand_claim` 呼び出しが未知の子会社/プロパティ/商標に着地し、エージェントのポリシーが「ポートフォリオチームに尋ねる」である場合、エージェントは `pending_review` 記録をエンキューし、チームに通知を表面化します。実装はエージェント側です。一般的なパターン:

* `brand.json` `contact.email` のアドレスで**ポートフォリオチームにメール**する。
* ブランドの既存トラッカー（Jira、Linear、Zendesk）で**チケットを開く**。
* ポートフォリオチャンネルに **Slack 通知**する。

通知はクレームペイロード、呼び出し元アイデンティティ、`expected_resolution_window_days` を運びます。レビュアーのアクション — 受け入れ、拒否、移転、アーカイブ — が記録のステータスを反転させ、同じクレームでの次の `verify_brand_claim` 呼び出しが終端の回答を返します。

```typescript theme={null}
async function enqueuePendingReview(claim: SubsidiaryClaim, callerId: string): Promise<SubsidiaryRecord> {
  const record: SubsidiaryRecord = {
    subsidiary_brand_id: claim.subsidiary_brand_id ?? "",
    subsidiary_domain: claim.subsidiary_domain,
    status: "pending_review",
    first_observed_by_house_at: new Date().toISOString(),
    expected_resolution_window_days: 14,
  };
  await subsidiaries.create(record);
  await notifications.send({
    channel: "portfolio_team",
    subject: `New subsidiary claim: ${claim.subsidiary_domain}`,
    body: { claim, caller: callerId, window_days: 14 },
  });
  return record;
}
```

### UI considerations

エージェントが `disputed` または `not_ours` を返すと、コンシューマーは拒否を自身の UI（DSP インベントリショッピング、ポートフォリオエクスプローラー、クリエイティブクリアランス）でレンダリングします。エージェントは明確な `context_note` を負っています — その文字列は人間の前に現れます。`context_note` テキストを書く際に留意すべきコンシューマー側の慣例については、[拒否されたクレームの UI ガイダンス](/docs/brand-protocol/ui-guidance) を参照。

## ティア2: 権利のみ

タレント事務所や音楽シンクプラットフォーム向けに、権利探索とライセンスのために `get_rights` と `acquire_rights` を追加します。

```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 });

    // buyer_brand が提供されている場合はバイヤーの互換性でフィルタリングする
    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` は同じパターンに従う — `get_rights` からの `rights_id` と `pricing_option_id` を受け取り、既存の契約に照らしてクリアし、生成資格情報付きの条件を返します。レスポンスには認証済みの `approval_webhook`（[`push-notification-config`](https://adcontextprotocol.org/schemas/latest/core/push-notification-config.json) を使用）が含まれ、バイヤーがレビュー用のクリエイティブを送信できます。完全なスキーマは [acquire\_rights タスクリファレンス](/docs/brand-protocol/tasks/acquire_rights) を参照。

## 機密ブランドルール

ブランドには開示できないルールがあることが多い — 公人ポリシー、内部の除外リスト、法的制限。エージェントはこれらを内部で評価し、ルール自体を明かさずにサニタイズされた理由を返します。

プロトコルはシンプルな慣例でこれをサポートする: 拒否に `suggestions` が含まれている場合、バイヤーは問題を修正できます。含まれていない場合、拒否は最終的でバイヤーは次に進むべきです。

```typescript theme={null}
async function evaluateAcquisition(request, talent) {
  // 機密ルール — バイヤーはこれらを見ない
  const confidentialResult = await evaluateConfidentialRules(request, talent);
  if (confidentialResult.blocked) {
    return {
      status: "rejected",
      reason: confidentialResult.sanitized_reason,
      // 代替案なし — これは最終的、バイヤーが変更できることはない
    };
  }

  // 実行可能な拒否 — バイヤーはリクエストを調整できる
  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}`,
      ],
    };
  }

  // 承認済み — 条件に進む
  return { status: "acquired", /* ... */ };
}
```

同じパターンが `get_rights` の除外にも適用されます: バイヤーがクエリを調整できる場合（別のマーケット、別の日程）は除外結果に `suggestions` を含め、除外が交渉不可の場合は省略します。

### プロービングへの防御

執拗なバイヤーエージェントが若干異なる変数で `get_rights` を呼び出す — 異なるブランド、業界、国 — 拒否のパターンから機密ルールをマッピングしようとするかもしれない。次の方法で軽減する:

* **類似した機密拒否全体で一貫した汎用的な表現を使用します。** 3つの異なるルールがすべて「これはタレントのライフスタイルガイドラインに抵触します」と表示されれば、バイヤーは繰り返しの試みから何も学べない。
* **どの特定のルールがトリガーされたかに関わらず同じ理由を返します。** ルールに基づいて表現を変えないこと — サイドチャンネルが生まれる。
* **バイヤーごとに探索呼び出しをレート制限します。** `buyer_brand` ごとのクエリ量を追跡し、閾値を超えたら徐々に具体性を下げた理由を返します。

`get_rights` レスポンスの `exclusivity_status.existing_exclusives` フィールドは特に注意が必要です。具体的な契約条件（「Acme Sports がオランダで Q3 まで独占権を持っている」）を入力すると競合情報を明かすことになります。曖昧な説明（「このカテゴリーで独占的なコミットメント」）を使うか、機密性が懸念される場合はフィールドを省略します。

## フィールド選択とユースケース

`fields` パラメーターにより呼び出し元は必要なセクションのみをリクエストできます。効率的に実装する — リクエストされていない場合は高コストのデータ（アセットカタログ、音声設定）の読み込みを避ける:

```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;
}
```

`use_case` パラメーターは参考情報 — 返されたセクション内のコンテンツを調整するが `fields` をオーバーライドしません。`"likeness"` ユースケースは `logos` セクションでアクションフォトを優先し、`"creative_production"` ユースケースはベクターロゴとブランドマークを優先します。

## マルチテナンシー

単一の MCP エンドポイントで複数のブランドを提供できます。各リクエストの `brand_id` パラメーターが呼び出し元がどのブランドについて尋ねているかを明確にします。

```typescript theme={null}
// 1つのエージェント、多くのブランド
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;
}
```

ロスター内の各ブランドは、バイヤーエージェントが MCP 呼び出しをする前に発見できるよう `brand.json` ファイルの `brands` 配列にも表示される必要があります。

## アカウントリンク

バイヤーはあなたのエージェントで [`sync_accounts`](/docs/accounts/tasks/sync_accounts) を呼び出すことで認可を確立します。リンク後、その後の `get_brand_identity` リクエストは認可済みと認識されます。

これをサポートするために [アカウントプロトコル](/docs/accounts/overview) を実装します。リンクされたアカウントは MCP トランスポートの呼び出し元の資格情報で識別される — ブランドプロトコルリクエストにアカウント ID を渡す必要はない。

### 呼び出し元アイデンティティの抽出

```typescript theme={null}
async function checkLinkedAccount(extra: any): Promise<boolean> {
  // 呼び出し元のアイデンティティは認証ミドルウェアから来る。
  // sync_accounts がバイヤーをリンクした後、資格情報を保存し
  // その後のリクエストで確認する。
  const sessionId = extra?.sessionId;
  if (!sessionId) return false;
  return await db.isLinkedAccount(sessionId);
}
```

呼び出し元の識別方法は認証セットアップによる。MCP トランスポートがセッション情報を提供し、認証ミドルウェアがそれをリンクされたアカウントにマッピングします。パターンについては [認証ガイド](/docs/building/by-layer/L2/authentication) を参照。

## 権利とクリエイティブの統合

バイヤーが `acquire_rights` で権利を取得すると、`generation_credentials` と `rights_constraint` を受け取ります。これらは権利付与とクリエイティブ制作をつなぐ。

### ブランドエージェント側の視点

`acquire_rights` を実装する際は、承認後にレスポンスで両方を返します:

```typescript theme={null}
// acquire_rights ハンドラーで、承認後:
const response = {
  status: "acquired",
  rights_id: "rgt_dj_001",
  terms: { /* ... 価格、日程、制限 */ },
  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",
  },
};
```

### バイヤー側での使用方法

バイヤーのオーケストレーターが `generation_credentials` をクリエイティブエージェントに渡し、クリエイティブエージェントがそれを AI プロバイダーで使用します。`rights_constraint` はクリエイティブマニフェストの `rights` 配列に埋め込まれる — クリエイティブと一緒にサプライチェーンを伝わり、チェーン内のすべてのシステムが使用条件を知ることができます。

```typescript theme={null}
// バイヤー側: クリエイティブエージェントに権利を渡す
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],
  },
});
```

クリエイティブエージェントは `generation_credentials` を使って AI プロバイダー（Midjourney、ElevenLabs など）に認証してアセットを制作します。`rights` 配列はクリエイティブマニフェストのメタデータの一部となる — ダウンストリームシステム（広告サーバー、検証ベンダー）はそれを調べてクリエイティブが適切にライセンスされていることを確認できます。

完全なクリエイティブマニフェストの仕様は [クリエイティブマニフェスト](/docs/creative/creative-manifests) を参照。

## テスト

`validate_brand_agent` MCP ツールを使ってエージェントが到達可能で正しく応答しているかを確認します。開発中の自動テストには MCP SDK のインメモリトランスポートを使用します:

```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" },
});
```

確認すべき主要事項: パブリックの呼び出し元にはコアフィールドが返されること、認可済みの呼び出し元にはより深いデータが返されること、`available_fields` が保留中のセクションをリストアップすること、無効な ID には `brand_not_found` エラーが返されること。

## デプロイチェックリスト

* [ ] `brand.json` が `/.well-known/brand.json` にホストされ、`brand_agent.url` が MCP エンドポイントを指しています
* [ ] `get_adcp_capabilities` が `supported_protocols: ["brand"]` を返す
* [ ] `get_brand_identity` がパブリックの呼び出し元にコアフィールドを返す
* [ ] `get_brand_identity` が認可済みの呼び出し元に深いデータを返す
* [ ] `available_fields` が保留中のセクションを正しくリストアップします
* [ ] エラーレスポンスが `errors` 配列フォーマットを使用します
* [ ] 権利を実装する場合: `get_rights` が価格オプションを返し、`acquire_rights` が条件を返す

## 関連

* [ブランドプロトコル概要](/docs/brand-protocol/index) — ブランド探索の仕組み
* [brand.json 仕様](/docs/brand-protocol/brand-json) — ブランド宣言のファイル形式
* [get\_brand\_identity](/docs/brand-protocol/tasks/get_brand_identity) — アイデンティティタスクリファレンス
* [get\_rights](/docs/brand-protocol/tasks/get_rights) — 権利探索タスクリファレンス
* [acquire\_rights](/docs/brand-protocol/tasks/acquire_rights) — 権利取得タスクリファレンス
* [アカウント概要](/docs/accounts/overview) — アカウントリンクの仕組み
