mcpField
A
add-app-to-server

add-app-to-server

github.com/modelcontextprotocol開発3.0k1.2k 回インストール2 日前に更新
リポジトリ

add-app-to-server について

既存の MCP サーバーに MCP Apps SDK でインタラクティブな UI を追加する開発者向けスキルです。ツールの選定からリソース登録、UI のビルド、テストまで扱い、テキスト出力も維持します。

主な機能

  1. 01UI 化するツールの評価
  2. 02ツールと HTML リソースの連携
  3. 03テキストのみの出力を維持
  4. 04複数フレームワークでの UI 構築
  5. 05ホストスタイルと CSP の設定

活用シーン

  1. 01ツールのリストや指標を可視化
  2. 02ツール UI で地図や PDF を表示
  3. 03UI 専用のツールを追加

MCP 対応

MCP Apps SDK を使い、既存の MCP サーバーツールと HTML リソースを連携させる方法を案内します。ツール呼び出し時に MCP クライアントが UI を表示でき、テキストのみのクライアントにはテキスト結果を返せます。

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/modelcontextprotocol/ext-apps --skill add-app-to-server

インストール後、Claude Code で/add-app-to-serverを入力して読み込まれたか確認します。

詳細

MCP サーバーに UI を追加する

MCP Apps SDK(@modelcontextprotocol/ext-apps)を使って、既存の MCP サーバーのツールにインタラクティブな UI を追加します。

仕組み

既存のツールに、ホストの会話内にインライン表示される HTML リソースを組み合わせます。ツールはテキストのみのクライアントでも引き続き動作します。UI は機能を強化するものであり、置き換えるものではありません。UI のメリットがある各ツールを _meta.ui.resourceUri でリソースにリンクすると、ツールが呼び出されたときにホストがそのリソースをサンドボックス化された iframe に表示します。

参考コードの入手

SDK リポジトリをクローンして、動作するサンプルと API ドキュメントを確認します。

git clone --branch "v$(npm view @modelcontextprotocol/ext-apps version)" --depth 1 https://github.com/modelcontextprotocol/ext-apps.git /tmp/mcp-ext-apps
API リファレンス(ソースファイル)

/tmp/mcp-ext-apps/src/ の JSDoc ドキュメントを直接読みます。

ファイル内容
src/app.tsApp クラス、ハンドラー(ontoolinput、ontoolresult、onhostcontextchanged、onteardown)、ライフサイクル
src/server/index.tsregisterAppTool、registerAppResource、getUiCapability、ツールの表示オプション
src/spec.types.tsすべての型定義:McpUiHostContext、CSS 変数キー、表示モード
src/styles.tsapplyDocumentTheme、applyHostStyleVariables、applyHostFonts
src/react/useApp.tsxReact アプリ向けの useApp フック
src/react/useHostStyles.tsuseAppStyles、useHostStyleVariables、useHostFonts フック
主なサンプル(複数のツールパターン)

これらのサンプルでは、App 対応ツールと通常のツールの両方を備えたサーバーを紹介しています。今回追加するパターンそのものです。

サンプルパターン
examples/map-server/show-map(App ツール)+ geocode(通常のツール)
examples/pdf-server/display_pdf(App ツール)+ list_pdfs(通常のツール)+ read_pdf_bytes(App 専用ツール)
examples/system-monitor-server/get-system-info(App ツール)+ poll-system-stats(App 専用のポーリングツール)
フレームワークテンプレート

/tmp/mcp-ext-apps/examples/basic-server-{framework}/ を参考にし、適宜取り入れます。

テンプレート主なファイル
basic-server-vanillajs/server.ts、src/mcp-app.ts、mcp-app.html
basic-server-react/server.ts、src/mcp-app.tsx(useApp フックを使用)
basic-server-vue/server.ts、src/App.vue
basic-server-svelte/server.ts、src/App.svelte
basic-server-preact/server.ts、src/mcp-app.tsx
basic-server-solid/server.ts、src/mcp-app.tsx

ステップ 1:既存のツールを分析する

