Livebook で MCP サーバーを起動する【VSCode の Copilot チャットにずんだもんの声で喋ってもらう】

## はじめに

VSCode で MCP サーバーを使っていますか？

予め設定を加えておくことで、 GitHub Copilot が必要に応じて MCP サーバーで提供されている機能にアクセスし、各種 API などと連携して回答してくれるようになります

https://code.visualstudio.com/docs/copilot/chat/mcp-servers

```mermaid
sequenceDiagram
    participant User as 開発者
    participant VSCode as VSCode
    participant Copilot as GitHub Copilot
    participant MCP as MCPサーバー

    User->>VSCode: チャットプロンプトを入力
    VSCode->>Copilot: プロンプトを送信
    Copilot->>VSCode: 応答を生成
    Copilot->>MCP: 必要に応じてツールを呼び出し
    MCP-->>Copilot: ツールの実行結果を返す
    Copilot-->>VSCode: 応答とツールの結果を返す
    VSCode-->>User: 応答を表示
```

本記事では Elixir の Livebook 上で MCP サーバーを起動することで、 VSCode の Copilot チャットから VOICEVOX の音声合成を実行します

```mermaid
sequenceDiagram
    participant User as 開発者
    participant VSCode as VSCode
    participant Copilot as GitHub Copilot
    participant MCP as MCPサーバー（Livebook上）
    participant VOICEVOX as VOICEVOX API

    User->>VSCode: チャットプロンプトを入力
    VSCode->>Copilot: プロンプトを送信
    Copilot->>MCP: 音声合成リクエストを送信
    MCP->>VOICEVOX: テキストを送信して音声合成を実行
    VOICEVOX-->>MCP: 音声データを返却
    MCP->>MCP: 音声データをファイルとして保存
    MCP-->>Copilot: 音声ファイルのパスを返却
    Copilot->>VSCode: 音声ファイルを再生
    VSCode-->>User: 音声を再生
```

実装したノートブックはこちら

https://github.com/RyoWakabayashi/elixir-learning/blob/main/livebooks/mcp/voicevox_mcp_server.livemd

## 事前準備

### Livebook のインストール

公式サイトから Livebook のインストーラーを取得し、インストールします

https://livebook.dev/

### VSCode のインストール

公式サイトから VSCode のインストーラーを取得し、インストールします

https://code.visualstudio.com/

### GitHub Copilot の開始

GitHub にアカウントを作成しましょう

https://github.com/

アカウント作成後、右上のユーザーアイコンをクリックしてください

![スクリーンショット 2025-04-27 11.01.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/6a91bf07-73db-4492-901e-44557d378db7.png)

開いたメニューから "Your Copilot" をクリックします

![スクリーンショット 2025-04-27 11.00.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/ab20345c-9d8a-4e78-9af0-c0a84799d5c5.png)

開いた Copilot の設定画面で "Start using Copilot Free" をクリックします

![スクリーンショット 2025-04-27 10.59.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/d78c2734-8f39-490a-baeb-fb15a7c49220.png)

Copilot が Free プランで利用できるようになり、チャット画面が開きます

![スクリーンショット 2025-04-27 11.31.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/68e7bcaf-b499-4bcc-b817-7b074c2e0b79.png)

VSCode に Copilot 用の拡張機能をインストールしましょう

https://marketplace.visualstudio.com/items?itemName=GitHub.copilot

https://marketplace.visualstudio.com/items?itemName=GitHub.copilot-chat

インストール後、 VSCode の上部、検索ボックスの右側にある Copilot アイコン（バツマーク付き）をクリックします

![スクリーンショット 2025-04-27 11.37.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/cbcf277d-d805-460e-8ce9-0c895780595f.png)

VSCode から GitHub にサインインします

![スクリーンショット 2025-04-27 11.37.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/f83faf57-9e75-4288-8755-987256cb42e4.png)

サインインできると、 Copilot アイコンのバツマークが外れています

Copilot アイコンをクリックして開くドロップダウンから "Open Chat" をクリックしてください

![スクリーンショット 2025-04-27 11.40.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/618b5546-01fe-4c52-9347-cffcff802a09.png)

Copilot とチャットするための画面が開きました

![スクリーンショット 2025-04-27 11.41.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/5c90494e-7036-4a3c-8435-b8bf658fcf54.png)

公式のドキュメントはこちら

https://marketplace.visualstudio.com/items?itemName=GitHub.copilot

### VOICEVOX のインストール

公式サイトから VOICEVOX のインストーラーを取得し、インストールします

https://voicevox.hiroshiba.jp/

### Node.js のインストール

本記事では MCP サーバーの挙動を確認するための Inspector を利用します

https://github.com/modelcontextprotocol/inspector

その実行環境として Node.js が必要です

