Knowledge

MCP Apps × Vonage で、多言語カスタマーサポートをサポートする MCPサーバーを作ってみた

公開日:2026/03/04
技術ノウハウ・Tips勉強会・社内ナレッジ共有

はじめに

先日、弊社主催のエンジニア交流会の中で、株式会社KDDIウェブコミュニケーションズ さま(以下、KWC さま)にオフラインハンズオン会を開催していただきました。

エンジニア交流会については、弊社ニュース記事をご覧ください。

第1回エンジニア交流会を開催しました ― Vonageハンズオン×エンジニア同士の対話で広がる学びの場

Vonage という CPaaS の Voice API と OpenAI の Realtime API を組み合わせて「チャッピー電話」(AI ボイスアシスタント)を作るという内容で、電話番号の取得から WebSocket 音声ストリーミングまで、CPaaS の世界を実際に手を動かして体験できました。KWC さま、素晴らしいハンズオン会をありがとうございました。
詳細につきましては、ハンズオン会を主導していただいた「□い(しかくい)芸人」こと高橋さまの記事をご覧ください。

チャッピー電話を作ろう!

ハンズオン会で「Vonage、数行のコードで SMS も電話もできて面白いな」と感じました。 それとは別に、私は MCP Apps にも注目していました。MCP Apps は、AI チャット画面の中にインタラクティブな UI を埋め込める仕組みです。

この 2つを組み合わせて何か作ってみようと考え、思い至ったのが多言語カスタマーサポートをサポートする MCPサーバーです。

完成物

まずは動くものをお見せします。

自然言語の指示だけでチケットのトリアージ → 詳細確認 → 多言語返信ドラフト生成 → SMS送信 → マネージャーへのエスカレーション通話まで、一連のカスタマーサポート業務を完結させるデモ。UIはすべて MCP Apps のサンドボックス iframe内にインタラクティブに表示されています。

主な機能

ツール できること
triage_dashboard
多言語チケットを緊急度スコアで自動分類し、ダッシュボード表示
view_ticket
スペイン語・ポルトガル語・韓国語のチケットを OpenAI で日本語翻訳
draft_reply

顧客の言語で返信ドラフトを AI 生成し、バックトランスレーションで内容確認
send_customer_notify

Vonage SMS / WhatsApp で顧客にワンクリック通知
escalate_call

Vonage Voice API でマネージャーへ自動架電(TTS)
analytics_overview

言語別・チャネル別・優先度別の統計ダッシュボード

ソースコード全体はこちらのリポジトリで公開しています。

multilingual-cs-mcp-app

警告ブロック
⚠️
検証用のコードです。プロダクションレベルではありませんので、ご利用は計画的に。

前提知識

この記事で登場する 3 つの主要技術を簡単に紹介します。

MCP(Model Context Protocol)とは

What is the Model Context Protocol (MCP)?

MCP は、LLM に外部ツールを接続するためのオープンプロトコルです。

Anthropic が 2024 年末に公開したもので、「LLM に道具を使う手を与える」ための仕組みです。MCP Server がツール(関数)を公開し、MCP Host(Claude Desktop など)を介して LLM がそれを呼び出せます。

MCP Appsとは

MCP Apps-Model Context Protocol

GitHub ext-apps/specification at main・modelcontextprotocol/ext-apps/tree/main/specification

通常の MCP ツールはテキストを返します。LLM がそれを読んで要約し、ユーザーに伝えます。

MCP Apps はこれを拡張し、ツールの結果としてインタラクティブな HTML UI を返せるようにします。返された HTML は MCP Host 内の sandboxed iframe に描画され、ユーザーはボタンクリックやフォーム操作で直接サーバーとやり取りできます。

警告ブロック
⚠️
MCP Apps は 2026 年 1 月に正式な MCP extensionとしてリリースされましたが、仕様はまだまだ変わり得ると考えています。 最新の状況は上述の公式ドキュメントや仕様をご確認ください。

Vonage CPaaS とは

Vonage API Developer

Vonage は SMS、WhatsApp、音声通話などを API で操作できる CPaaS(Communications Platform as a Service)です。
KWC さまのハンズオンでは Voice API + WebSocket を使いました。
今回は以下の API を使います。

Vonage API 用途
Messages API (SMS) 顧客への通知送信
Messages API (WhatsApp) WhatsApp 経由の通知送信
Voice API (TTS) 緊急エスカレーション時の自動架電

