Current section
Files
Jump to
Current section
Files
lib/phiaui_design/mcp/server.ex
defmodule PhiaUiDesign.Mcp.Server do
@moduledoc """
MCP (Model Context Protocol) server for Claude Code integration.
Implements a JSON-RPC 2.0 server over stdio transport. Claude Code
connects to this server to manipulate PhiaUI designs programmatically.
## Protocol
The server supports:
- `initialize` — handshake with protocol version and capabilities
- `notifications/initialized` — client confirmation
- `tools/list` — enumerate all available design tools
- `tools/call` — execute a tool by name with arguments
## Usage
Started via `mix phia.design.mcp`. Configure in `.mcp.json`:
{
"mcpServers": {
"phiaui-design": {
"command": "mix",
"args": ["phia.design.mcp"],
"env": {"MIX_ENV": "dev"}
}
}
}
"""
alias PhiaUiDesign.Canvas.Scene
alias PhiaUiDesign.Mcp.{ToolRegistry, Tools}
@protocol_version "2024-11-05"
@server_name "phiaui-design"
@server_version "0.1.16"
@doc """
Run the MCP server. Blocks indefinitely reading from stdin.
"""
def run(opts \\ []) do
# Ensure application dependencies are started
Application.ensure_all_started(:phoenix)
Application.ensure_all_started(:phoenix_live_view)
Application.ensure_all_started(:phia_ui)
# Start the catalog server for component introspection
start_catalog()
# Create a fresh scene
scene = Scene.new()
state = %{
scene: scene,
initialized: false,
theme: :zinc,
project_path: Keyword.get(opts, :project_path)
}
# Redirect Elixir/Erlang log output to stderr so stdout stays clean
:logger.set_primary_config(:level, :warning)
loop(state)
end
# ---------------------------------------------------------------------------
# Message loop
# ---------------------------------------------------------------------------
defp loop(state) do
case IO.gets("") do
:eof ->
:ok
{:error, _} ->
:ok
line ->
line = String.trim(line)
state =
if line != "" do
case Jason.decode(line) do
{:ok, message} ->
{state, response} = process(message, state)
if response, do: write_response(response)
state
{:error, _} ->
state
end
else
state
end
loop(state)
end
end
defp write_response(response) do
json = Jason.encode!(response)
IO.write(:stdio, json <> "\n")
end
# ---------------------------------------------------------------------------
# JSON-RPC message processing
# ---------------------------------------------------------------------------
defp process(%{"method" => "initialize", "id" => id}, state) do
result = %{
"protocolVersion" => @protocol_version,
"capabilities" => %{
"tools" => %{}
},
"serverInfo" => %{
"name" => @server_name,
"version" => @server_version
}
}
{%{state | initialized: true}, jsonrpc_result(id, result)}
end
defp process(%{"method" => "notifications/initialized"}, state) do
{state, nil}
end
defp process(%{"method" => "tools/list", "id" => id}, state) do
result = %{"tools" => ToolRegistry.all_tools()}
{state, jsonrpc_result(id, result)}
end
defp process(%{"method" => "tools/call", "id" => id, "params" => params}, state) do
tool_name = params["name"]
arguments = params["arguments"] || %{}
{result, state} = Tools.execute(tool_name, arguments, state)
{state, jsonrpc_result(id, result)}
end
# Ping / unknown method with id
defp process(%{"method" => "ping", "id" => id}, state) do
{state, jsonrpc_result(id, %{})}
end
defp process(%{"id" => id}, state) do
{state, jsonrpc_error(id, -32601, "Method not found")}
end
# Notifications without id
defp process(_message, state) do
{state, nil}
end
# ---------------------------------------------------------------------------
# JSON-RPC helpers
# ---------------------------------------------------------------------------
defp jsonrpc_result(id, result) do
%{"jsonrpc" => "2.0", "id" => id, "result" => result}
end
defp jsonrpc_error(id, code, message) do
%{
"jsonrpc" => "2.0",
"id" => id,
"error" => %{"code" => code, "message" => message}
}
end
# ---------------------------------------------------------------------------
# Setup
# ---------------------------------------------------------------------------
defp start_catalog do
case PhiaUiDesign.Catalog.CatalogServer.start_link() do
{:ok, _} -> :ok
{:error, {:already_started, _}} -> :ok
end
end
end