【MCP入門】MCPを使ってGitHub CopilotからImageFlux Live Streamingに配信してみた

こんにちは、テリーです。最近MCPというワードがAI業界でホットです。AIの活用が世界中で広がる中、複数のAIを組み合わせて使用するニーズが高まっています。そのための仕組みで提案されたものが急成長中のMCPです。AI業界大手が軒並みMCP対応を進めているので、デファクトスタンダードとなるのは間違いなしです。

今回はMCPを使ってImageFlux Live StreamingのAPIを呼び、ライブ配信とHLS視聴をチャット文章一発で実現するサンプルを紹介します。

動作確認環境

  • macOS Sequoia 15.5
  • VSCode 1.100.2
  • GitHub Copilot extension 1.322.0
  • GitHub Copilot Chat 0.27.1
  • MCP TypeScript SDK 1.11.3
  • WebRTC Native Client Momo 2024.1.1

MCPは現在進行形で開発が進められているため、バージョンごとに挙動がまったく変わってくることが想定されます。適宜読み替えてください。

MCPとは?

MCP(Model Context Protocol)は、アプリケーションと大規模言語モデル(LLM)が「コンテキスト」をやり取りするための、オープンかつ標準化されたプロトコルです。ここでいう「コンテキスト」とは、AIとの対話における持続的な文脈情報のことを指します。簡単に言えば、「少し前の会話で出てきた内容」や「ユーザーの目的・スタイル・前提条件」など、モデルの応答に影響を与える背景情報です。

MCPに対応したさまざまなクライアント、サーバ、API、リソースは、この文脈情報を共通の形式でやり取りし、AIの出力に一貫性やパーソナライズ性を持たせることができます。

MCPと従来のWeb APIの主な違いは、以下の2点です。

  • ステート管理:Web APIは基本的に「ステートレス」で、毎回必要な情報を明示的に送る必要があります。一方、MCPは「ステートフル」で、コンテキストが継続的に保持・活用されます。
  • 通信の方向性:Web APIは主にクライアントからサーバへのリクエスト主導ですが、MCPではクライアントとサーバ間での非同期かつ双方向でのコンテキスト共有が可能です。

詳しくは下記のサイトをご覧ください。
https://modelcontextprotocol.io/introduction

GitHub Copilotのエージェントモード

GitHub Copilotのコード補完を使ったことがあってもエージェントモードを使ったことがない方はまだ多いかもしれません。ごく最近のアップデートでMCPに対応しました。

GitHub Copilot Chatのパネルを開き、右下の「Ask」を「Agent」に切り替えます。「Ask」はコードに関しての質問に答えて修正案を提示するチャットボットですが、「Agent」は指示したタスクを可能な限り実行するモードです。日本語で指示を出せるシェルスクリプトとも言えるかもしれません。

Hello Worldの作成

MCPがどのように動作するかをつかむために、実際にMCPサーバを作成します。まずは最も簡単なMCPサーバとして、"Hello MCP World"と応答するものを作ります。

ターミナルで下記のようにフォルダを作成し、ライブラリをインストールします。

mkdir testmcp
cd testmcp
npm init -y
npm i @modelcontextprotocol/sdk
mkdir .vscode
touch .vscode/mcp.json
touch testmcp.mjs
code .

.vscode/mcp.json という新たな設定ファイルが最近定義されました。このファイルがあるプロジェクトをVSCodeで開くと、GitHub CopilotがMCPサーバの候補として自動的に読み込まれます。チャットですぐに使えます。.vscode/mcp.jsonをvscodeで開き(もしくは作成し)、下記のコードをペーストします。

{
  "servers": {
    "testmcp": {
      "command": "node",
      "args": [
        "${workspaceFolder}/testmcp.mjs"
      ]
    }
  }
}

実行中は下図のように、「Running | Stop | Restart | 1 tools」と表示されます。実行前は「Start | 1 cached tools」と表示されます。マウスカーソルを合わせてクリックすると、MCPサーバを起動、停止、再起動を気軽に実行できます。カレントディレクトリがプロジェクトルートとは限らないのでファイル名は絶対パスで書く必要があります。

次にMCPサーバのプログラム本体です。ファイル名は自由ですが、ここではtestmcp.mjsとしています。testmcp.mjsをvscodeで開き、下記のコードをペーストします。

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

const server = new McpServer({
  name: "Test MCP",
  version: "1.0.0"
});

server.tool("hellofunction",
  "call this tool when you want to test the function",
  async () => ({
    content: [{ type: "text", text: "Hello MCP world" }]
  })
);

const transport = new StdioServerTransport();
await server.connect(transport);

GitHub Copilot Chatを開き、下記のように入力し、エンターを押します。

hellofunctionを呼んで

MCPサーバが起動され、toolの呼び出しをして良いか確認のメッセージが出ます。ここで青いボタン「Continue」を押すとtoolの関数が実行されますが、次回同じtoolを呼び出した時も再度確認ボタンが現れます。「Allow in this Session」等を押すと「このtoolは確認なく実行して良い」と記憶され、次回から即座にtoolの関数が実行されます。