アーキテクチャ

全体像

MCP Host とサーバーの間は stdio(Claude Desktop 向け)または HTTP Streamable(ブラウザテスト向け)で接続します。サーバーは外部 API として OpenAI(翻訳)と Vonage(通信)を利用し、チケットや顧客データはモックで構築しています。

通信フロー

例として、ユーザーが「未対応チケットの状況を見せて」と言ってからの流れを示します。

  1. ユーザー(または LLM) → MCP Host: ツール呼び出し triage_dashboard
  2. MCP Host → MCP Server : tools/call JSON-RPC リクエスト
  3. MCP Server: モックDBからチケット取得 → スコアリング → content(テキスト)とstructuredContent(JSON)を返却
  4. MCP Host:_meta.ui.resourceUriに対応する HTML リソースを取得し、sandboxed iframe に描画
  5. MCP Host → iframe:structuredContentpostMessageで送信
  6. iframe: データを受信して UI を描画

ポイントは ステップ 3 です。ツールは 2 種類のレスポンスを返します。

  • contentテキストフォールバック。UI を表示できないホスト向け
  • structuredContent 構造化データ。iframe 内の JS が受け取って UI を描画する用

※ iframe に渡す HTML は CSPで外部リソースの読み込みを許可する構成も取れますが、今回はvite-plugin-singlefile で CSS/JSをインライン化し、設定をシンプルにしています。

MCP Apps の実装パターン

MCP Apps では UI ResourceTool をペアで登録します。

サーバー側: Tool + UI Resource のペアリング

// server.ts
import {
  registerAppResource,
  registerAppTool,
  RESOURCE_MIME_TYPE,  // = "text/html;profile=mcp-app"
} from "@modelcontextprotocol/ext-apps/server";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

export function createServer(): McpServer {
  const server = new McpServer({
    name: "Multilingual CS Support",
    version: "1.0.0",
  });

  // ① UI Resource を登録
  const resourceUri = "ui://cs-app/ticket-view.html";
  registerAppResource(
    server,
    resourceUri,   // name(表示名。なんでもいいが、今回はURI をそのまま使用)
    resourceUri,   // uri(リソースの識別子)
    { mimeType: RESOURCE_MIME_TYPE },
    async () => ({
      contents: [{
        uri: resourceUri,
        mimeType: RESOURCE_MIME_TYPE,
        text: await fs.readFile("dist/views/ticket-view.html", "utf-8"),
      }],
    })
  );

  // ② Tool を登録(_meta.ui で UI Resource とリンク)
  registerAppTool(
    server,
    "view_ticket",
    {
      title: "View Ticket",
      description: "Display ticket details with translation",
      inputSchema: {
        ticketId: z.string().describe("Ticket ID (e.g. TK-1001)"),
      },
      _meta: {
        ui: {
          resourceUri,                   // ← UI Resource への参照
          visibility: ["model", "app"],  // LLM と UI の両方から使える
        },
      },
    },
    handleViewTicket  // ハンドラ関数
  );

  return server;
}

registerAppResource で HTML を登録し、registerAppTool _meta.ui.resourceUri でそれを参照する。このペアリングが MCP Apps の基本パターンです。

registerAppResource の引数なんか冗長じゃね?」と思い調べてみると、これはMCPプロトコルのリソース登録リソース読み取りが別の操作として設計されていることに由来していそうということで腹落ちしました。まだまだ登場したてですし、今後インターフェースも洗練されていくことでしょう。

  • リソース登録( resources/list で返すデータ)

    外側の引数: name, uri, { mineType } ホストが「このサーバーにはどんなリソースがある?」と問い合わせたときに返すカタログ情報です。ホストはこの mimeType を見て「MCP App の HTMLだ」と判断し、iframe の準備を始めるようです。

  • リソース読み取り(resource/readで返すコンテンツ)
     コールバック部分:{ contents: [{ uri, mineType, text }] }
    ホストが「そのリソースの中身をくれ」とリクエストしたときの実際のレスポンスです。MCP の仕様上、各レスポンスが自己記述的(それ単体で uri と mimeType がわかる)である必要があるため、ここにも同じ値が入ります。

ツールハンドラ: content と structuredContent を返す

