はじめに
VSCode で MCP サーバーを使っていますか?
予め設定を加えておくことで、 GitHub Copilot が必要に応じて MCP サーバーで提供されている機能にアクセスし、各種 API などと連携して回答してくれるようになります
本記事では Elixir の Livebook 上で MCP サーバーを起動することで、 VSCode の Copilot チャットから VOICEVOX の音声合成を実行します
実装したノートブックはこちら
事前準備
Livebook のインストール
公式サイトから Livebook のインストーラーを取得し、インストールします
VSCode のインストール
公式サイトから VSCode のインストーラーを取得し、インストールします
GitHub Copilot の開始
GitHub にアカウントを作成しましょう
アカウント作成後、右上のユーザーアイコンをクリックしてください
開いたメニューから "Your Copilot" をクリックします
開いた Copilot の設定画面で "Start using Copilot Free" をクリックします
Copilot が Free プランで利用できるようになり、チャット画面が開きます
VSCode に Copilot 用の拡張機能をインストールしましょう
インストール後、 VSCode の上部、検索ボックスの右側にある Copilot アイコン(バツマーク付き)をクリックします
VSCode から GitHub にサインインします
サインインできると、 Copilot アイコンのバツマークが外れています
Copilot アイコンをクリックして開くドロップダウンから "Open Chat" をクリックしてください
Copilot とチャットするための画面が開きました
公式のドキュメントはこちら
VOICEVOX のインストール
公式サイトから VOICEVOX のインストーラーを取得し、インストールします
Node.js のインストール
本記事では MCP サーバーの挙動を確認するための Inspector を利用します
その実行環境として Node.js が必要です
いずれかの方法で Node.js をインストールしてください
mise
mise use node@latest
asdf
asdf plugin add nodejs
asdf install nodejs latest
Homebrew
brew install node
公式インストーラー
公式サイトからインストーラーを取得し、インストールします
MCP サーバーの実装
セットアップ
Livebook を起動し、ホーム画面右上の "+ New Notebook" をクリックします
新しいノートブックが開きます
セットアップセル("Notebook dependencies and setup" と書いてある枠の中)をクリックし、以下のコードを貼り付けます
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" をクリックしてください
これにより、実装に必要な外部モジュールがインストールされます
キーとなるモジュールは MCP over SSE です
Elixir で MCP サーバーの基本的な動作を実装してくれています
ルーターの定義
Copilot からのリクエストを各処理に振り分けるためのルーターを定義します
以下のコードをセル(黒い枠)に貼り付けてください
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" ボタンをクリックし、コードを実行します
MCP サーバーの実装
先ほど実行したコードの下中央にカーソルを持っていくと、以下のようなボタン群が表示されます
"+ Elixir" のボタンをクリックしてください
新しいセルが追加されるので、以下のコードを貼り付けて実行してください
defmodule VoicevoxMCPServer do
@moduledoc """
Hex Server implementation and definitions of tools
- list_hex_packages
"""
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 呼び出しについては以下の記事に詳しく記載しています
アプリケーションの定義
MCP を Web サーバーとして起動するため、アプリケーションを定義します
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 サーバーが呼び出されるように設定します
Application.put_env(:mcp_sse, :mcp_server, VoicevoxMCPServer)
設定しないと MCP over SSE に定義されているデフォルトの MCP (大文字に変える "upcase" ツールを提供)が呼び出されます
以下のコードを実行し、サーバーを起動します
MyApplication.start(nil, nil)
Inspector による動作確認
以下のコマンドで Inspector を起動します
npx @modelcontextprotocol/inspector
インストールするか質問された場合、そのままエンターキーでインストールしてください
Inspector が起動したら ブラウザで http://127.0.0.1:6274 を開きましょう
Inspector の画面が開いたら、画面に以下のように入力します
- Transport Type: SSE
- URL: http://localhost:4000/sse
"Connect" ボタンをクリックしてください
ボタンの下に "Connected" と表示されたら接続できています
画面中央の "List Tools" ボタンをクリックすると、定義しておいた "speak" が表示されます
"speak" をクリックすると、右側に "words" の入力フォームが表示されます
「こんにちは」と入れて "Run Tool" をクリックすると、実行結果として "/tmp/こんにちは.wav" と返ってきます
作成されたファイルを再生すると、ちゃんと「ずんだもん」の声で「こんにちは」と喋りました
VSCode に MCP サーバーの設定を追加する
VSCode で上部中央の検索エリアをクリックしてドロップダウンを開きます
ドロップダウンから「コマンドの表示と実行」をクリックしてください
コマンドの入力エリアに "settings json" と入力し、選択肢の中から「Open User Settings (JSOn)」をクリックします
すると、 VSCode の設定ファイル(JSON形式)が開きます
設定に以下のコードを追加して保存します
"mcp": {
"servers": {
"voicevox": {
"type": "sse",
"url": "http://localhost:4000/sse"
}
}
},
Copilot チャットをエージェントモードに変更します(チャットの下中央を「エージェント」に設定)
チャットの左上、工具アイコンの左に再読込のアイコンが出ていればクリックします
工具アイコンをクリックし、 MCP サーバーが追加されていることを確認します
(追加されていない場合、VSCode の再起動などを試しましょう)
チャットからの MCP 呼び出し
Copilot チャットに以下のようなメッセージを書きましょう
- 「こんにちは」と喋ってください
すると、 "speak" ツールを使っていいか聞いてくるので、「続行」をクリックします
MCP サーバーが呼び出され、「こんにちは.wav」がリンクとして表示されます
「こんにちは.wav」をクリックすると VSCode 上で「こんにちは.wav」が開き、再生できます
あるいは「音声を再生して」と頼んでも再生してくれます
まとめ
Livebook 上で MCP サーバーを立て、 VSCode の Copilot チャットから利用することができました
各種サービスのc API やローカル LLM など、連携先は多様にあるので、活用の幅は広そうですね