公式の`convert-web-app` スキルを使って既存のWebアプリをMCP Appに対応させてみた

公式の`convert-web-app` スキルを使って既存のWebアプリをMCP Appに対応させてみた

2026.03.25

リテールアプリ共創部のるおんです。

これまでMCP Appsについて、概要と仕組みの解説、create-mcp-app スキルを使ったゼロからの開発と2本の記事を書いてきました。

https://dev.classmethod.jp/articles/mcp-apps-introduction-overview/

https://dev.classmethod.jp/articles/create-mcp-app-skill-pomodoro-timer/

今回は 既存のWebアプリをMCP Appに対応させる という、より実践的なシナリオに挑戦してみました。使用したのは、MCP Apps公式リポジトリで提供されている convert-web-app スキルです。

既存のECショップWebアプリ(React + Vite + shadcn/ui)を、スタンドアロンでもMCP Appとしても動作する ハイブリッドアプリ に変換し、Claude上で商品一覧がインタラクティブに表示されるところまでを試してみました。

「ECショップを見せてほしい」と言うプロンプトに対してECサイトがMCP Appとして表示されています。

スクリーンショット 2026-03-24 23.15.21

前提:今回変換するWebアプリ

今回MCP App化するのは、React + Vite + TypeScript で構築されたシンプルなECショップWebアプリです。UIコンポーネントにはshadcn/uiを使用しています。

主な機能は以下の通りです。

  • 商品一覧表示(カテゴリフィルター付き)
  • ショッピングカート(追加・削除・数量変更)
  • shadcn/uiの Card, Button, Badge, Sheet を使用

商品データはフロントエンド側にハードコードされており、外部APIは使用していません。画像はUnsplash(外部CDN)から取得しています。

スクリーンショット 2026-03-24 23.53.56

この一般的なECサイトのwebアプリをMCP Appに対応させるのが今回のテーマです

convert-web-app スキルとは

convert-web-app は、MCP Apps公式リポジトリ ext-apps で提供されている AIコーディングエージェント向けのスキル です。

前回の記事で紹介した create-mcp-app がゼロからMCP Appを作るためのスキルだったのに対し、convert-web-app既存のWebアプリをMCP App対応に変換 するためのスキルです。

スキル 用途
create-mcp-app ゼロからMCP Appを新規作成
convert-web-app 既存のWebアプリをMCP App対応に変換
add-app-to-server 既存のMCPサーバーにUIを追加
migrate-oai-app OpenAI Apps SDKからの移行

convert-web-app スキルは、既存のWebアプリの構造を分析し、以下を自動で行ってくれます。

  • MCP App用のエントリーポイント(mcp-app.html, mcp-main.tsx)の作成
  • MCPサーバー(server.ts, main.ts)の作成
  • Vite設定の更新(vite-plugin-singlefile によるシングルファイルバンドル対応)
  • 既存のスタンドアロン動作を維持したままのハイブリッド化

ハイブリッド化の設計

convert-web-app スキルの核心は、既存のWebアプリを壊さずにMCP App対応を追加する という点です。

スタンドアロン:  ブラウザ → index.html → main.tsx → App.tsx → レンダリング
MCP App:        ホスト → ツール呼び出し → mcp-app.html → mcp-main.tsx → App.tsx → レンダリング

Appコンポーネントの描画ロジックは共有し、データの取得元だけが変わる という構造です。スタンドアロンではURLパラメータやローカルの状態から、MCP Appではツール引数からデータを受け取ります。

やってみた

スキルの実行

Claude Codeでスキルがインストールされた状態で、/mcp-apps:convert-web-app を実行しました。スキルが起動すると、Claude Codeが以下のステップを自動で進めてくれます。

  1. 既存アプリの分析 - データソース、外部依存、ビルドシステム、ユーザー操作の調査
  2. CSP要件の調査 - 外部画像(Unsplash)へのアクセスに必要なCSP設定の特定
  3. MCPサーバーの作成 - ツールとリソースの登録
  4. ビルドパイプラインの更新 - シングルファイルバンドル対応
  5. MCP App初期化の追加 - ハイブリッドエントリーポイントの作成
  6. ホストスタイリングの統合 - safeAreaInsetsへの対応

実際の作業はClaude Codeが一気に実行してくれるので、開発者は結果を確認して微調整するだけです。

生成されたプロジェクト構成

変換後のプロジェクトは以下のようになりました。元のファイルに加えて、MCP App用のファイルが追加されています。

ec-shop/
├── index.html           ← スタンドアロン用(既存)
├── src/
│   ├── main.tsx             ← スタンドアロン用エントリー(既存)
│   ├── App.tsx              ← 共通のUIコンポーネント(修正)
+│   └── mcp-main.tsx        ← MCP App用エントリー(新規)
├── mcp-app.html         ← MCP App用HTMLシェル(新規)
+├── server.ts            ← MCPサーバー(新規)
+├── main.ts              ← サーバーエントリーポイント(新規)
+├── tsconfig.server.json ← サーバー用TypeScript設定(新規)
├── vite.config.ts       ← ビルド設定(修正)
└── package.json         ← スクリプト追加(修正)