Continueを押した後、関数が実行され、文字列がチャットボットに返され、表示されました。

ログ出力

プログラムを書いて思い通りに動作しない時、ログを出したくなります。MCPサーバはブラウザアプリと違って標準入出力を使用するため、console.logが使用できません。console.errorを使用するか、sendLoggingMessage関数を使用します。console.logを使用すると、toolの戻り値と混同されエラーが出ることがあります。

testmcp.mjsを下記のように上書きし、再読み込みします。

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

const server = new McpServer({
  name: "Test MCP",
  version: "1.0.0"
}, {
  capabilities: {
    logging: {},
  }
});

server.tool("hellofunction",
  "call this tool when you want to test the function",
  async () => {
    console.error("error message");
    console.log("log message");
    server.server.sendLoggingMessage({
      level: "error",
      data: `sendLoggingMessage error`,
    });
    server.server.sendLoggingMessage({
      level: "info",
      data: `sendLoggingMessage info`,
    });
    return ({
      content: [{ type: "text", text: "Hello MCP world" }]
    })
  }
);

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}
main().catch((error) => {
  console.error("MCP Server error:", error);
  process.exit(1);
});

ログの出力はVSCodeの「OUTPUT」タブです。本記事執筆時点では、MCPのログは選択肢に出ないことがあります。

testmcp.mjsの末尾等に下記の1行を追加し、あえてエラーを発生させます。

a=1

するとチャット入力欄に赤いボタンが現れますので、クリックします。

「Show Output」をクリックします。

「MCP: testmcp」という選択肢が現れ、ログが表示されるようになります。console.logの行で警告が出ていることがわかります。

外部API呼び出しとAPIキーの保存

MCPサーバが実行できるようになったので、外部のAPIを呼んでみます。ここから先は一般的なNode.jsのアプリケーションと同じです。fetchしてパースしてリターンです。

外部のAPIを呼ぶ際、APIキーが必要になることが多くあります。チャットで毎回APIキーをペーストするのは面倒ですが、かといってコードに埋め込むこともできません。そのための仕組みがあります。

.vscode/mcp.jsonを下記のように書き換えます。inputsとenvが増えています。inputsはMCPサーバが起動した直後に入力フォームが表示され、vscodeがキャッシュしてくれます。envはその値を環境変数としてプログラムに渡します。

{
  "inputs": [
    {
      "type": "promptString",
      "id": "imageflux-api-key",
      "description": "Imageflux API Key",
      "password": true
    }
  ],
  "servers": {
    "testmcp": {
      "command": "node",
      "args": [
        "${workspaceFolder}/testmcp.mjs"
      ],
      "env": {
        "IMAGEFLUX_API_KEY": "${input:imageflux-api-key}"
      }
    }
  }
}

testmcp.mjsを下記のように書き換えます。

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

const server = new McpServer({
  name: "Test MCP",
  version: "1.0.0"
}, {
  capabilities: {
    logging: {},
  }
});

server.tool("CreateChannel",
  "call this tool when you want to get channelid for ImageFlux Live Streaming",
  async () => {
    console.error("called CreateChannel APIKEY=", process.env.IMAGEFLUX_API_KEY);
    return ({
      content: [{ type: "text", text: "Hello MCP world" }]
    })
  }
);

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}
main().catch((error) => {
  console.error("MCP Server error:", error);
  process.exit(1);
});

MCPを再読み込みして実行すると、VSCode画面上部に入力フォームが現れます。ここでは「testpassword」と入力してみます。

ログに「testpassword」と出力されます。

間違った値を入れた場合は「= 」のところにマウスカーソルを合わせると「Edit | Clear | Clear All」のリンクが現れます。Editを押し、正しいAPIキーを入力します。

ImageFlux Live StreamingのAPI呼び出しを記述します。ここではCreateMultistreamChannelWithHLS APIを呼び出しています。

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

const server = new McpServer({
  name: "Test MCP",
  version: "1.0.0"
}, {
  capabilities: {
    logging: {},
  }
});

server.tool("CreateChannel",
  "call this tool when you want to get channelid for ImageFlux Live Streaming",
  async () => {
    console.error("called CreateChannel APIKEY=", process.env.IMAGEFLUX_API_KEY);
    const response = await fetch(IMAGEFLUX_API_ENDPOINT, {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer ' + process.env.IMAGEFLUX_API_KEY,
        'X-Sora-Target': 'ImageFlux_20200316.CreateMultistreamChannelWithHLS',
      },
      body: JSON.stringify({
        hls: [
          {
            durationSeconds: 2,
            startTimeOffset: -2,
            video: {
              width: 640,
              height: 480,
              fps: 30,
              bps: 1000000,
            },
            audio: {
              bps: 96000,
            },
          },
        ],
      }),
    })
    const data = await response.text();
    console.error("response=", data);
    return ({
      content: [{ type: "text", text: data }]
    })
  }
);

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}
main().catch((error) => {
  console.error("MCP Server error:", error);
  process.exit(1);
});

