MCP Apps とは
MCP AppsはClaudeやChatGPTの様な生成AIのチャット画面に、Webページの様なインタラクティブなUIを提供する技術です。
今までもOpenAI Apps SDKの様に、LLMベンダーのアプリに特化したものはありましたが、それがOpen AIに限らず標準化された仕様となっています。
今回は公式のQuick Startを参考にMCP Appsを作成しVS CodeのCopilot Chatで呼び出してみます。
実行した時のイメージは以下の通り。
実装したソースコード
公式のQuick Start
プロジェクト設定
はじめに必要ライブラリのインストールと設定ファイルを作成。
mkdir sample-mcp-app && cd sample-mcp-app
依存パッケージをインストール
npm init -y
npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk express cors
npm install -D typescript vite vite-plugin-singlefile @types/express @types/cors @types/node tsx concurrently cross-env
packaget.jsonに設定を追加
npm pkg set type=module
npm pkg set scripts.build="tsc --noEmit && tsc -p tsconfig.server.json && cross-env INPUT=mcp-app.html vite build"
npm pkg set scripts.start="concurrently 'cross-env NODE_ENV=development INPUT=mcp-app.html vite build --watch' 'tsx watch main.ts'"
tsconfig.jsonを作成します。
{
"compilerOptions": {
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"strict": true,
"skipLibCheck": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src", "server.ts", "main.ts"]
}
サーバー側の設定としてtsconfig.server.jsonを作成
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"declaration": true,
"emitDeclarationOnly": true,
"outDir": "./dist",
"rootDir": ".",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["server.ts", "main.ts"]
}
最後にviteの設定を行う。
MCP AppsではUIを1つのHTMLファイルにまとめて返却する必要があるため、viteを利用して一つのHTMLファイルにする必要がある(参考)
import { defineConfig } from "vite";
import { viteSingleFile } from "vite-plugin-singlefile";
const INPUT = process.env.INPUT;
if (!INPUT) {
throw new Error("INPUT environment variable is not set");
}
const isDevelopment = process.env.NODE_ENV === "development";
export default defineConfig({
plugins: [viteSingleFile()],
build: {
sourcemap: isDevelopment ? "inline" : undefined,
cssMinify: !isDevelopment,
minify: !isDevelopment,
rollupOptions: {
input: INPUT,
},
outDir: "dist",
emptyOutDir: false,
},
});
この設定でpackage.jsonのbuildとstartに設定された以下のviteのビルドの際にdist/mcp-app.htmlとして出力される
INPUT=mcp-app.html vite build
MCPサーバーの開発
import {
registerAppResource,
registerAppTool,
RESOURCE_MIME_TYPE,
} from "@modelcontextprotocol/ext-apps/server";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import fs from "node:fs/promises";
import path from "node:path";
const DIST_DIR = path.join(import.meta.dirname, "dist");
/**
* Toolと表示するUI Resourceを登録するMCPサーバーを作成
*/
export function createServer(): McpServer {
const server = new McpServer({
name: "MCP App Server",
version: "1.0.0",
});
// Toolに表示するUI ResourceのURIを設定。ToolとResourceの両方で同じUIを設定する
const resourceUri = "ui://get-time/mcp-app.html";
// UIのResourceをToolの`meta.ui.resourceUri`に設定する
// Toolは呼び出されたとき`meta.ui.resourceUri`を参照し、UIをチャット上にレンダリングする。
registerAppTool(
server,
"get-time",
{
title: "Get Time",
description: "Returns the current server time.",
inputSchema: {},
_meta: { ui: { resourceUri } }, // Resourceの設定
},
async () => {
// 日本時間を返却するTool
const time = new Date().toLocaleString("ja-JP", { timeZone: "Asia/Tokyo" });
return { content: [{ type: "text", text: time }] };
},
);
// 表示するUIリソースを設定。viteでビルドされた mcp-app.htmlを
// チャット画面に表示されるResourceとして登録する
registerAppResource(
server,
resourceUri,
resourceUri,
{ mimeType: RESOURCE_MIME_TYPE },
async () => {
const html = await fs.readFile(path.join(DIST_DIR, "mcp-app.html"), "utf-8");
return {
contents: [
{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html },
],
};
},
);
return server;
}
次にMCPサーバをExpressを利用して作成する
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
/**
* Expressを利用してStreamable HTTPを利用したMCPサーバーを起動する
* リクエストごとに新しいMCP Serverインスタンスを作成する
*/
export async function startStreamableHTTPServer(
createServer: () => McpServer,
): Promise<void> {
const port = parseInt(process.env.PORT ?? "3001", 10);
// MCP用のExpressのアプリを作成。MPC用にJSON-RPC用のエントリーポイントを提供する
const app = createMcpExpressApp({ host: "0.0.0.0" });
app.use(cors());
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
const transport = new StreamableHTTPServerTransport({
// SessionIdを維持することで過去の会話の状態を維持できるが、
// 今回はシンプルなサービスでセッションを維持する必要がないのでステートレスな運用
sessionIdGenerator: undefined,
});
res.on("close", () => {
transport.close().catch(() => {});
server.close().catch(() => {});
});
try {
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
} catch (error) {
console.error("MCP error:", error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: "2.0",
error: { code: -32603, message: "Internal server error" },
id: null,
});
}
}
});
const httpServer = app.listen(port, (err) => {
if (err) {
console.error("Failed to start server:", err);
process.exit(1);
}
console.log(`MCP server listening on http://localhost:${port}/mcp`);
});
const shutdown = () => {
console.log("\nShutting down...");
httpServer.close(() => process.exit(0));
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
}
async function main() {
await startStreamableHTTPServer(createServer);
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
以上でサーバー側の開発は完了
チャット画面表示するUIを作成
チャットUI上に表示されるHTMLファイルを作成
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Get Time App</title>
</head>
<body>
<p>
<strong>サーバー時間:</strong> <code id="server-time">Loading...</code>
</p>
<button id="get-time-btn">現在のサーバー時間を取得</button>
<script type="module" src="/src/mcp-app.ts"></script>
</body>
</html>
次にMCPサーバーと接続するクライアントを作成する。
import { App } from "@modelcontextprotocol/ext-apps";
const serverTimeEl = document.getElementById("server-time")!;
const getTimeBtn = document.getElementById("get-time-btn")!;
// MCPサーバー側と接続するためのUIクライアントを作成する
const app = new App({ name: "Get Time App", version: "1.0.0" });
// `app.connect()`より前に設定する必要あり
// MCPサーバのツールの実行結果を受け取るイベント
// get-timeがresult.contentに現在時刻を設定して返却するので、その値を取得してserverTimeElに代入する
app.ontoolresult = (result) => {
const time = result.content?.find((c) => c.type === "text")?.text;
serverTimeEl.textContent = time ?? "[ERROR]";
};
// ボタンクリック時の挙動
getTimeBtn.addEventListener("click", async () => {
// `app.callServerTool()`で指定したToolを実行する
const result = await app.callServerTool({ name: "get-time", arguments: {} });
const time = result.content?.find((c) => c.type === "text")?.text;
serverTimeEl.textContent = time ?? "[ERROR]";
});
// ホストと接続
app.connect();
最終的に以下のフォルダ構成となっている。
sample-mcp-app/
├── main.ts
├── mcp-app.html
├── package.json
├── server.ts
├── src/
│ └── mcp-app.ts
├── tsconfig.json
├── tsconfig.server.json
└── vite.config.ts
ビルドを実行しエラー等が出なければOK
npm run build
最後にサーバーを起動すると、3001ポートにMCPのエンドポイントが起動する
npm start
> my-mcp-app@1.0.0 start
> concurrently 'cross-env NODE_ENV=development INPUT=mcp-app.html vite build --watch' 'tsx watch main.ts'
[0] vite v7.3.1 building client environment for production...
[0]
[0] watching for file changes...
[0]
[0] build started...
[0] transforming...
[1] Warning: Server is binding to 0.0.0.0 without DNS rebinding protection. Consider using the allowedHosts option to restrict allowed hosts, or use authentication to protect your server.
[1] MCP server listening on http://localhost:3001/mcp
[0] ✓ 142 modules transformed.
[0] rendering chunks...
[0] [plugin vite:singlefile]
[0]
[0] [plugin vite:singlefile] Inlining: mcp-app-BG8yD_ef.js
[0] computing gzip size...
[0] dist/mcp-app.html 2,127.43 kB │ gzip: 425.50 kB
[0] built in 867ms.
VS Codeから実行
記事執筆時点(2026/1/31)ではVS Codeで実行する場合は、insiders版でのみMCP Appsを利用可能です。以下のURLよりインストールしてください
VS Codeが今回作成したMCP Appsに接続できる様に設定を行う。
{
"servers": {
"my-mcp": {
"type": "http",
"url": "http://localhost:3001/mcp"
}
}
}
設定後VS Code上に「Runnning」が表示されるのでクリック
その後ツールの設定で作成したmy-appのMCPサーバーを選択し「OK」をクリック
その後チャット画面で「今何時?」と質問するとMCPサーバに接続されUIが表示される。
「現在のサーバー時間を取得」をクリックすることでUI上のサーバー時間が更新されていることが確認できる。