コードを書く前に、サーバーの既存ツールを分析し、どのツールに UI のメリットがあるかを判断します。

  1. サーバーのソースを読み、登録されているツールをすべて列挙します
  2. 各ツールについて、UI のメリットがあるか(視覚化できるデータを返す、ユーザーとのやり取りがあるなど)、テキストのみで問題ないか(単純な検索、ユーティリティ関数など)を評価します
  3. App 専用ヘルパーにできるツール(UI がポーリングまたは取得する必要があるが、モデルが直接呼び出す必要はないデータ)を特定します
  4. 分析結果をユーザーに提示し、どのツールを拡張するか確認します
判断基準
ツールの出力タイプUI のメリット例
構造化データ/リスト/テーブル高 — インタラクティブなテーブル、検索、フィルタリングアイテム一覧、検索結果
時系列のメトリクス/数値高 — グラフ、ゲージ、ダッシュボードシステム統計、分析データ
メディア/リッチコンテンツ高 — ビューアー、プレーヤー、レンダラー地図、PDF、画像、動画
単純なテキスト/確認メッセージ低 — テキストで十分「ファイルを作成しました」、「設定を更新しました」
他のツール向けのデータApp 専用を検討ポーリング用エンドポイント、チャンクローダー

ステップ 2:依存関係の追加

npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@^2.0.0 @modelcontextprotocol/server@^2.0.0 zod@^4.2.0
npm install -D vite vite-plugin-singlefile

必要に応じてフレームワーク固有の依存関係(React の場合は react、react-dom、@vitejs/plugin-react など)と、サーバーが HTTP トランスポートを使用する場合は @modelcontextprotocol/node / @modelcontextprotocol/express も追加します。

ext-apps 2.x では、分割されたベース MCP SDK パッケージのバージョン ^2.0.0 が必要です (@modelcontextprotocol/core は依存関係を介して追加されます)。引き続き @modelcontextprotocol/sdk v1 をインポートしている既存のサーバーでは、App 統合を追加する前に インポートとハンドラースキーマを移行する必要があります。

SDK v1(@modelcontextprotocol/sdk)SDK v2
sdk/server/mcp.js(McpServer)@modelcontextprotocol/server
sdk/server/streamableHttp.js(StreamableHTTPServerTransport)@modelcontextprotocol/node の NodeStreamableHTTPServerTransport
手動での Express 接続@modelcontextprotocol/express の createMcpExpressApp
sdk/server/stdio.js@modelcontextprotocol/server/stdio
sdk/types.js の型(CallToolResult、…)@modelcontextprotocol/client または @modelcontextprotocol/server
sdk/types.js の zod スキーマ(CallToolResultSchema、…)@modelcontextprotocol/core
生の zod 形状:inputSchema: { q: z.string() }inputSchema: z.object({ q: z.string() })
ツールコールバック内の extra.signalextra.mcpReq.signal
setRequestHandler(SomeRequestSchema, handler)setRequestHandler("some/method", { params: ParamsSchema }, handler)(2 引数形式は "tools/call" などの仕様上のメソッドに限ります)

ステップ 3:ビルドパイプラインの設定

Vite の設定

vite-plugin-singlefile を使用して UI を単一の HTML ファイルにバンドルするため、vite.config.ts を作成します。

import { defineConfig } from "vite";
import { viteSingleFile } from "vite-plugin-singlefile";

export default defineConfig({
  plugins: [viteSingleFile()],
  build: {
    outDir: "dist",
    rollupOptions: {
      input: "mcp-app.html", // one per UI, or one shared entry
    },
  },
});
HTML エントリーポイント

mcp-app.html を作成します(ツールごとに異なるビューが必要な場合は、UI ごとに 1 つ作成します)。

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>MCP App</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="./src/mcp-app.ts"></script>
  </body>
</html>
ビルドスクリプト

package.json にビルドスクリプトを追加します。サーバーコードをバンドルする前に、UI をビルドする必要があります。

{
  "scripts": {
    "build:ui": "vite build",
    "build:server": "tsc",
    "build": "npm run build:ui && npm run build:server",
    "serve": "tsx server.ts"
  }
}

ステップ 4:ツールを App ツールに変換