サーバー側:server.ts

MCPサーバーでは、show-ec-shop というMCP Appsツールを登録しています。

server.ts
registerAppTool(
  server,
  "show-ec-shop",
  {
    title: "EC Shop",
    description:
      "ECショップの商品一覧を表示します。カテゴリを指定して商品をフィルターできます。",
    inputSchema: {
      category: z
        .string()
        .optional()
        .describe(
          "表示する商品カテゴリ(例: オーディオ, バッグ, ガジェット, キッチン, スポーツ, インテリア)",
        ),
    },
+   _meta: { ui: { resourceUri } },
  },
  async (args): Promise<CallToolResult> => {
    const category = args.category ?? "すべて";
    return {
      content: [
        { type: "text", text: `EC Shopを表示しています(カテゴリ: ${category})` },
      ],
+     structuredContent: { category },
    };
  },
);
  • _meta: { ui: { resourceUri } } で、このツールがUI付きのMCP Appsツールであることを宣言
  • structuredContent でカテゴリ情報をApp側に渡す。これにより「オーディオのカテゴリを見せて」と指示すると、該当カテゴリでフィルターされた状態で表示される
  • テキストの content も併せて返すことで、UI非対応のホストでもテキストとして結果を表示できる

リソース登録では、Unsplash画像を読み込むためのCSP設定も行っています。

server.ts
registerAppResource(
  server,
  resourceUri,
  resourceUri,
  {
    mimeType: RESOURCE_MIME_TYPE,
+   _meta: {
+     ui: {
+       csp: {
+         resourceDomains: ["https://images.unsplash.com"],
+       },
+     },
+   },
  },
  async (): Promise<ReadResourceResult> => {
    const html = await fs.readFile(
      path.join(DIST_DIR, "mcp-app.html"),
      "utf-8",
    );
    return {
      contents: [
        { uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html },
      ],
    };
  },
);

MCP Appはサンドボックス化されたiframe内で動作するため、外部リソースへのアクセスには CSPの明示的な宣言 が必要です。resourceDomains にUnsplashのドメインを指定することで、商品画像が正しく表示されるようになります。

App側:src/mcp-main.tsx

MCP App用のエントリーポイントです。useApp フックでホストとの接続を確立し、ツール引数からカテゴリ情報を受け取ります。

src/mcp-main.tsx
import { useApp } from "@modelcontextprotocol/ext-apps/react";

function McpApp() {
  const [category, setCategory] = useState<string>("すべて");
  const [hostContext, setHostContext] = useState<McpUiHostContext | undefined>();

  const { app, error } = useApp({
    appInfo: { name: "EC Shop", version: "1.0.0" },
    capabilities: {},
    onAppCreated: (app) => {
+     app.ontoolinput = async (input) => {
+       const cat = input.arguments?.category as string | undefined;
+       if (cat) setCategory(cat);
+     };

+     app.ontoolresult = async (result) => {
+       const cat = result.structuredContent?.category as string | undefined;
+       if (cat) setCategory(cat);
+     };

      app.onhostcontextchanged = (ctx) => {
        setHostContext((prev) => ({ ...prev, ...ctx }));
      };
    },
  });

  // ...
  return (
    <div style={{
      paddingTop: hostContext?.safeAreaInsets?.top,
      paddingBottom: hostContext?.safeAreaInsets?.bottom,
    }}>
+     <App initialCategory={category} />
    </div>
  );
}
  • app.ontoolinput でツール入力(カテゴリ)をリアルタイムに受け取る
  • app.ontoolresultstructuredContent からカテゴリ情報を取得
  • app.onhostcontextchanged でホストのテーマ変更や safeAreaInsets に対応
  • 共通の App コンポーネントに initialCategory を渡すことで、MCP Appでもスタンドアロンでも同じUIを表示

既存Appコンポーネントの変更

元の App.tsx への変更は最小限です。initialCategory propsを追加しただけです。

src/App.tsx
+interface AppProps {
+  initialCategory?: string;
+}