// src/tools/view-ticket.ts
export async function handleViewTicket(args: { ticketId: string }) {
  const ticket = getTicketById(args.ticketId);
  const customer = getCustomerById(ticket.customerId);

  // 日本語以外のチケットはOpenAI で翻訳
  const translation = ticket.language !== "ja"
    ? await translateTicket(ticket.id, ticket.body, ticket.language, "ja")
    : null;

  return {
    // テキストフォールバック(UI非対応ホスト向け)
    content: [{
      type: "text",
      text: `Ticket ${ticket.id}: ${ticket.subject}\\n${ticket.body}`,
    }],
    // 構造化データ(iframe の JS に渡される)
    structuredContent: {
      ticket,
      customer,
      translation,
    },
  };
}

View(iframe)側: データを受け取って UI 描画

// views/ticket-view.ts
import { App } from "@modelcontextprotocol/ext-apps";

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

// ① サーバーからの structuredContent を受け取って描画
app.ontoolresult = (result) => {
  const { ticket, customer, translation } = result.structuredContent;
  renderTicketView(ticket, customer, translation);
};

// ② UI のボタンにサーバーのツール呼び出しを登録
document.getElementById("btn-reply").addEventListener("click", async () => {
  await app.callServerTool({
    name: "draft_reply",
    arguments: { ticketId: currentTicket.id },
  });
});

// ③ チャットにメッセージを送る(LLM に情報を渡す)イベントを登録
document.getElementById("btn-escalate").addEventListener("click", async () => {
  await app.sendMessage({
    role: "user",
    content: [{
      type: "text",
      text: `チケット ${currentTicket.id} をエスカレーションしてください`,
    }],
  });
});

// ④ テーマ(light/dark)対応
app.onhostcontextchanged = (ctx) => {
  if (ctx.theme) {
    document.documentElement.setAttribute("data-theme", ctx.theme);
  }
};

// ⑤ 接続開始(ハンドラ登録後に呼ぶ)
app.connect();
メソッド 方向 用途
app.ontoolresult
 Server → View  ツール結果(データ)を受信して UI 描画
app.callServerTool()
 View → Server  UI 操作からサーバーのツールを呼び出す
app.sendMessage()  View → Chat  チャットにメッセージを送り、LLM に判断を委ねる

ホスト環境への適応(onhostcontextchanged

ホストはテーマ切り替えやウィンドウリサイズなど、環境が変化するたびにこのコールバックを呼び出します。

app.onhostcontextchanged = (ctx) => {
  if (ctx.theme) {
    document.documentElement.setAttribute("data-theme", ctx.theme);
  }
};

今回はtheme"light" / "dark")のみ使用していますが、コールバックにはホストの詳細な情報が渡されます。

 プロパティ  内容  活用例
theme
"light" / "dark"
 ダークモード対応
 styles.variables  ホストの CSS 変数  ホストの配色に合わせた UI
 locale / timeZone  "ja-JP" / "Asia/Tokyo"  日時のローカライズ
 platform  "web" / "desktop" / "mobile"  レスポンシブ対応
 containerDimensions  iframe のサイズ  レイアウト調整
 safeAreaInsets  モバイルのセーフエリア  ノッチ対応

多言語カスタマーサポートのようなアプリではlocaleplatformも活用の余地がありそうです。

主要機能の実装解説

6 つのツールの中から外部APIを呼び出している 2つを取り上げます。

1. 翻訳(view_ticket + OpenAI)

スペイン語やポルトガル語で書かれたチケットを、OpenAI gpt-4o-mini でリアルタイムに日本語翻訳します。

// src/services/translator.ts
export async function translateTicket(
  ticketId: string,
  originalText: string,
  originalLanguage: string,
  targetLang: string = "ja"
): Promise<TranslationResult> {
  // 日本語訳と英語バックトランスレーションを並列で実行
  const [translated, backTranslation] = await Promise.all([
    chat(
      `Translate from ${langName(originalLanguage)} to ${langName(targetLang)}. Return ONLY the translation.`,
      originalText
    ),
    chat(
      `Translate from ${langName(originalLanguage)} to English. Return ONLY the translation.`,
      originalText
    ),
  ]);

  return {
    original: originalText,
    originalLanguage,
    translated,
    targetLanguage: targetLang,
    backTranslation,
  };
}
src/services/translator.tsで OpenAI gpt-4o-mini を呼び出しています。

2. Vonage SMS / Voice 連携(send_customer_notify / escalate_call)

UI のボタンを押すと、iframe からapp.callServerTool()でサーバーのツールが呼ばれ、サーバーが Vonage API を叩いて実際に SMS送信 / 架電が行われます。

