Skip to main content
Glama

Scenic MCP

server.ex3.82 kB
defmodule ScenicMcp.Server do @moduledoc """ The server is apart of our Scenic app, it is the bridge which our Typescript ScenicMCP server talks to. This server recieves JSON from that typescript server and convert it to actions, send back responses also as JSON back over the same channel. MCP Server (TypeScript) → TCP Bridge (stdIO) → Elixir Server (this module) → Scenic ViewPort → Scenic App """ use GenServer require Logger def start_link(opts) do GenServer.start_link(__MODULE__, opts, name: __MODULE__) end def init(opts) do port = Keyword.fetch!(opts, :port) app_name = Keyword.get(opts, :app_name, "Unknown") case :gen_tcp.listen(port, [:binary, packet: :line, active: false, reuseaddr: true]) do {:ok, listen_socket} -> Logger.info("ScenicMCP TCP server listening for #{app_name} on port #{port}") {:ok, %{listen_socket: listen_socket, port: port, app_name: app_name}, {:continue, :accept}} {:error, :eaddrinuse} -> Logger.error("❌ Port #{port} is already in use for #{app_name}!") Logger.error("💡 Configure a unique port in config.exs: config :scenic_mcp, port: UNIQUE_PORT") Logger.error("📋 Suggested ports: Flamelex=9999, Quillex=9997, Tests=9996/9998") {:stop, :eaddrinuse} {:error, reason} -> Logger.error("Failed to start TCP server on port #{port} for #{app_name}: #{inspect(reason)}") {:stop, "Failed to start TCP server"} end end def handle_continue(:accept, %{listen_socket: listen_socket} = state) do case :gen_tcp.accept(listen_socket) do {:ok, client} -> {:noreply, Map.put(state, :client, client), {:continue, :loop}} {:error, reason} -> Logger.error("Failed to accept connection: #{inspect(reason)}") {:noreply, state, {:continue, :accept}} end end def handle_continue(:loop, %{client: client} = state) do case :gen_tcp.recv(client, 0) do {:ok, data} -> # Parse the incoming data response = parse_message(String.trim(data)) json_response = Jason.encode!(response) <> "\n" :gen_tcp.send(client, json_response) {:noreply, state, {:continue, :loop}} {:error, :closed} -> Logger.info("Client disconnected") {:noreply, Map.delete(state, :client), {:continue, :accept}} {:error, reason} -> Logger.error("Error receiving data: #{inspect(reason)}") :gen_tcp.close(client) {:noreply, Map.delete(state, :client), {:continue, :accept}} end end def handle_info(msg, state) do Logger.warning("#{__MODULE__} - Unexpected message: #{inspect(msg)}") {:noreply, state} end def parse_message("hello") do # Special case for "hello" command used by TypeScript for connection testing %{status: "ok", message: "Hello from Scenic MCP Server"} end def parse_message(json_string) do case Jason.decode(json_string) do {:ok, %{"action" => "status"}} -> # Handle status command %{status: "ok", message: "Scenic MCP Server is running"} {:ok, %{"action" => all_other_actions} = actn} when is_binary(all_other_actions) -> # Handle tool calls ScenicMcp.Tools.handle_action(actn) |> handle_tool_result() {:ok, command} -> Logger.error("#{__MODULE__} received unknown command: #{inspect(command)}") %{error: "Unknown command", command: command} {:error, _} -> Logger.error("#{__MODULE__} received invalid JSON: #{inspect(json_string)}") %{error: "Invalid JSON"} end end # Convert {:ok, result} | {:error, reason} tuples to maps for JSON encoding defp handle_tool_result({:ok, result}), do: result defp handle_tool_result({:error, reason}), do: %{error: reason} end

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/scenic-contrib/scenic_mcp_experimental'

If you have feedback or need assistance with the MCP directory API, please join our Discord server