-function App() {
+function App({ initialCategory = "すべて" }: AppProps) {
  const [cart, setCart] = useState<CartItem[]>([]);
- const [selectedCategory, setSelectedCategory] = useState("すべて");
+ const [selectedCategory, setSelectedCategory] = useState(initialCategory);
  // ...残りはそのまま

スタンドアロンで動作する場合は initialCategory が未指定(デフォルト値 "すべて")となり、従来通りの動作になります。

Vite設定の更新

vite.config.ts では、環境変数 INPUT の有無でスタンドアロンビルドとMCP Appビルドを切り替えています。

vite.config.ts
const INPUT = process.env.INPUT;

export default defineConfig({
  plugins: [react(), tailwindcss(), ...(INPUT ? [viteSingleFile()] : [])],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
  build: {
    ...(INPUT
      ? {
          rollupOptions: { input: INPUT },
          outDir: "dist",
          emptyOutDir: false,
        }
      : {}),
  },
});
  • INPUT が未指定 → 通常のViteビルド(スタンドアロン)
  • INPUT=mcp-app.htmlvite-plugin-singlefile でHTML/CSS/JSを1ファイルにバンドル(MCP App用)

package.json のスクリプトは以下のようになります。

package.json
{
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "build:mcp": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json",
    "serve": "tsx main.ts",
    "start:mcp": "npm run build:mcp && npm run serve"
  }
}

Claudeでテストしてみた

ビルドとサーバー起動

bash
npm run build:mcp
npx tsx main.ts

http://localhost:3002/mcp でMCPサーバーが起動します。

cloudflaredでトンネルを作成

Claude(Web版/Desktop版)からローカルサーバーにアクセスするには、インターネットに公開する必要があります。cloudflared を使ってトンネルを作成します。

bash
npx cloudflared tunnel --url http://localhost:3002

実行すると、https://random-name.trycloudflare.com のようなURLが生成されます。

Claudeにカスタムコネクターを追加

  1. claude.ai または Claude Desktop を開く
  2. プロフィールアイコンをクリック → SettingsConnectors
  3. Add custom connector をクリック
  4. URLに https://生成されたURL/mcp を入力
  5. 保存

動かしてみる

Claudeに「ECショップを見せてほしい」と入力すると、show-ec-shop ツールが呼び出され、会話内にECショップのUIが表示されました。

スクリーンショット 2026-03-24 23.15.21

商品カード、カテゴリフィルター、カートなど、スタンドアロンで動いていたすべての機能がClaude内でそのまま動作しています。「オーディオのカテゴリだけ見せて」のようにカテゴリを指定すると、フィルターされた状態で表示されます。

create-mcp-app との違い

前回の create-mcp-app スキルとの違いを整理します。

create-mcp-app convert-web-app
入力 自然言語での指示 既存のWebアプリのコードベース
出力 新規プロジェクト一式 既存プロジェクトへの差分追加
既存コードの影響 なし(新規作成) 最小限の変更で既存動作を維持
ビルド MCP App専用 スタンドアロンとMCP Appのデュアルビルド
ユースケース プロトタイプ、新規開発 既存WebアプリのMCP App対応

convert-web-app の最大のメリットは、既存のWebアプリをそのまま維持しながら、MCP Appとしても動作させられる 点です。ブラウザで直接アクセスすれば従来通りのWebアプリとして、Claude経由で呼び出せばMCP Appとして動作する。1つのコードベースで2つの配信チャネルを持てるということです。

注意点

実際に試してみて気づいた点をいくつか挙げます。

  • ローカル画像はMCP Appで表示されない:MCP Appはサンドボックス化されたiframe内で動作するため、/smartwatch.svg のようなローカルファイルパスは使えません。画像はBase64でインライン化するか、外部CDNから取得する必要があります
  • CSPの宣言漏れに注意:外部ドメインへのアクセスはすべて resourceDomains(画像・フォント等)や connectDomains(API等)で明示する必要があります。宣言漏れはサイレントに失敗するため、開発時に気づきにくいです
  • スキルの出力は確認が必要:スキルはAIエージェントへの知識注入であり、生成されるコードの品質はAIの能力に依存します。特にCSP設定やハンドラーの登録順序など、MCP Apps固有のパターンは出力後に確認するのがおすすめです

おわりに

今回は、convert-web-app スキルを使って既存のECショップWebアプリをMCP Appに対応させてみました。

既存のWebアプリを壊さずにMCP App対応を追加できるのは、実運用を考えると非常に重要です。「まずはWebアプリとして開発し、後からMCP App対応を追加する」というアプローチが取れるため、MCP Appsの導入ハードルがぐっと下がります。

これまでの3本の記事を通じて、MCP Appsの概要理解 → ゼロからの新規開発 → 既存アプリの変換、と段階的に試してきました。MCP Appsは「AIチャットの中にインタラクティブなUIを埋め込む」という仕組みですが、開発体験としてはWeb開発そのものです。既存のWebフロントエンドの知識がそのまま活かせるので、Web開発者にとっては非常に入りやすい技術だと感じました。

以上、どなたかの参考になれば幸いです。

参考

https://modelcontextprotocol.io/extensions/apps

https://modelcontextprotocol.io/extensions/apps/build

https://github.com/modelcontextprotocol/ext-apps

この記事をシェアする

FacebookHatena blogX

関連記事