> For the complete documentation index, see [llms.txt](https://docs.maiagent.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.maiagent.ai/tech/ja/mcp/self-hosted-mcp-server.md).

# セルフホスト MCP Server の接続仕様

自社システムを MCP Server として公開し、MaiAgent の Agent から呼び出すための仕様です。対応するトランスポートと URL 要件、認証（固定ヘッダー、連絡先の認証情報、OAuth 動的登録）、許可するツール、タイムアウトと再試行、モデルへのエラー返却、MCP Apps のインタラクティブカードについて説明します。

このページは「自社システムを MCP Server として公開し、MaiAgent の Agent から呼び出したい」エンジニア向けです。MaiAgent がサーバーに接続する方法、送信するヘッダー、OAuth の処理、ツール一覧の取得とツール呼び出し時のタイムアウト、再試行、エラー処理について説明します。

* MCP の概念と仕組みについては [MCP（Model Context Protocol）](/tech/ja/mcp/mcp-model-context-protocol.md) を、Composio や Zapier などのホスト型サービスについては [Remote MCP サービスの概要](/tech/ja/mcp/remote-mcp.md) を参照してください。
* 管理画面での操作手順（MCP ツールを追加する画面や各フィールドの入力内容）は、ユーザーマニュアルの [MCP ツールの作成](https://docs.maiagent.ai/build/tools/mcp-setup) を参照してください。このページでは操作手順を繰り返し説明しません。
* MCP Server ではなく通常の REST API に接続する場合は、[API ツールエンジニアリングガイド](/tech/ja/api-integration/api-tool-engineering-guide.md) を参照してください。

## 1 回の呼び出しの流れ <a href="#call-flow" id="call-flow"></a>

```mermaid
sequenceDiagram
    participant A as MaiAgent の Agent
    participant P as MaiAgent プラットフォーム（MCP Client）
    participant S as 自社の MCP Server

    Note over P: Agent のツールを構成するたびに実行
    P->>S: initialize → tools/list（設定済みヘッダーを送信）
    S-->>P: ツール一覧（name、description、inputSchema、annotations）
    P->>P: 許可するツールで絞り込み、名前にプレフィックスを付け、モデル用のツール定義に変換
    A->>P: tool call（ツール名＋JSON 引数）
    P->>S: 新しいセッションを開始：initialize → tools/call
    S-->>P: CallToolResult
    P->>P: テキストに変換し、コンテキストウィンドウに合わせて切り詰め、ツール実行ログを記録
    P-->>A: 結果のテキスト（またはエラーメッセージ）
```

MaiAgent は **MCP Client** です。Agent は呼び出すツールと引数を決定するだけで、実際の接続はプラットフォームが行います。

## トランスポートと URL 要件 <a href="#transport-and-url" id="transport-and-url"></a>

| 項目          | 仕様                                                                                                                                                                                   |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 対応するトランスポート | **Streamable HTTP** と **SSE** に対応します。stdio には対応しません（プラットフォーム上で `npx` や `node` などのローカルコマンドを起動しません）。                                                                                   |
| トランスポートの判定  | URL から自動判定します。パスの末尾が `/sse`、パスに `/sse/` が含まれる、またはクエリ文字列に `transport=sse` が含まれる場合は SSE を使用し、それ以外はすべて Streamable HTTP を使用します。                                                          |
| URL スキーム    | `https://` が必須です。接続先がプラットフォームで許可された内部ネットワーク（デプロイ設定の内部 CIDR 許可リスト）に解決される場合のみ、`http://` を許可します。                                                                                        |
| ネットワーク上の配置  | ドメインの名前解決が必要です。プライベートアドレスやループバックなどの内部アドレスに解決される場合は拒否します（内部 CIDR 許可リストに含まれるアドレスは除きます）。リンクローカルアドレス（`169.254.x.x` など）は常に拒否し、許可リストでも許可できません。SaaS 環境では、サーバーにパブリックインターネットからアクセスできる必要があります。 |
| TLS         | デフォルトで厳格な証明書検証を行います。Subject Key Identifier のない古いルート証明書が証明書チェーンに含まれ、接続できない場合、検証を緩和するオプションは MaiAgent がシステム管理側でツールごと、またはドメインごとに有効にする必要があります。MaiAgent にお問い合わせください。                       |

{% hint style="info" %}
Streamable HTTP を使用し、MCP エンドポイントを専用のパス（例：`https://mcp.example.com/mcp`）に配置することを推奨します。サーバーが SSE のみを提供する場合は、URL の末尾を `/sse` にすると、プラットフォームが SSE で接続します。
{% endhint %}

## ツール作成時の接続検証 <a href="#setup-validation" id="setup-validation"></a>

管理画面または API で MCP ツールを作成・更新するとき、プラットフォームはまずサーバーに実際に接続して `tools/list` を 1 回実行します。

* 接続できない、またはツール一覧を取得できない場合、作成は失敗します。エラーメッセージには接続エラーの詳細が含まれます。
* リクエストで `mcpAllowedTools` を指定しない、または空配列 `[]` を渡すと、プラットフォームは **その時点** の `tools/list` が返すすべてのツール名を許可リストとして保存します。その後サーバーに追加したツールは **自動では有効になりません**。ツール設定で一覧を再取得し、選択する必要があります。
* 接続検証では、`rawMcpHeader` のうち **`{{変数}}` を含まない** ヘッダーのみを送信します（この時点では変数を代入する対話がありません）。サーバーが各リクエストに動的ヘッダーを要求する場合は、固定ヘッダーだけでも `tools/list` が応答できるようにしてください。
* URL が OAuth に設定された MCP Registry に対応する場合（[OAuth 2.0](#oauth) を参照）、作成・更新時の検証を省略し、許可リストを空のままにします。認可完了後にツール一覧を取得します。

## 認証方式 <a href="#authentication" id="authentication"></a>

プラットフォームは HTTP ヘッダーを使ってサーバーに識別情報を送信します。送信するヘッダーは、ツールの <mark style="color:blue;">認可対象</mark>（API フィールド `authSubject`）によって決まります。

| `authSubject`   | 管理画面の名称                                     | 使用するユーザーの識別情報      | 適用するヘッダー（後のヘッダーが同名の前のヘッダーを上書きします）                        |
| --------------- | ------------------------------------------- | ------------------ | -------------------------------------------------------- |
| `shared`（デフォルト） | <mark style="color:blue;">組織共通</mark>       | 組織全体で 1 組の認可を共有します | ツール単位の OAuth トークン → `rawMcpHeader` → 該当する連絡先の MCP 認証情報   |
| `contact`       | <mark style="color:blue;">連絡先ごとの個別認可</mark> | 対話中の連絡先本人です        | `rawMcpHeader` → 該当する連絡先の MCP 認証情報 → 該当する連絡先の OAuth トークン |

`contact` モードでは、匿名の連絡先（`sourceId` がない連絡先）との対話で、このツールを Agent に **一切提供しません**。

### 固定ヘッダー（API Key） <a href="#static-headers" id="static-headers"></a>

最も簡単な方法は、ツールの `rawMcpHeader` に JSON オブジェクトを設定することです。接続のたびに送信します。

```json
{
  "Authorization": "Bearer <your-api-key>",
  "X-Tenant": "{{organization_id}}"
}
```

* `rawMcpHeader` は書き込み専用フィールドです。ツールを取得すると、マスク済みの `rawMcpHeaderMasked` のみが返されます。
* 値では次の `{{変数}}` を使用でき、現在の対話に応じて代入します：`contact_id`、`source_id`、`contact_name`、`conversation_id`、`inbox_id`、`organization_id`、`agent_slug`、`agent_version`。

### 連絡先単位の MCP 認証情報 <a href="#contact-credentials" id="contact-credentials"></a>

Agent が **エンドユーザー本人の権限** でサーバーを呼び出せるようにするには、ユーザーのトークンを該当する連絡先の MCP 認証情報に書き込みます。

* 各連絡先は、MCP ツールごとに最大 1 件の認証情報を持つことができ、その内容は `headers` のセットです。
* 対話中、プラットフォームは「現在の対話の連絡先＋このツール」で認証情報を検索し、`headers` を **そのまま** リクエストヘッダーにマージします。同名の場合は `rawMcpHeader` を上書きします。認証情報のヘッダーには `{{変数}}` を **代入しません**。
* 書き込み方法：
  * Web Chat の埋め込み時に `setup-contact-credentials` を呼び出し、`mcpCredentials` に `toolId` と `headers` を含めます（`toolId` は MCP タイプのツールである必要があり、それ以外は 400 を返します）。
  * バックエンドから連絡先 API の `mcp-credentials` エンドポイントを呼び出し、追加、更新、削除します。

両方の方法の全パラメータ、トークン更新、ログアウト時の失効処理は、[連絡先の識別情報の同期とトークン更新](/tech/ja/serve/contacts/contact-credentials-sync.md) を参照してください。管理画面での手動設定は、ユーザーマニュアルの [連絡先の MCP 認証情報の設定](https://docs.maiagent.ai/build/tools/setup-contacts-mcp-credentials) を参照してください。

### 対話識別ヘッダー <a href="#conversation-header" id="conversation-header"></a>

対話コンテキストがある場合、プラットフォームは各リクエストに `X-Conversation-Id: <対話 ID>` を追加します。状態を保持するサーバーは、モデルにセッショントークンを渡させることなく、対話単位で作業状態を保存できます。認証ヘッダーが同名の場合は、認証ヘッダーを優先します。

### OAuth 2.0 <a href="#oauth" id="oauth"></a>

MCP Server が OAuth で保護されている場合、プラットフォームは MCP 認可仕様に従い、次の処理を自動で行います。

{% stepper %}
{% step %}

#### 保護対象リソースの探索 <a href="#oauth-resource-discovery" id="oauth-resource-discovery"></a>

プラットフォームはまず、トークンなしで MCP URL に `GET` を送信します。`401` と `WWW-Authenticate: Bearer resource_metadata="..."` が返された場合、その URL を読み取ります（`https` が必須で、ホストは MCP URL と同じである必要があります）。それ以外の場合は、RFC 9728 のデフォルトの場所を試します。最初に `https://{host}/.well-known/oauth-protected-resource{MCP パス}`、次に `https://{host}/.well-known/oauth-protected-resource` を試します。

そこから `resource`（トークンの audience）と `authorization_servers`（最初のエントリを使用し、`https` が必須です）を取得します。探索結果は 5 分間キャッシュします。
{% endstep %}

{% step %}

#### 認可サーバーのメタデータの取得 <a href="#oauth-server-metadata" id="oauth-server-metadata"></a>

RFC 8414 に従って `/.well-known/oauth-authorization-server` を読み取ります。認可サーバーの URL にパスが含まれる場合、「ホストとパスの間に well-known を挿入」「パスの後ろに well-known を追加」「ホストのルート」の 3 か所を順に試します。
{% endstep %}

{% step %}

#### Client の動的登録（RFC 7591） <a href="#oauth-dynamic-registration" id="oauth-dynamic-registration"></a>

メタデータに `registration_endpoint` がある場合、プラットフォームは次を送信します。

```json
{
  "client_name": "MaiAgent MCP Client",
  "redirect_uris": ["<MaiAgent callback URL>"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}
```

`registration_endpoint` がない場合、MCP Registry に `oauthClientId` を事前設定する必要があります（機密クライアントでは `oauthClientSecret` も設定します）。設定がないと認可は失敗します。
{% endstep %}

{% step %}

#### 認可コード＋PKCE <a href="#oauth-authorization-code" id="oauth-authorization-code"></a>

認可リクエストには `response_type=code`、`state`、`code_challenge`（`S256`）、`resource` を含めます。`scope` は、MCP Registry に設定された scope → 保護対象リソースのメタデータの `scopes_supported` → 認可サーバーのメタデータの `scopes_supported` の順で選択します。`state` の有効期間は 10 分です。

トークン交換では `grant_type=authorization_code` とともに `code_verifier` と `resource` を送信し、client secret がある場合のみそれも含めます。
{% endstep %}

{% step %}

#### トークンの自動更新 <a href="#oauth-refresh" id="oauth-refresh"></a>

トークンの有効期限まで 60 秒以内になると、プラットフォームは `grant_type=refresh_token` で更新します。応答に新しい `refresh_token` がない場合は、古いものを引き続き使用します。
{% endstep %}
{% endstepper %}

**Callback URL**：認可サーバーが動的登録に対応せず、redirect URI の事前登録が必要な場合は、以下の 2 つを両方登録してください。登録がないと、対応する処理が `/authorize` で失敗します。

| 処理            | Callback URL                                                                                       |
| ------------- | -------------------------------------------------------------------------------------------------- |
| メンバー（組織共通）の認可 | MaiAgent 管理画面のドメイン + `/oauth/mcp/callback`                                                         |
| 連絡先ごとの個別認可    | `https://api.maiagent.ai/api/mcp/oauth/contacts/callback/`（SaaS。プライベートデプロイでは、その環境の API ドメインを使用します） |

**MCP Registry**：自社ホスト型サーバーで固定の `oauthClientId`、認可サーバー URL、または `resourceIdentifier` を指定する必要がある場合、コネクタ管理権限を持つメンバーが組織専用の MCP Registry（`/api/v1/mcp/private-registry/`）を作成できます。`authType` は `oauth`、`service_account`（ヘッダーでキーを送信）、`none` から選択できます。プラットフォームは「MCP URL が Registry の `remoteMcpUrl` で始まるか」で、ツールに対応する Registry を判定します。空欄のフィールドのみ、自動探索の結果を使用します。

## 許可するツール（`mcpAllowedTools`） <a href="#allowed-tools" id="allowed-tools"></a>

`mcpAllowedTools` はサーバーのツール名の配列で、Agent に提供するツールを決定します。実行時、保存された値は認可対象に応じて次のように解釈します。

| 値                        | `shared`           | `contact`          |
| ------------------------ | ------------------ | ------------------ |
| `null`                   | サーバーのすべてのツールを許可します | サーバーのすべてのツールを許可します |
| `[]`（空配列）                | **ツールを一切許可しません**   | サーバーのすべてのツールを許可します |
| `["search_orders", ...]` | 指定されたツールのみ許可します    | 指定されたツールのみ許可します    |

`contact` モードで空配列をすべて許可と扱うのは、管理者が連絡先のトークンを持たず、事前にツール一覧を取得できないためです。OAuth を使用する `shared` モードでは、管理者が認可を完了してツール一覧を取得した後に、一覧に内容が入ります。

{% hint style="warning" %}
API でツールを作成・更新する場合：

* **通常の MCP ツール**：`mcpAllowedTools` を省略する場合も `[]` を渡す場合も、その時点で取得したすべてのツール名に置き換えられます（[ツール作成時の接続検証](#setup-validation) を参照）。そのため、`[]` ではすべてのツールを無効にできません。
* **OAuth MCP Registry に対応するツール**：`[]` はそのまま保存します。更新時に `[]` を渡す、またはこのフィールドを省略すると、許可リストも空になり、`shared` モードではツールを一切許可しません。このタイプのツールを更新するときは、必ず完全な `mcpAllowedTools` を含めてください。
  {% endhint %}

## ツール一覧をモデルに渡す仕組み <a href="#tool-list-to-model" id="tool-list-to-model"></a>

* **取得のタイミング**：Agent のツールを構成するたびに、サーバーへ接続して `tools/list` を実行し、許可リストで絞り込みます。
* **名前**：ツールに `name` が設定されている場合、モデルに見える名前は `{name}__{自社のツール名}` です。英数字とアンダースコア以外の文字はアンダースコアに置き換え、先頭が英字でない場合は `t_` を付けます。64 文字を超える場合は切り詰め、6 文字のハッシュサフィックスを付けます。
* **説明**：モデルに見える説明は、`tools/list` で返した `description` です。用途と使用するタイミングを明確に記述してください。
* **パラメータ**：`inputSchema` をモデルのパラメータ定義に変換します。プラットフォームはよくある 2 つの形式を自動処理します。配列形式のプロパティ値を `anyOf` に変換し、ローカルの `$ref` をインライン展開します（再帰参照は任意の値を受け入れる形式に変更します）。それでもスキーマを変換できない場合は **そのツールだけを省略** し、ほかのツールは通常どおり提供します。
* **取得失敗**：接続不能、タイムアウト、認証失敗の場合、この MCP ツールは今回の応答でモデルに **ツールを一切提供しません**。対話自体は中断しません。

## ツール呼び出し：タイムアウト、再試行、エラー <a href="#tool-call" id="tool-call"></a>

### セッションとタイムアウト <a href="#session-and-timeout" id="session-and-timeout"></a>

* `tools/call` ごとに **新しい MCP セッションを開始** します（`initialize` の後に呼び出します）。呼び出し間でセッションを再利用しません。複数の呼び出しにまたがって状態を保持する場合は、`X-Conversation-Id` または独自の認証済み識別情報を使用してください。
* MCP セッションの読み取りタイムアウトは 30 秒です。

### 一時的なエラーの自動再試行 <a href="#transient-retry" id="transient-retry"></a>

接続タイムアウト、接続の中断やリセット、サーバーが応答する前の切断、途中で閉じられたセッション、および接続終了を示す MCP エラーコードまたは 408 の場合、プラットフォームは最大 3 回再試行します。間隔は 1、2、4 秒（上限 8 秒）です。サーバーが返すエラー結果やアプリケーション層のエラーは **再試行しません**。

再送しても安全なツールのみ再試行します。判定順序は次のとおりです。

1. ツールの `annotations` に `readOnlyHint: true` または `idempotentHint: true` がある場合 → 再試行します。
2. いずれかが明示的に `false` の場合 → 再試行しません。
3. どちらも宣言されていない場合、ツール名の最初の単語が `get`、`list`、`read`、`fetch`、`search`、`find`、`check`、`query`、`describe`、`show`、`view`、`browse`、`lookup` のいずれかかを確認します（camelCase と `-` は先に単語に分割します）。

{% hint style="info" %}
`tools/list` で各ツールの `readOnlyHint`／`idempotentHint` を明示することを推奨します。副作用があるツール（注文やフォーム送信）は `false` にし、切断後の再送を防いでください。
{% endhint %}

### モデルに返す内容 <a href="#response-to-model" id="response-to-model"></a>

| 状況                             | モデルに見える内容                                                                                                                |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------ |
| 呼び出し成功                         | `CallToolResult` から変換したテキスト（`content`、`structuredContent`、`isError` フィールドを含みます）。`isError: true` を返した場合も、モデルはこの内容を受け取ります。 |
| 呼び出し中の例外（再試行後も失敗、または一時的でないエラー） | `MCP ツール実行エラー\n MCP Tool Execution Error: <エラー内容>`                                                                       |
| 結果が長すぎる場合                      | 上限はモデルのコンテキストウィンドウ × ツール出力比率（デフォルト 0.8）で、トークン数から推定します。超過時は先頭と末尾を半分ずつ残して中間を省略し、先頭に結果が不完全であることを示す通知を付けます。                  |

呼び出しごとにツール実行ログ（ツール、実際の MCP ツール名、入力引数、出力またはエラー、所要時間、再試行回数）を記録します。デバッグ方法は [API ツールエンジニアリングガイドのログとデバッグ](/tech/ja/api-integration/api-tool-engineering-guide.md#records-and-debugging) と同じです。

### 課金区分 <a href="#billing" id="billing"></a>

成功した呼び出しごとに MCP ツール使用量を 1 回として計上し、ツール名から読み取り系または書き込み系に分類します。名前の最初の単語が上記の読み取りプレフィックスに一致する場合は読み取り系、それ以外で名前に書き込みキーワードが含まれる場合は書き込み系、残りはすべて読み取り系です。組織のクレジットが不足している場合、その呼び出しはサーバーに送信しません。

## MCP Apps（インタラクティブカード） <a href="#mcp-apps" id="mcp-apps"></a>

MaiAgent は MCP Apps 拡張（SEP-1865）に対応しています。ツールはデータに加えて HTML インターフェースを添付でき、対話に操作可能なカードが表示されます。たとえばグラフ、フォーム、検索結果一覧などです。ユーザーから見た動作と制限は、ユーザーマニュアルの [MCP インタラクティブカード（MCP Apps）](https://docs.maiagent.ai/build/tools/mcp-apps) を参照してください。すぐに試す場合は、[最小の実行可能なサンプル](#mcp-apps-example) に進んでください。

### サーバー側：UI リソースの返し方 <a href="#mcp-apps-server" id="mcp-apps-server"></a>

1. **UI リソースを関連付ける**：`tools/list` のツール定義、または `tools/call` の結果の `_meta` に `ui.resourceUri` を含めます。値は `ui://` で始まる必要があります。例：`{"_meta": {"ui": {"resourceUri": "ui://orders/list"}}}`。両方にある場合は呼び出し結果を優先します。フラット形式の `_meta["ui/resourceUri"]`（`@modelcontextprotocol/ext-apps` の `registerAppTool` が生成する形式）も受け付けます。ツール定義の `_meta` は、ツール一覧を構成するたびに読み取ります。
2. **リソースの内容を提供する**：ツール呼び出しの成功後、プラットフォームは同じ MCP セッションでこの URI に `resources/read` を送信し、最初のコンテンツの `text` をカードの HTML として使用します。`mimeType` は `text/html;profile=mcp-app` を推奨します。
3. **データを返す**：カードが受け取るのは今回のツール結果、つまり `content` の最初のテキストと `structuredContent`（オブジェクト）です。カードのデータソースには `structuredContent` を使用してください。プラットフォームはツールの入力引数をカードに渡しません（`ui/notifications/tool-input` の `arguments` は空オブジェクトです）。

{% hint style="warning" %}
プラットフォームの MCP client は、`initialize` 時に `io.modelcontextprotocol/ui` 拡張機能を **宣言しません**。`getUiCapability()` などの判定で `_meta` を付けるかどうかを決めないでください。そうすると、プラットフォームが UI リソースを取得できなくなります。
{% endhint %}

次の場合、カードは表示されないか、テキスト表示に切り替わります。

| 状況                            | 結果                                 |
| ----------------------------- | ---------------------------------- |
| ツール結果が `isError: true`        | カードを生成しません                         |
| `ui://` リソースが関連付けられていない       | カードを生成せず、対話には通常どおりツール結果を表示します      |
| `resources/read` が失敗、または内容がない | カードには代わりに `content` の最初のテキストを表示します |

カードの HTML は生成時にメッセージとともに保存します。その後サーバー上の UI を更新しても、過去のメッセージですでに生成されたカードは変わりません。新しい UI は、古いバージョンが生成したデータも処理できるようにしてください。

### フロントエンド：カードの表示方法 <a href="#mcp-apps-rendering" id="mcp-apps-rendering"></a>

* **表示位置**：Agent の応答完了後、その応答の下にカードを表示します。同じ応答内では、同じソースかつ同じ UI リソースのカードは最新の 1 枚のみ表示します。
* **サンドボックス**：カードは `sandbox="allow-scripts"` の iframe で実行します（`allow-same-origin` はありません）。ホストページの Cookie やストレージを読み取れず、自身で新しいウィンドウを開くこともできません。
* **コンテンツセキュリティポリシー（CSP）**：デフォルトではインラインの script と style のみ許可します。画像、メディア、フォントは `data:`／`blob:` のみ許可し、`connect-src` は `'none'`（外部へのリクエストは禁止）です。フォームの送信もできません。JS、CSS、画像は 1 つの HTML にまとめてください。
* **追加で許可するオリジン**：外部リソースの読み込みが必要な場合、組織専用の MCP Registry（`/api/v1/mcp/private-registry/`）の `cspAllowedOrigins` にオリジンを指定します。`https://` で始まり、パスやワイルドカードを含まない origin のみ許可します。例：`https://viewer.example.com`。MCP ツールの作成・更新時に、URL が Registry の `remoteMcpUrl` で始まるツールを、その Registry に対応付けます。
* **サイズと表示モード**：カードのデフォルトの高さは 220px で、カードが `ui/notifications/size-changed` で報告する `height` に応じて調整します（上限 2000px）。メッセージストリーム内の最大高さは `min(62vh, 620px)` で、超過時はカード内でスクロールします。カードは全画面（`fullscreen`）への切り替えを要求できます。プラットフォームは右上に終了ボタンを追加し、Esc でも元に戻れます。折りたたみ後は、カードが最後に報告した高さを維持します。`pip` の要求は通常（`inline`）モードで表示します。
* **ストリーミング中は非表示**：Agent の応答ストリーミング終了後にカードをマウントします。同じ応答が複数回更新される場合、プラットフォームは `ui/notifications/tool-result` で新しい結果をその場で渡し、iframe を再作成しません。

### カードとホストのハンドシェイク <a href="#mcp-apps-handshake" id="mcp-apps-handshake"></a>

カードとプラットフォームは、MCP Apps の JSON-RPC を `window.parent.postMessage` でやり取りします（ホスト側は `@modelcontextprotocol/ext-apps` 1.2 の `AppBridge` を使用し、プロトコルバージョンは `2026-01-26` です）。順序は次のとおりです。

1. カードが `ui/initialize`（`appInfo`、`appCapabilities`、`protocolVersion`）を送信します。プラットフォームは `hostInfo`、`hostCapabilities`（`serverTools`、`message.text`、`openLinks`）、`hostContext`（`displayMode`、`availableDisplayModes: ["inline", "fullscreen"]`、`theme: { primaryColor, mode }`）を返します。
2. カードが `ui/notifications/initialized` を送信します。
3. プラットフォームは `ui/notifications/tool-input`（`arguments` は空）と `ui/notifications/tool-result`（完全な `CallToolResult`）を順に送信します。その後、表示モードやテーマが変わると `ui/notifications/host-context-changed` を送信し、カードをアンマウントする前に `ui/resource-teardown` リクエストを送信します。

プラットフォームはさらに、カード HTML の `<head>` に `window.__MCP_INITIAL_RENDER_DATA__`（同じツール結果）を注入し、ハンドシェイク完了前でも描画できるようにします。正式なデータは引き続き `tool-result` 通知を基準にしてください。

### カードから呼び出せるホスト機能 <a href="#mcp-apps-host-api" id="mcp-apps-host-api"></a>

| メソッド                                                          | プラットフォームの処理                                                                                                                                                                                                                                                                           |
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tools/call`                                                  | プラットフォームが代理で呼び出します。カードは自社サーバーや MaiAgent バックエンドへ直接接続できません。このカードを生成した同じ MCP ソースのツールで、この対話で有効かつ許可リストに含まれるツールのみ呼び出せます。認証ヘッダーは Agent の呼び出しと同じです。1 回のタイムアウトは 30 秒で、カードに返す結果は `content` の最初のテキスト、`structuredContent`、`isError` のみです。失敗時はカードの外側に「この操作を完了できませんでした。しばらくしてからもう一度お試しください。」と表示します。 |
| `ui/message`                                                  | `content` はコンテンツブロックの **配列** が必要です。プラットフォームはテキストブロックを抽出し、ユーザーメッセージとして対話に送信します。先にカードを `inline` に折りたたみ、その後 Agent が回答します                                                                                                                                                                 |
| `ui/open-link`                                                | プラットフォームが新しいタブを開きます。`https:`、`http:`、`tel:`、`mailto:` のみ許可し、URL 内のユーザー名とパスワードは削除します。許可されない、またはブラウザーにブロックされた場合は `{ "isError": true }` を返します                                                                                                                                            |
| `ui/request-display-mode`                                     | `inline` と `fullscreen` を切り替え、実際に適用したモードを返します                                                                                                                                                                                                                                         |
| `ui/notifications/size-changed`                               | `height` に応じてカードの高さを調整します（全画面時は無視します）                                                                                                                                                                                                                                                 |
| `ui/download-file`、`ui/update-model-context`、`resources/read` | 未対応です。プラットフォームは `-32601 Method not found` を返します                                                                                                                                                                                                                                       |

### 適用範囲 <a href="#mcp-apps-scope" id="mcp-apps-scope"></a>

* カードは Web Chat と MaiGPT の対話に表示します。LINE、Messenger などのほかのメッセージングプラットフォームには、Agent のテキスト応答のみを送信します。
* Agent に追加した MCP ツール（Agent モード）と、メンバーが MaiGPT で使用を認可したコネクタの両方に対応します。MaiAgent 側で追加のスイッチは不要です。
* オンプレミス環境の Web Chat でカードを表示するかは、デプロイ設定によって決まります。MaiAgent の担当者にお問い合わせください。

### 最小の実行可能なサンプル <a href="#mcp-apps-example" id="mcp-apps-example"></a>

Node.js（18 以上）の MCP Server です。注文状況を調べるツール（ダミーデータを返します）が `ui://orders/status` カードに関連付けられ、カードから再照会や AI への質問ができます。3 つのファイルを同じフォルダーに配置します。

{% stepper %}
{% step %}

#### プロジェクトの作成 <a href="#mcp-apps-example-setup" id="mcp-apps-example-setup"></a>

```bash
mkdir order-status-mcp && cd order-status-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod
```

{% endstep %}

{% step %}

#### `server.mjs`：ツールと UI リソース <a href="#mcp-apps-example-server" id="mcp-apps-example-server"></a>

```javascript
import { createServer } from 'node:http'
import { readFileSync } from 'node:fs'
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
import { z } from 'zod'

const PORT = Number(process.env.PORT ?? 8787)
const CARD_URI = 'ui://orders/status'
const CARD_HTML = readFileSync(new URL('./card.html', import.meta.url), 'utf8')

// ダミーデータ：本番環境では自社システムへの問い合わせに置き換えます
const ORDERS = {
  A1001: { status: 'shipped', carrier: '黒猫宅急便', eta: '2026-10-06', items: ['ワイヤレスマウス ×1', 'USB-C ケーブル ×2'] },
  A1002: { status: 'processing', carrier: null, eta: '2026-10-08', items: ['メカニカルキーボード ×1'] },
}

function buildServer() {
  const server = new McpServer({ name: 'order-status-demo', version: '1.0.0' })

  server.registerTool(
    'get_order_status',
    {
      description: '注文の現在の発送状況と配達予定日を取得します',
      inputSchema: { orderId: z.string().describe('注文番号（例：A1001）') },
      annotations: { readOnlyHint: true },
      // インタラクティブカードを関連付けます：プラットフォームは _meta.ui.resourceUri を読み取ります
      _meta: { ui: { resourceUri: CARD_URI } },
    },
    async ({ orderId }) => {
      const order = ORDERS[orderId]
      if (!order) {
        return { isError: true, content: [{ type: 'text', text: `注文 ${orderId} が見つかりません` }] }
      }
      return {
        // content の最初のテキストブロックはモデルが読む内容であり、カードの読み込みに失敗した場合のプレーンテキストの代替表示でもあります
        content: [{ type: 'text', text: `注文 ${orderId}：${order.status}、配達予定日 ${order.eta}` }],
        // カードは structuredContent をデータソースとして使用します
        structuredContent: { orderId, ...order },
      }
    },
  )

  // カードの HTML：ツール呼び出しが成功すると、プラットフォームは同じセッションでこの URI に resources/read を送信します
  server.registerResource(
    'order-status-card',
    CARD_URI,
    { mimeType: 'text/html;profile=mcp-app' },
    async (uri) => ({
      contents: [{ uri: uri.href, mimeType: 'text/html;profile=mcp-app', text: CARD_HTML }],
    }),
  )

  return server
}

createServer(async (req, res) => {
  if (new URL(req.url, 'http://localhost').pathname !== '/mcp') {
    res.writeHead(404).end()
    return
  }
  // ステートレスモード：リクエストごとに server + transport を作成します。プラットフォームはもともと呼び出しごとに新しいセッションを開きます
  const server = buildServer()
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true })
  res.on('close', () => { transport.close(); server.close() })
  await server.connect(transport)
  await transport.handleRequest(req, res)
}).listen(PORT, () => console.log(`MCP server listening on http://localhost:${PORT}/mcp`))
```

{% endstep %}

{% step %}

#### `card.html`：単一ファイルのカード <a href="#mcp-apps-example-card" id="mcp-apps-example-card"></a>

パッケージを読み込まず、`postMessage` で直接 JSON-RPC を使用します。ハンドシェイク、`tool-result` の受信、高さの通知、`tools/call` による再取得、`ui/message` による AI への質問を行います。JS と CSS は同じファイル内に記述します（サンドボックスの CSP は外部リソースの読み込みを許可しません）。

```html
<!doctype html>
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<style>
  body { margin: 0; padding: 16px; font: 14px/1.6 system-ui, sans-serif; color: #1f2937; }
  h4 { margin: 0 0 8px; font-size: 16px; }
  .status { display: inline-block; padding: 2px 8px; border-radius: 999px; background: #e0f2fe; color: #075985; }
  table { border-collapse: collapse; margin: 8px 0; width: 100%; }
  td { padding: 4px 0; border-bottom: 1px solid #e5e7eb; }
  button { margin: 8px 8px 0 0; padding: 8px 14px; border: 0; border-radius: 6px; background: #2563eb; color: #fff; cursor: pointer; }
  button.secondary { background: #6b7280; }
  #hint { color: #6b7280; }
</style>
</head>
<body>
  <div id="view"><p id="hint">読み込み中…</p></div>
  <button id="refresh" hidden>再取得</button>
  <button id="ask" class="secondary" hidden>この注文について AI に質問</button>

<script>
  // ---- ホスト（MaiAgent）との JSON-RPC 通信：すべて window.parent.postMessage を使用します ----
  var pending = {}
  var nextId = 1
  var currentOrder = null

  function request(method, params) {
    var id = nextId++
    return new Promise(function (resolve, reject) {
      pending[id] = { resolve: resolve, reject: reject }
      window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, '*')
    })
  }
  function notify(method, params) {
    window.parent.postMessage({ jsonrpc: '2.0', method: method, params: params || {} }, '*')
  }

  window.addEventListener('message', function (event) {
    var msg = event.data
    if (!msg || msg.jsonrpc !== '2.0') return
    // 1) こちらから送信したリクエストへの応答です
    if (msg.id !== undefined && !msg.method) {
      var p = pending[msg.id]
      if (!p) return
      delete pending[msg.id]
      msg.error ? p.reject(msg.error) : p.resolve(msg.result)
      return
    }
    // 2) ホストからの通知：ハンドシェイク完了後、まず tool-input、次に tool-result を受信します
    if (msg.method === 'ui/notifications/tool-result') render(msg.params)
    // 3) ホストがカードの終了処理を要求した場合は、空の結果を返すだけで構いません
    if (msg.method === 'ui/resource-teardown' && msg.id !== undefined) {
      window.parent.postMessage({ jsonrpc: '2.0', id: msg.id, result: {} }, '*')
    }
  })

  // ---- 画面 ----
  function render(result) {
    var data = result && result.structuredContent
    var view = document.getElementById('view')
    if (!data || result.isError) {
      var text = result && result.content && result.content[0] ? result.content[0].text : 'データがありません'
      view.innerHTML = '<p id="hint"></p>'
      view.firstChild.textContent = text
    } else {
      currentOrder = data.orderId
      var rows = (data.items || []).map(function (i) { return '<tr><td>' + escapeHtml(i) + '</td></tr>' }).join('')
      view.innerHTML =
        '<h4>注文 ' + escapeHtml(data.orderId) + ' <span class="status">' + escapeHtml(data.status) + '</span></h4>' +
        '<div>配達予定日：' + escapeHtml(data.eta) + (data.carrier ? '（' + escapeHtml(data.carrier) + '）' : '') + '</div>' +
        '<table>' + rows + '</table>'
      document.getElementById('refresh').hidden = false
      document.getElementById('ask').hidden = false
    }
    // 高さを通知すると、ホストはその値に基づいてカードの高さを調整します
    notify('ui/notifications/size-changed', { height: document.body.scrollHeight })
  }
  function escapeHtml(s) {
    return String(s).replace(/[&<>"]/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c] })
  }

  // カード内の操作：ホストが同じ MCP server のツールを代理で呼び出します
  document.getElementById('refresh').addEventListener('click', function () {
    request('tools/call', { name: 'get_order_status', arguments: { orderId: currentOrder } })
      .then(render)
      .catch(function (err) { console.error('tools/call failed', err) })
  })
  // カード内の質問：テキストはユーザーメッセージとして会話に送信され、Agent が続けて回答します
  document.getElementById('ask').addEventListener('click', function () {
    request('ui/message', { role: 'user', content: [{ type: 'text', text: '注文 ' + currentOrder + ' はいつ届きますか？' }] })
  })

  // ---- ハンドシェイク：ui/initialize → ui/notifications/initialized の後に、ホストが tool-result を送信します ----
  request('ui/initialize', {
    appInfo: { name: 'order-status-card', version: '1.0.0' },
    appCapabilities: {},
    protocolVersion: '2026-01-26',
  }).then(function () {
    notify('ui/notifications/initialized')
  })
</script>
</body>
</html>
```

{% endstep %}

{% step %}

#### 起動と検証 <a href="#mcp-apps-example-verify" id="mcp-apps-example-verify"></a>

```bash
node server.mjs
# MCP server listening on http://localhost:8787/mcp
```

別のターミナルを開き、プラットフォームと同じ順序で 3 つのリクエストを送信します（ステートレスモードではセッションヘッダーは不要です）：

```bash
MCP=http://localhost:8787/mcp
H='-H Content-Type:application/json -H Accept:application/json,text/event-stream'

# 1) tools/list：ツールに _meta.ui.resourceUri が含まれていることを確認します
curl -s $MCP $H -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# {"result":{"tools":[{"name":"get_order_status", ... ,"_meta":{"ui":{"resourceUri":"ui://orders/status"}}}]}, ...}

# 2) tools/call：content のテキストと structuredContent があることを確認します
curl -s $MCP $H -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_order_status","arguments":{"orderId":"A1001"}}}'
# {"result":{"content":[{"type":"text","text":"注文 A1001：shipped、配達予定日 2026-10-06"}],"structuredContent":{"orderId":"A1001","status":"shipped", ...}}, ...}

# 3) resources/read：HTML 全体が返されることを確認します
curl -s $MCP $H -d '{"jsonrpc":"2.0","id":3,"method":"resources/read","params":{"uri":"ui://orders/status"}}'
# {"result":{"contents":[{"uri":"ui://orders/status","mimeType":"text/html;profile=mcp-app","text":"<!doctype html>..."}]}, ...}
```

3 つとも正しい応答が返れば、プラットフォーム側でカードを表示できます。
{% endstep %}
{% endstepper %}

### MaiAgent に接続する手順 <a href="#mcp-apps-example-connect" id="mcp-apps-example-connect"></a>

1. Server にインターネットから `https://` でアクセスできるようにします（ローカルテストでは、`cloudflared tunnel --url http://localhost:8787` や `ngrok http 8787` などのツールで一時的な URL を取得できます）。MCP エンドポイントは `https://<你的網域>/mcp` です。
2. ユーザーマニュアルの [MCP ツールの作成](https://docs.maiagent.ai/build/tools/mcp-setup) に従って管理画面で MCP ツールを追加し、URL に前の手順のエンドポイントを入力します。この例ではヘッダーは不要です。「許可されたツール」の一覧を再取得すると、`get_order_status` が表示されます。
3. 回答モードが Agent モードの Agent にツールを設定し、Web Chat または MaiGPT で「注文 A1001 は今どこにありますか？」と質問します。Agent の回答が完了すると、回答の下にカードが表示されます。<mark style="color:blue;">再取得</mark> をクリックすると `tools/call`、<mark style="color:blue;">この注文について AI に質問</mark> をクリックすると `ui/message` が実行されます。ユーザー側の動作については [MCP インタラクティブカード（MCP Apps）](https://docs.maiagent.ai/build/tools/mcp-apps) を参照してください。

### よくあるエラー <a href="#mcp-apps-example-errors" id="mcp-apps-example-errors"></a>

| 現象                                               | 原因                                                                                                                                                                                                                                |
| ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Agent がツールを呼び出してもカードが表示されません                     | `tools/list` のツール定義と `tools/call` の結果のどちらにも `_meta.ui.resourceUri` がありません（または値が `ui://` で始まっていません）。ツールが `isError: true` を返しているか、LINE などカードを表示しないチャネルを使用している場合もあります                                                                |
| カードの位置にプレーンテキストだけが表示されます                         | `resources/read` が失敗するか空の内容を返すと、プラットフォームは `content` の最初のテキストブロックを代わりに表示します。上記の 3 番目の curl で確認してください                                                                                                                               |
| カードが「読み込み中…」のままです                                | カードのハンドシェイクが完了していません（`ui/initialize` の `params` に `appInfo`／`appCapabilities`／`protocolVersion` がないか、`ui/notifications/initialized` を送信していません）。この場合、プラットフォームは `tool-result` を送信しません。カードの HTML が読み込む外部 JS が CSP にブロックされている可能性もあります |
| カードが空白です                                         | HTML が依存する外部 CSS／JS／画像がサンドボックスの CSP にブロックされています。同じ HTML ファイルにまとめるか、Registry で `cspAllowedOrigins` を設定してください                                                                                                                       |
| カードに「この操作は完了しませんでした。しばらくしてからもう一度お試しください。」と表示されます | カードの `tools/call` がプラットフォームに拒否されたか、失敗しています。ツール名がその MCP ツールの許可リストにない、ツールが Agent から削除された、認可が失効した、Server がエラーを返した、または 30 秒を超えて応答しなかった可能性があります                                                                                        |
| カードの「AI に質問」が反応しません                              | `ui/message` の `content` が配列ではないか、`type: "text"` のブロックがありません                                                                                                                                                                      |

## 公開前のチェックリスト <a href="#checklist" id="checklist"></a>

* [ ] エンドポイントが `https://` でインターネットからアクセスでき、Streamable HTTP を使用しています。SSE のみに対応する場合、URL の末尾は `/sse` です。
* [ ] 固定ヘッダーだけを指定した場合でも `tools/list` が正常に応答します。
* [ ] 各ツールの `description` に用途が明記され、`inputSchema` が有効な JSON Schema になっています。
* [ ] 各ツールに `readOnlyHint`／`idempotentHint` を宣言しています。
* [ ] 複数の呼び出しにまたがる MCP セッションに依存していません。状態が必要な場合は `X-Conversation-Id` またはユーザー ID を使用します。
* [ ] ユーザー単位の権限：連絡先の MCP 認証情報を書き込むタイミングを計画しています（ログイン時に同期し、ログアウト時に削除します）。
* [ ] OAuth を使用する場合：RFC 9728 のリソースメタデータ、RFC 8414 の認可サーバーメタデータ、PKCE（S256）、`resource` パラメータに対応しています。動的登録に対応しない場合は、両方の Callback URL を登録済みです。
* [ ] Server にツールを追加した後、ツール設定に戻って一覧を再取得します。
* [ ] インタラクティブカードを提供する場合：`_meta.ui.resourceUri` で `ui://` リソースを関連付け、`resources/read` で HTML 全体を返します。カードは `structuredContent` をデータソースとし、外部リクエストを送信しません（必要な場合は Registry で `cspAllowedOrigins` を設定します）。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.maiagent.ai/tech/ja/mcp/self-hosted-mcp-server.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