SMS 送信:

// src/vonage/sms.ts
export async function sendSms(params: SendSmsParams): Promise<SendSmsResult> {
  if (process.env.VONAGE_MOCK === "true") {
    return { success: true, messageUuid: `mock-${Date.now()}` };
  }

  const resp = await vonage.messages.send({
    messageType: "text",
    text: params.text,
    to: params.to,                              // "34612345678"
    from: params.from ?? process.env.VONAGE_SMS_FROM!,
    channel: "sms",
  });
  return { success: true, messageUuid: resp.messageUUID };
}

Voice API(TTS 自動架電):

// src/vonage/voice.ts
export async function makeEscalationCall(params: MakeCallParams): Promise<MakeCallResult> {
  if (process.env.VONAGE_MOCK === "true") {
    return { success: true, callUuid: `mock-call-${Date.now()}` };
  }

  const resp = await vonage.voice.createOutboundCall({
    to: [{ type: "phone", number: params.to }],
    from: { type: "phone", number: process.env.VONAGE_SMS_FROM! },
    ncco: [{
      action: "talk",
      text: params.message,     // TTS で読み上げる内容
      language: params.language ?? "ja-JP",
      style: 0,
      loop: 2,                  // 2 回繰り返し
    }],
  });
  return { success: true, callUuid: resp.uuid };
}

Vonage の Voice API は NCCO(Nexmo Call Control Objects)という JSON で通話の動作を定義します。 例えば、action: "talk" で TTS(Text To Speech)、language: "ja-JP"で日本語音声を指定しています。

俺、バカだからよくわかんねぇけどよ…

私↓

これ、普通にWebアプリケーション作ったらよくね?という疑問が浮かんできました。どう思う?

AI↓

「普通に業務アプリケーション作ればよくない?」と思ったということですね。
たしかに、今の時点ではそうかもしれません。

ただ、私が見ている景色は少し違います。

いま私たちは「PCの前に座り、画面を見つめ、マウスとキーボードで操作する」ことを当たり前だと思っています。でも、それは本当にこの先もずっと続く前提でしょうか。

たとえば——ウェアラブルデバイス越しにAIエージェントへ話しかける。すると目の前の空間にトリアージダッシュボードが浮かび上がる。気になるチケットをタップすれば詳細が展開され、「この顧客に返信して」と一言添えるだけで、相手の言語に合わせたドラフトが生成される。PCの前に張り付く必要はなく、移動中でも、現場にいても、業務はシームレスに進んでいく——そんな未来は、もうSFの話ではないはずです。

人間とデジタルの接点は、スマートフォン、音声アシスタント、ARグラス、空間コンピューティングと、急速に多様化しています。そのとき求められるのは、特定のデバイスや画面サイズに縛られた固定のUIではなく、文脈に応じて動的に生成されるインターフェースです。

MCP Apps が示しているのは、まさにその可能性の入口だと思っています。AIが対話の文脈を理解し、必要なUIをその場で組み立てて提示する。デバイスが変わっても、インタラクションの本質は変わらない。そういう世界の土台になり得る技術が、いま手の届くところにある。
それだけで、試してみる価値は十分にあるのではないでしょうか。

私↓

・・・ふむ。まあ許したるわ

という感じで論破されました。俺バカ構文にマジレスするとは可愛くないですね。
でもたしかに納得感はありました。人間とデジタルのインターフェースが従来よりも柔軟になっていきそうな未来は感じられました。

まとめ

KWC さんのハンズオン会で Vonage に触れたことをきっかけに、MCP Apps と組み合わせて多言語カスタマーサポートダッシュボードを作りました。

MCP Apps は、テキストしか返せなかった AI ツールに「目と手」を与えます。ダッシュボード、フォーム、ボタン――これまで別のアプリケーションとして作る必要があったものが、AI チャット画面の中に自然に統合される体験は新鮮でした。 ツールの結果をリッチな UI で表現するというアイデアには大きな可能性を感じます。今後の成長が楽しみです。

Vonage CPaaS は、SMS も WhatsApp も音声通話も、わずかな API コールで実現できます。今後も、コミュニケーションが様々なインターフェースに自然に統合されていくことが期待できました。

あなたに最適な解決策を一緒に考えます。

Contact お問い合わせ

状況に応じた最適なご提案で、お客様の課題解決をサポートいたします