いずれかの方法で Node.js をインストールしてください

#### mise

https://github.com/jdx/mise

```bash
mise use node@latest
```

#### asdf

https://asdf-vm.com/

```bash
asdf plugin add nodejs
asdf install nodejs latest
```

#### Homebrew

https://brew.sh/

```bash
brew install node
```

#### 公式インストーラー

公式サイトからインストーラーを取得し、インストールします

https://nodejs.org/ja

## MCP サーバーの実装

### セットアップ

Livebook を起動し、ホーム画面右上の "+ New Notebook" をクリックします

![スクリーンショット 2025-04-27 11.48.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/99d2afe7-2276-49c8-b06b-77f5b194e8f5.png)

新しいノートブックが開きます

![スクリーンショット 2025-04-27 11.49.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/8b9d8cec-0d5d-4718-be2a-98d7f01fd32d.png)

セットアップセル（"Notebook dependencies and setup" と書いてある枠の中）をクリックし、以下のコードを貼り付けます

```elixir
Mix.install([
  {:mcp_sse, "~> 0.1.6"},
  {:kino, "~> 0.15.3"},
  {:jason, "~> 1.4"},
  {:plug, "~> 1.17"},
  {:bandit, "~> 1.6"},
  {:req, "~> 0.5.10"}
])
```

左上の "Reconnect and setup" をクリックしてください

![スクリーンショット 2025-04-27 11.52.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/170b3757-0836-4dbf-b4f8-ff8a7a3507db.png)

これにより、実装に必要な外部モジュールがインストールされます

キーとなるモジュールは MCP over SSE です

https://github.com/kEND/mcp_sse

Elixir で MCP サーバーの基本的な動作を実装してくれています

### ルーターの定義

Copilot からのリクエストを各処理に振り分けるためのルーターを定義します

以下のコードをセル（黒い枠）に貼り付けてください

```elixir
defmodule MyRouter do
  use Plug.Router

  plug Plug.Parsers,
    parsers: [:urlencoded, :json],
    pass: ["text/*"],
    json_decoder: JSON

  plug :match
  plug :ensure_session_id
  plug :dispatch

  # Middleware to ensure session ID exists
  def ensure_session_id(conn, _opts) do
    case get_session_id(conn) do
      nil ->
        # Generate a new session ID if none exists
        session_id = generate_session_id()
        %{conn | query_params: Map.put(conn.query_params, "sessionId", session_id)}
      _session_id ->
        conn
    end
  end

  # Helper to get session ID from query params
  defp get_session_id(conn) do
    conn.query_params["sessionId"]
  end

  # Generate a unique session ID
  defp generate_session_id do
    Base.encode16(:crypto.strong_rand_bytes(8), case: :lower)
  end

  forward "/sse", to: SSE.ConnectionPlug
  forward "/message", to: SSE.ConnectionPlug

  match _ do
    send_resp(conn, 404, "Not found")
  end
end
```

セルの左上、 "Evaluate" ボタンをクリックし、コードを実行します

![スクリーンショット 2025-04-27 12.08.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/ad62d5af-cb9d-4705-b9f0-9dad5f3e2e0a.png)

### MCP サーバーの実装

先ほど実行したコードの下中央にカーソルを持っていくと、以下のようなボタン群が表示されます

![スクリーンショット 2025-04-27 12.10.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/27883661-c65c-40f3-9735-18fb3cf16c7d.png)

"+ Elixir" のボタンをクリックしてください

新しいセルが追加されるので、以下のコードを貼り付けて実行してください