GitHub Copilotのチャット欄に「CreateChannelを呼んで」とタイプして実行すると、channel_id, sora_urlが取得できます。

日本語で質問すると値が返ってきます。エージェントが認識していることを確認できます。

サンプル

最後にサンプルプログラムです。Channel作成、Momo(後述)を起動、Playlist URLを取得、HLSを再生、Momoを終了、の機能をtoolで定義しています。

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { spawn } from "child_process";

const server = new McpServer({
  name: "Test MCP",
  version: "1.0.0"
}, {
  capabilities: {
    logging: {},
  }
});

server.tool("CreateChannel",
  "call this tool when you want to get channelid for ImageFlux Live Streaming",
  async () => {
    console.error("called CreateChannel APIKEY=", process.env.IMAGEFLUX_API_KEY);
    const response = await fetch(IMAGEFLUX_API_ENDPOINT, {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer ' + process.env.IMAGEFLUX_API_KEY,
        'X-Sora-Target': 'ImageFlux_20200316.CreateMultistreamChannelWithHLS',
      },
      body: JSON.stringify({
        hls: [
          {
            durationSeconds: 2,
            startTimeOffset: -2,
            video: {
              width: 640,
              height: 480,
              fps: 30,
              bps: 1000000,
            },
            audio: {
              bps: 96000,
            },
          },
        ],
      }),
    })
    const data = await response.text();
    console.error("response=", data);
    return ({
      content: [{ type: "text", text: data }]
    })
  }
);

server.tool("GetPlaylist",
  "call this tool when you want to get hls playlist url for ImageFlux Live Streaming",
  { channel_id: z.string() },
  async ({ channel_id }) => {
    console.error("called GetPlaylist APIKEY=", process.env.IMAGEFLUX_API_KEY);
    const response = await fetch(IMAGEFLUX_API_ENDPOINT, {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer ' + process.env.IMAGEFLUX_API_KEY,
        'X-Sora-Target': 'ImageFlux_20200207.ListPlaylistURLs',
      },
      body: JSON.stringify({ channel_id }),
    })
    const data = await response.text();
    console.error("response=", data);
    return ({
      content: [{ type: "text", text: data }]
    })
  }
);

server.tool("StartMomo",
  "call this tool when you want to execute Momo with the given channel id and sora url",
  { channel_id: z.string(), sora_url: z.string() },
  async ({ channel_id, sora_url }) => {
    console.error("called StartMomo channel_id=", channel_id, "sora_url=", sora_url);
    const args = ["--no-audio-device", "sora", "--signaling-urls", sora_url, "--channel-id", channel_id,
      "--video-codec-type", "VP8", "--video-bit-rate", "500", "--audio", "false", "--role", "sendonly"]
    const child = spawn("momo", args);
    return ({
      content: [{ type: "text", text: "ok" }]
    })
  }
);

server.tool("StopMomo",
  "call this tool when you want to stop Momo",
  async () => {
    spawn("pkill", ["momo"]);
    spawn("pkill", ["ffplay"]);
    return ({
      content: [{ type: "text", text: "ok" }]
    })
  }
);

server.tool("PlayHLS",
  "call this tool when you want to play hls using ffplay",
  { playlist_url: z.string() },
  async ({ playlist_url }) => {
    console.error("called PlayHLS playlist_url=", playlist_url);
    const child = spawn("ffplay", [playlist_url]);
    return ({
      content: [{ type: "text", text: "ok" }]
    })
  }
);

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}
main().catch((error) => {
  console.error("MCP Server error:", error);
  process.exit(1);
});

HLSの再生は簡略化のためffplayを使用しています。WebRTCの配信は外部コマンドのmomoを使用しています。それぞれパスを通します。

このMCPを再読み込みした後、GitHub Copilotに下記のように入力して、一連の処理を依頼します。

createchannelで新規に作成したchannelを使ってmomoを起動し、playlistを取得してhlsを再生して。30秒後にmomoを終了

30秒のスリープも勝手に入れてくれました。

Momoについて

ブラウザ外からWebRTCの配信を行うためのネイティブプログラムです。ソースコードとバイナリはこちらにあります。macOS向けのバイナリファイルはGitHubからダウンロードしただけでは実行できません。下記のコマンドを実行して、バイナリのフラグを変更します。その後パスの通っている場所(/usr/local/binなど)に移動してください。

xattr -d com.apple.quarantine ./momo

まとめ

MCPを使ってImageFlux Live StreamingのAPIを呼び、その値を使用してライブ配信するサンプルを紹介しました。一度MCPを作ると、さまざまなLLMアプリケーション、チャットアプリケーションから外部APIを気軽に自然言語で呼び出すことができます。おそらく他のMCPクライアントからでも呼び出せるでしょう。ブラウザでは難しい、メモリもディスクも大量に使うAI配信もローカルならば気軽に実現できそうです。ぜひ挑戦してみてください。より詳しい実装方法に興味がある方はお気軽にご相談ください。