McpServer
View SourceMcpServer is an Elixir library that builds a DSL for defining Model Context Protocol (MCP) tools, prompts, and routers in Elixir. It allows you to easily expose tool endpoints with input/output schemas and validation, as well as define interactive prompts with argument completion.
Key Features
- Type-Safe Structs: All MCP protocol structures are now typed Elixir structs with compile-time validation
- Connection Context: All controller functions receive a
connparameter as their first argument, providing access to session information, user data, and other connection-specific context throughconn.session_idandconn.private - Validated Tools: Define tools with automatic input validation and output schemas
- Interactive Prompts: Create prompts with argument completion support
- Resource Management: Define and serve resources with URI templates
- Automatic JSON Encoding: All structs automatically encode to proper MCP JSON format
Installation and setup
- Add dependencies to your
mix.exs:
def deps do
[
{:mcp_server, "~> 0.8.0"},
{:bandit, "~> 1.0"} # HTTP server
]
end- Define your MCP Router:
Create a module that uses McpServer.Router and defines your tools and prompts. Example:
defmodule MyApp.MyController do
import McpServer.Controller, only: [message: 3, completion: 2, content: 3]
alias McpServer.Tool.Content, as: ToolContent
alias McpServer.Tool.CallResult
# Tool functions - all receive conn as first parameter
# Return {:ok, CallResult.new(content: [...])} or {:error, reason}
def echo(_conn, args) do
{:ok, CallResult.new(content: [ToolContent.text(Map.get(args, "message", "default"))])}
end
def greet(conn, args) do
name = Map.get(args, "name", "World")
{:ok, CallResult.new(content: [ToolContent.text("Hello, #{name}, you are connected with session #{conn.session_id}!")])}
end
def calculate(_conn, args) do
result = Map.get(args, "a", 0) + Map.get(args, "b", 0)
{:ok, CallResult.new(content: [ToolContent.text("#{result}")])}
end
# Prompt functions - all receive conn as first parameter
def get_greet_prompt(_conn, %{"user_name" => user_name}) do
[
message("user", "text", "Hello #{user_name}! Welcome to our MCP server. How can I assist you today?"),
message("assistant", "text", "I'm here to help you with any questions or tasks you might have.")
]
end
def complete_greet_prompt(_conn, "user_name", user_name_prefix) do
names = ["Alice", "Bob", "Charlie", "David"]
filtered_names = Enum.filter(names, &String.starts_with?(&1, user_name_prefix))
completion(filtered_names, total: 100, has_more: true)
end
# Resource reader example - receives conn as first parameter, returns ReadResult struct
def read_user(_conn, %{"id" => id}) do
McpServer.Resource.ReadResult.new(
contents: [
content(
"User #{id}",
"https://example.com/users/#{id}",
mimeType: "application/json",
text: "{\"id\": \"#{id}\", \"name\": \"User #{id}\"}",
title: "User title #{id}"
)
]
)
end
end
defmodule MyApp.Router do
use McpServer.Router
# Define tools
tool "greet", "Greets a person", MyApp.MyController, :greet do
input_field("name", "The name to greet", :string, required: false)
output_field("greeting", "The greeting message", :string)
end
tool "calculate", "Adds two numbers", MyApp.MyController, :calculate do
input_field("a", "First number", :integer, required: true)
input_field("b", "Second number", :integer, required: true)
output_field("result", "The sum of the numbers", :integer)
end
tool "echo", "Echoes back the input", MyApp.MyController, :echo,
title: "Echo",
hints: [:read_only, :non_destructive, :idempotent, :closed_world] do
input_field("message", "The message to echo", :string, required: true)
output_field("response", "The echoed message", :string)
end
# Define prompts
prompt "greet", "A friendly greeting prompt that welcomes users" do
argument("user_name", "The name of the user to greet", required: true)
get MyApp.MyController, :get_greet_prompt
complete MyApp.MyController, :complete_greet_prompt
end
# Define resources
resource "user", "https://example.com/users/{id}" do
description "User resource"
mimeType "application/json"
title "User title"
read MyApp.MyController, :read_user
complete MyApp.MyController, :complete_user
end
end- Start the Bandit server with your router:
Add to your application supervision tree:
Make sure to respect the recommended security options for MCP servers
children = [
{Bandit, plug: {
McpServer.HttpPlug,
router: MyApp.Router,
server_info: %{name: "MyApp MCP Server", version: "1.0.0"}
}, port: 4000, ip: {127, 0, 0, 1}}
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)Your MCP server will now be running and serving your defined tools and prompts.
Tools
Tools are functions that can be called by the MCP client. They support input validation and output schemas.
Tool Definition
tool "tool_name", "Description", ControllerModule, :function_name do
input_field("param", "Parameter description", :type, required: true)
output_field("result", "Result description", :type)
endController Implementation
Tool controller functions receive conn and args, and must return {:ok, CallResult.new(...)} or {:error, reason}:
alias McpServer.Tool.Content, as: ToolContent
alias McpServer.Tool.CallResult
# Return text content
def my_tool(_conn, %{"query" => query}) do
{:ok, CallResult.new(content: [ToolContent.text("Results for: #{query}")])}
end
# Return multiple content types
def generate_chart(_conn, %{"data" => data}) do
chart_image = create_chart(data)
{:ok, CallResult.new(content: [
ToolContent.text("Chart generated successfully"),
ToolContent.image(chart_image, "image/png")
])}
end
# Return structured content for UI rendering
def get_weather(_conn, %{"location" => location}) do
weather = fetch_weather(location)
{:ok, CallResult.new(
content: [ToolContent.text("Weather in #{location}: #{weather.temp}°F")],
structured_content: %{
"temperature" => weather.temp,
"humidity" => weather.humidity
}
)}
end
# Signal an error
def risky_tool(_conn, %{"input" => input}) do
case process(input) do
{:ok, result} -> {:ok, CallResult.new(content: [ToolContent.text(result)])}
{:error, reason} -> {:error, reason}
end
endPrompts
Prompts are interactive message templates with argument completion support. They're useful for generating structured conversations.
Prompt Definition
prompt "prompt_name", "Description" do
argument("arg_name", "Argument description", required: true)
get ControllerModule, :get_function
complete ControllerModule, :complete_function
endController Implementation
Prompt controllers need two functions:
- Get function - Receives
connand arguments, returns a list of messages:
def get_prompt_name(conn, %{"arg_name" => value}) do
# Access session info via conn.session_id or conn.private
[
message("user", "text", "User message with #{value}"),
message("assistant", "text", "Assistant response"),
message("system", "text", "System instructions")
]
end- Complete function - Receives
conn, argument name, and prefix, returns completion suggestions:
def complete_prompt_name(conn, "arg_name", prefix) do
# Access session info via conn.session_id or conn.private
suggestions = ["option1", "option2", "option3"]
filtered = Enum.filter(suggestions, &String.starts_with?(&1, prefix))
completion(filtered, total: 100, has_more: true)
endHelper Functions
The McpServer.Prompt module provides utility functions:
message(role, type, content)- Creates message structurescompletion(values, opts)- Creates completion responses
Structures Reference
The library provides typed structs for all MCP message types and data structures. All structs implement Jason.Encoder for JSON serialization.
See STRUCTURES.md for a complete reference of all available structs and their fields.
MCP Apps (Interactive UIs)
McpServer supports the MCP Apps extension (io.modelcontextprotocol/ui) for delivering interactive UIs alongside AI conversations. This includes:
- UI Tools — Link tools to UI resources with
uiandvisibilityoptions - UI Resources — Serve HTML content via
ui://URIs with CSP and sandbox permissions - Structured Content — Return rich data for UI rendering via
McpServer.Tool.CallResult
See MCP_APPS.md for the complete guide.
Usage & Testing
Testing Tools
defmodule MyApp.RouterTest do
use ExUnit.Case
setup do
%{conn: %McpServer.Conn{session_id: "test-session"}}
end
test "call a tool", %{conn: conn} do
{:ok, result} = MyApp.Router.call_tool(conn, "echo", %{"message" => "Hello World"})
assert %McpServer.Tool.CallResult{content: [content]} = result
assert %McpServer.Tool.Content.Text{text: "Hello World"} = content
end
test "list tools", %{conn: conn} do
{:ok, tools} = MyApp.Router.list_tools(conn)
echo = Enum.find(tools, &(&1.name == "echo"))
assert echo.description == "Echoes back the input"
end
endTesting Prompts
defmodule MyApp.PromptTest do
use ExUnit.Case
setup do
%{conn: %McpServer.Conn{session_id: "test-session"}}
end
test "get prompt messages", %{conn: conn} do
{:ok, messages} = MyApp.Router.get_prompt(conn, "greet", %{"user_name" => "Alice"})
assert hd(messages).role == "user"
assert hd(messages).content.text =~ "Hello Alice"
end
test "complete prompt arguments", %{conn: conn} do
{:ok, completion} = MyApp.Router.complete_prompt(conn, "greet", "user_name", "A")
assert "Alice" in completion.values
assert completion.has_more == true
end
test "list prompts", %{conn: conn} do
{:ok, prompts} = MyApp.Router.prompts_list(conn)
greet = Enum.find(prompts, &(&1.name == "greet"))
assert greet.description == "A friendly greeting prompt that welcomes users"
end
end