```elixir
defmodule VoicevoxMCPServer do
  use MCPServer
  require Logger

  @protocol_version "2024-11-05"
  @voicevox_api_url "http://localhost:50021"

  @impl true
  @spec handle_ping(any()) :: {:ok, %{id: any(), jsonrpc: <<_::24>>, method: <<_::32>>}}
  def handle_ping(request_id) do
    {:ok,
     %{
       jsonrpc: "2.0",
       id: request_id,
       result: %{}
     }}
  end

  @impl true
  def handle_initialize(request_id, params) do
    # Log client connection
    client_name = get_in(params, ["client_info", "name"]) || "Unknown Client"
    client_version = get_in(params, ["client_info", "version"]) || "Unknown Version"

    Logger.info("Client connected: #{client_name} v#{client_version}")

    case validate_protocol_version(params["protocolVersion"]) do
      :ok ->
        # Return server capabilities
        {:ok,
         %{
           jsonrpc: "2.0",
           id: request_id,
           result: %{
             protocolVersion: @protocol_version,
             serverInfo: %{
               name: "VoiceVoxMcpServer",
               version: "0.1.0"
             },
             capabilities: %{
               tools: %{
                 listChanged: true
               }
             }
           }
         }}

      {:error, reason} ->
        {:error, reason}
    end
  end

  @impl true
  def handle_list_tools(request_id, _params) do
    # MCP サーバーとして提供するツールの一覧を定義する
    # description でどういうツールなのか生成AIが理解できるようにしておく
    {:ok,
     %{
       jsonrpc: "2.0",
       id: request_id,
       result: %{
         tools: [
           %{
             name: "speak",
             description: "Create an audio file and obtain the audio file path",
             inputSchema: %{
               type: "object",
               properties: %{
                 words: %{
                   type: "string",
                   description: "The words to speak"
                 }
               }
             },
             outputSchema: %{
               type: "object",
               properties: %{
                 audio_file_path: %{
                   type: "string",
                   description: "The audio file path"
                 }
               }
             }
           }
         ]
       }
     }}
  end

  @impl true
  def handle_call_tool(request_id, %{
        "name" => "speak",
        "arguments" => %{"words" => words}
      }) do
    # ツールが呼ばれたときの具体的な挙動を定義する

    case speak(words) do
      {:ok, audio_file_path} ->
        {:ok,
         %{
           jsonrpc: "2.0",
           id: request_id,
           result: %{
             content: [
               %{
                 type: "text",
                 text: audio_file_path
               }
             ]
           }
         }}

      {:error, reason} ->
        {:error,
         %{
           jsonrpc: "2.0",
           id: request_id,
           error: %{
             code: -32_000,
             message: "Failed to say: #{reason}"
           }
         }}
    end
  end

  @impl true
  def handle_call_tool(request_id, %{"name" => unknown_tool} = params) do
    Logger.warning(
      "Unknown tool called: #{unknown_tool} with params: #{inspect(params, pretty: true)}"
    )
    # 定義していないツールが呼び出されたときの挙動

    {:error,
     %{
       jsonrpc: "2.0",
       id: request_id,
       error: %{
         code: -32_601,
         message: "Method not found",
         data: %{
           name: unknown_tool
         }
       }
     }}
  end

  defp speak(words) do
    case Req.post("#{@voicevox_api_url}/audio_query", params: %{text: words, speaker: 3}) do
      {:ok, %{status: 200, body: audio_query}} ->

        case Req.post("#{@voicevox_api_url}/synthesis", params: %{speaker: 3}, json: audio_query, receive_timeout: 120_000) do
          {:ok, %{status: 200, body: data}} ->
            audio_file_path = "/tmp/#{String.slice(words, 0..10)}.wav"
            File.write!(audio_file_path, data)

            {:ok, audio_file_path}

          {:ok, %{status: status, body: body}} ->
            error_message = get_in(body, ["error", "message"]) || "HTTP error: #{status}"
            Logger.error("Voicevox API error: #{error_message}")
            {:error, error_message}
    
          {:error, exception} ->
            Logger.error("Request error: #{inspect(exception)}")
            {:error, "Failed to connect to Voicevox service"} 
        end

      {:ok, %{status: status, body: body}} ->
        error_message = get_in(body, ["error", "message"]) || "HTTP error: #{status}"
        Logger.error("Voicevox API error: #{error_message}")
        {:error, error_message}

      {:error, exception} ->
        Logger.error("Request error: #{inspect(exception)}")
        {:error, "Failed to connect to Voicevox service"}
    end
  end
end
```

`handle_list_tools` 関数で MCP サーバーとして提供可能なツールの一覧を返しています

ツール毎に以下の項目を定義します

|項目|内容|
|:--|:--|
|name|ツールの名前|
|description|ツールの機能説明。生成AIがここを見てツールの使用を判断する|
|inputSchema|入力形式。生成AIからはこの形式に沿って入力が与えられる|
|outputSchema|出力形式。生成AIに返す際の形式|

`handle_call_tool` で、実際にツールが呼び出されたときの挙動を定義します

Elixir らしく、パターンマッチで定義できるようになっています

今回の記事では `"name" => "speak"` でツールが呼び出された場合、 `speak` 関数を呼び出して応答するようにしています

`speak` 関数では VOICEVOX の API を呼び出し、音声合成を実行しています

`%{speaker: 3}` の部分で声の種類を「ずんだもん」に指定しています

`audio_file_path = "/tmp/#{String.slice(words, 0..10)}.wav"` の部分で音声ファイルの保存先を指定しているので、必要に応じて（特に Windows の場合は）変更してください

Livebook からの VOICEVOX 呼び出しについては以下の記事に詳しく記載しています

https://qiita.com/RyoWakabayashi/items/dfcbd6fde07fb38cd7a3

## アプリケーションの定義

MCP を Web サーバーとして起動するため、アプリケーションを定義します