通常の MCP ツールを、UI を備えた App ツールに変換します。

変換前(通常の MCP ツール):

server.registerTool(
  "my-tool",
  { inputSchema: z.object({ param: z.string() }) },
  async ({ param }) => {
    const data = await fetchData(param);
    return { content: [{ type: "text", text: JSON.stringify(data) }] };
  },
);

変換後(UI を備えた App ツール):

import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";

const resourceUri = "ui://my-tool/mcp-app.html";

registerAppTool(
  server,
  "my-tool",
  {
    description: "Shows data with an interactive UI",
    inputSchema: z.object({ param: z.string() }),
    _meta: { ui: { resourceUri } },
  },
  async ({ param }) => {
    const data = await fetchData(param);
    return {
      content: [{ type: "text", text: JSON.stringify(data) }], // text fallback for non-UI hosts
      structuredContent: { data }, // structured data for the UI
    };
  },
);

重要なガイドライン:

  • テキストのみを扱うクライアント向けのテキストフォールバックを含む content 配列は、必ず保持してください
  • UI の描画に必要なデータには structuredContent を追加します
  • _meta.ui.resourceUri を使って、ツールをそのリソースに関連付けます
  • UI の利点がないツールは変更せず、通常のツールのままにします

ステップ 5:リソースの登録

ホストが取得できるように、HTML リソースを登録します。

import fs from "node:fs/promises";
import path from "node:path";

const resourceUri = "ui://my-tool/mcp-app.html";

registerAppResource(
  server,
  "My Tool UI",
  resourceUri,
  {},
  async () => {
    const html = await fs.readFile(
      path.resolve(import.meta.dirname, "dist", "mcp-app.html"),
      "utf-8",
    );
    return {
      contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }],
    };
  },
);

複数のツールで同じ UI を共有する場合は、同じ resourceUri とリソース登録を参照できます。

ステップ 6:UI の構築

ハンドラーの登録

app.connect() を呼び出す前に、すべてのハンドラーを登録します。

import { App, PostMessageTransport, applyDocumentTheme, applyHostStyleVariables, applyHostFonts } from "@modelcontextprotocol/ext-apps";

const app = new App({ name: "My App", version: "1.0.0" });

app.ontoolinput = (params) => {
  // Render the UI using params.arguments and/or params.structuredContent
};

app.ontoolresult = (result) => {
  // Update UI with final tool result
};

app.onhostcontextchanged = (ctx) => {
  if (ctx.theme) applyDocumentTheme(ctx.theme);
  if (ctx.styles?.variables) applyHostStyleVariables(ctx.styles.variables);
  if (ctx.styles?.css?.fonts) applyHostFonts(ctx.styles.css.fonts);
  if (ctx.safeAreaInsets) {
    const { top, right, bottom, left } = ctx.safeAreaInsets;
    document.body.style.padding = `${top}px ${right}px ${bottom}px ${left}px`;
  }
};

app.onteardown = async () => {
  return {};
};

await app.connect(new PostMessageTransport());
ホストのスタイル設定

テーマを連携させるには、ホストの CSS 変数を使用します。

.container {
  background: var(--color-background-secondary);
  color: var(--color-text-primary);
  font-family: var(--font-sans);
  border-radius: var(--border-radius-md);
}

主な変数グループは、--color-background-*、--color-text-*、--color-border-*、--font-sans、--font-mono、--font-text-*-size、--font-heading-*-size、--border-radius-* です。全一覧は src/spec.types.ts を参照してください。

React アプリでは、代わりに useApp と useHostStyles フックを使用します。パターンについては basic-server-react/ を参照してください。

任意の機能強化

App 専用のヘルパーツール

UI から呼び出すものの、モデルが直接呼び出す必要のないツール(ポーリング、ページネーション、チャンクの読み込みなど):

registerAppTool(server, "poll-data", {
  description: "Polls latest data for the UI",
  _meta: { ui: { resourceUri, visibility: ["app"] } },
}, async () => {
  const data = await getLatestData();
  return { content: [{ type: "text", text: JSON.stringify(data) }] };
});

UI からは、次のように呼び出します。 app.callServerTool({ name: "poll-data", arguments: {} })。

