Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

VOICEVOX MCP Server

livebooks/mcp/voicevox_mcp_server.livemd

VOICEVOX MCP Server

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

Router

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

MCP Server

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
    {: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

Application

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

Start MCP Server

Application.put_env(:mcp_sse, :mcp_server, VoicevoxMCPServer)
MyApplication.start(nil, nil)