```elixir
defmodule MyApplication do
  use Application

  @impl true
  def start(_type, _args) do
    children = [
      {Bandit, plug: MyRouter, port: 4000}
    ]

    opts = [strategy: :one_for_one, name: MySupervisor]
    Supervisor.start_link(children, opts)
  end
end
```

`plug: MyRouter` で先に定義しておいたルーターを指定しています

ポート番号は `4000` を指定していますが、任意のポート番号を指定してください

### MCP サーバーの起動

アプリケーションにアクセスされた際、実装した MCP サーバーが呼び出されるように設定します

```elixir
Application.put_env(:mcp_sse, :mcp_server, VoicevoxMCPServer)
```

設定しないと MCP over SSE に定義されているデフォルトの MCP （大文字に変える "upcase" ツールを提供）が呼び出されます

https://github.com/kEND/mcp_sse/blob/main/lib/mcp_sse/mcp/default_server.ex

以下のコードを実行し、サーバーを起動します

```elixir
MyApplication.start(nil, nil)
```

## Inspector による動作確認

以下のコマンドで Inspector を起動します

```bash
npx @modelcontextprotocol/inspector
```

:::note info
インストールするか質問された場合、そのままエンターキーでインストールしてください
:::

Inspector が起動したら ブラウザで <http://127.0.0.1:6274> を開きましょう

Inspector の画面が開いたら、画面に以下のように入力します

- Transport Type: SSE
- URL: http://localhost:4000/sse

![スクリーンショット 2025-04-27 12.33.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/821b1f9d-2878-4c8d-9599-6ad9447130fe.png)

"Connect" ボタンをクリックしてください

ボタンの下に "Connected" と表示されたら接続できています

画面中央の "List Tools" ボタンをクリックすると、定義しておいた "speak" が表示されます

![スクリーンショット 2025-04-27 13.08.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/b607bd7a-a146-4e8f-b11b-3b8887d4ff45.png)

"speak" をクリックすると、右側に "words" の入力フォームが表示されます

「こんにちは」と入れて "Run Tool" をクリックすると、実行結果として "/tmp/こんにちは.wav" と返ってきます

![スクリーンショット 2025-04-27 13.13.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/9f866e09-5fb6-4178-95df-5fd377a8696b.png)

作成されたファイルを再生すると、ちゃんと「ずんだもん」の声で「こんにちは」と喋りました

## VSCode に MCP サーバーの設定を追加する

VSCode で上部中央の検索エリアをクリックしてドロップダウンを開きます

ドロップダウンから「コマンドの表示と実行」をクリックしてください

![スクリーンショット 2025-04-27 22.01.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/3e2c512f-0a0e-4c2d-bece-f71e109e4736.png)

コマンドの入力エリアに "settings json" と入力し、選択肢の中から「Open User Settings (JSOn)」をクリックします

![スクリーンショット 2025-04-27 22.01.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/c334d9ea-4cab-4b5f-ad13-bd1efc4df38b.png)

すると、 VSCode の設定ファイル（JSON形式）が開きます

設定に以下のコードを追加して保存します

```json
  "mcp": {
    "servers": {
      "voicevox": {
        "type": "sse",
        "url": "http://localhost:4000/sse"
      }
    }
  },
```

Copilot チャットをエージェントモードに変更します（チャットの下中央を「エージェント」に設定）

![スクリーンショット 2025-04-27 22.16.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/12b4102c-2ca7-4cf1-937c-b41d4dc922ef.png)

チャットの左上、工具アイコンの左に再読込のアイコンが出ていればクリックします

工具アイコンをクリックし、 MCP サーバーが追加されていることを確認します
（追加されていない場合、VSCode の再起動などを試しましょう）

![スクリーンショット 2025-04-27 22.17.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/a2c2cb12-753d-41a2-a739-f2e05a46243a.png)

## チャットからの MCP 呼び出し

Copilot チャットに以下のようなメッセージを書きましょう

- 「こんにちは」と喋ってください

すると、 "speak" ツールを使っていいか聞いてくるので、「続行」をクリックします

![スクリーンショット 2025-04-27 22.19.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/396baf35-8fd1-4fd4-830e-805081c5368f.png)

MCP サーバーが呼び出され、「こんにちは.wav」がリンクとして表示されます

「こんにちは.wav」をクリックすると VSCode 上で「こんにちは.wav」が開き、再生できます

![スクリーンショット 2025-04-27 22.19.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/e750fd71-242d-4c05-843a-30b2b8666bb1.png)

あるいは「音声を再生して」と頼んでも再生してくれます

## まとめ

Livebook 上で MCP サーバーを立て、 VSCode の Copilot チャットから利用することができました

各種サービスのc API やローカル LLM など、連携先は多様にあるので、活用の幅は広そうですね