CSP の設定

UI で外部リソース(フォント、API、CDN)を読み込む必要がある場合は、ドメインを宣言します。

registerAppResource(server, "My Tool UI", resourceUri, {}, async () => ({
  contents: [
    {
      uri: resourceUri,
      mimeType: RESOURCE_MIME_TYPE,
      text: html,
      _meta: {
        ui: {
          csp: {
            connectDomains: ["https://api.example.com"], // fetch/XHR targets
            resourceDomains: ["https://cdn.example.com"], // scripts, styles, images
            frameDomains: ["https://embed.example.com"], // nested iframes
          },
        },
      },
    },
  ],
}));
部分入力のストリーミング

大きなツール入力の場合、LLM による生成中に進行状況を表示します。

app.ontoolinputpartial = (params) => {
  const args = params.arguments; // Healed partial JSON - always valid
  // Render preview with partial data
};

app.ontoolinput = (params) => {
  // Final complete input - switch to full render
};
getUiCapability() によるグレースフルデグラデーション

クライアントが UI に対応している場合に限り App ツールを登録し、対応していない場合はテキストのみのツールにフォールバックします。

import { getUiCapability, registerAppTool, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";

server.server.oninitialized = () => {
  const clientCapabilities = server.server.getClientCapabilities();
  const uiCap = getUiCapability(clientCapabilities);

  if (uiCap?.mimeTypes?.includes(RESOURCE_MIME_TYPE)) {
    // Client supports UI — register App tool
    registerAppTool(server, "my-tool", {
      description: "Shows data with interactive UI",
      _meta: { ui: { resourceUri } },
    }, appToolHandler);
  } else {
    // Text-only client — register plain tool
    server.registerTool(
      "my-tool",
      {
        description: "Shows data",
        inputSchema: z.object({ param: z.string() }),
      },
      plainToolHandler,
    );
  }
};
全画面モード

UI を全画面表示に拡張できるようにします。

app.onhostcontextchanged = (ctx) => {
  if (ctx.availableDisplayModes?.includes("fullscreen")) {
    fullscreenBtn.style.display = "block";
  }
  if (ctx.displayMode) {
    container.classList.toggle("fullscreen", ctx.displayMode === "fullscreen");
  }
};

async function toggleFullscreen() {
  const newMode = currentMode === "fullscreen" ? "inline" : "fullscreen";
  const result = await app.requestDisplayMode({ mode: newMode });
  currentMode = result.mode;
}

よくある間違いとその回避方法

  1. テキストの content フォールバックを忘れる — UI をサポートしないホスト向けに、テキストを含む content 配列を必ず用意してください
  2. connect() の後にハンドラーを登録する — app.connect() を呼び出す前に、すべてのハンドラーを登録してください
  3. vite-plugin-singlefile が不足している — これがないと、サンドボックス化された iframe でアセットが読み込まれません
  4. リソースの登録を忘れる — ツールが参照する resourceUri に対応するリソースが必要です
  5. スタイルをハードコードする — テーマを連携するには、ホストの CSS 変数(var(--color-*))を使用してください
  6. セーフエリアのインセットを処理しない — onhostcontextchanged で必ず ctx.safeAreaInsets を適用してください

テスト

basic-host を使用する

basic-host の例を使って、拡張したサーバーをテストします。

# Terminal 1: Build and run your server
npm run build && npm run serve

# Terminal 2: Run basic-host (from cloned repo)
cd /tmp/mcp-ext-apps/examples/basic-host
npm install
SERVERS='["http://localhost:3001/mcp"]' npm run start
# Open http://localhost:8080

サーバー URL の JSON 配列を使って SERVERS を設定してください(デフォルト:http://localhost:3001/mcp)。

確認項目
  1. 通常のツールが引き続き動作し、テキスト出力を返す
  2. アプリツールが iframe 内に UI を描画する
  3. ontoolinput ハンドラーがツールの引数で呼び出される
  4. ontoolresult ハンドラーがツールの結果で呼び出される
  5. ホストのスタイル(テーマ、フォント、色)が正しく適用される

コメント