
公式の`convert-web-app` スキルを使って既存のWebアプリをMCP Appに対応させてみた
リテールアプリ共創部のるおんです。
これまでMCP Appsについて、概要と仕組みの解説、create-mcp-app スキルを使ったゼロからの開発と2本の記事を書いてきました。
今回は 既存のWebアプリをMCP Appに対応させる という、より実践的なシナリオに挑戦してみました。使用したのは、MCP Apps公式リポジトリで提供されている convert-web-app スキルです。
既存のECショップWebアプリ(React + Vite + shadcn/ui)を、スタンドアロンでもMCP Appとしても動作する ハイブリッドアプリ に変換し、Claude上で商品一覧がインタラクティブに表示されるところまでを試してみました。
「ECショップを見せてほしい」と言うプロンプトに対してECサイトがMCP Appとして表示されています。

前提:今回変換するWebアプリ
今回MCP App化するのは、React + Vite + TypeScript で構築されたシンプルなECショップWebアプリです。UIコンポーネントにはshadcn/uiを使用しています。
主な機能は以下の通りです。
- 商品一覧表示(カテゴリフィルター付き)
- ショッピングカート(追加・削除・数量変更)
- shadcn/uiの
Card,Button,Badge,Sheetを使用
商品データはフロントエンド側にハードコードされており、外部APIは使用していません。画像はUnsplash(外部CDN)から取得しています。

この一般的な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が以下のステップを自動で進めてくれます。
- 既存アプリの分析 - データソース、外部依存、ビルドシステム、ユーザー操作の調査
- CSP要件の調査 - 外部画像(Unsplash)へのアクセスに必要なCSP設定の特定
- MCPサーバーの作成 - ツールとリソースの登録
- ビルドパイプラインの更新 - シングルファイルバンドル対応
- MCP App初期化の追加 - ハイブリッドエントリーポイントの作成
- ホストスタイリングの統合 - 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ツールを登録しています。
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設定も行っています。
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 フックでホストとの接続を確立し、ツール引数からカテゴリ情報を受け取ります。
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.ontoolresultでstructuredContentからカテゴリ情報を取得app.onhostcontextchangedでホストのテーマ変更やsafeAreaInsetsに対応- 共通の
AppコンポーネントにinitialCategoryを渡すことで、MCP Appでもスタンドアロンでも同じUIを表示
既存Appコンポーネントの変更
元の App.tsx への変更は最小限です。initialCategory propsを追加しただけです。
+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ビルドを切り替えています。
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.html→vite-plugin-singlefileでHTML/CSS/JSを1ファイルにバンドル(MCP App用)
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でテストしてみた
ビルドとサーバー起動
npm run build:mcp
npx tsx main.ts
http://localhost:3002/mcp でMCPサーバーが起動します。
cloudflaredでトンネルを作成
Claude(Web版/Desktop版)からローカルサーバーにアクセスするには、インターネットに公開する必要があります。cloudflared を使ってトンネルを作成します。
npx cloudflared tunnel --url http://localhost:3002
実行すると、https://random-name.trycloudflare.com のようなURLが生成されます。
Claudeにカスタムコネクターを追加
- claude.ai または Claude Desktop を開く
- プロフィールアイコンをクリック → Settings → Connectors
- Add custom connector をクリック
- URLに
https://生成されたURL/mcpを入力 - 保存
動かしてみる
Claudeに「ECショップを見せてほしい」と入力すると、show-ec-shop ツールが呼び出され、会話内にECショップのUIが表示されました。

商品カード、カテゴリフィルター、カートなど、スタンドアロンで動いていたすべての機能が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開発者にとっては非常に入りやすい技術だと感じました。
以上、どなたかの参考になれば幸いです。
参考










