# McpServer
[](https://hex.pm/packages/mcp_server)
[](https://hexdocs.pm/mcp_server)
McpServer 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 `conn` parameter as their first argument, providing access to session information, user data, and other connection-specific context through `conn.session_id` and `conn.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
1. **Add dependencies to your `mix.exs`:**
```elixir
def deps do
[
{:mcp_server, "~> 0.8.0"},
{:bandit, "~> 1.0"} # HTTP server
]
end
```
2. **Define your MCP Router:**
Create a module that uses `McpServer.Router` and defines your tools and prompts. Example:
```elixir
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
```
3. **Start the Bandit server with your router:**
Add to your application supervision tree:
Make sure to respect the recommended [security options for MCP servers](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#security-warning)
```elixir
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
```elixir
tool "tool_name", "Description", ControllerModule, :function_name do
input_field("param", "Parameter description", :type, required: true)
output_field("result", "Result description", :type)
end
```
### Controller Implementation
Tool controller functions receive `conn` and `args`, and must return `{:ok, CallResult.new(...)}` or `{:error, reason}`:
```elixir
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
end
```
## Prompts
Prompts are interactive message templates with argument completion support. They're useful for generating structured conversations.
### Prompt Definition
```elixir
prompt "prompt_name", "Description" do
argument("arg_name", "Argument description", required: true)
get ControllerModule, :get_function
complete ControllerModule, :complete_function
end
```
### Controller Implementation
Prompt controllers need two functions:
1. **Get function** - Receives `conn` and arguments, returns a list of messages:
```elixir
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
```
2. **Complete function** - Receives `conn`, argument name, and prefix, returns completion suggestions:
```elixir
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)
end
```
### Helper Functions
The `McpServer.Prompt` module provides utility functions:
- `message(role, type, content)` - Creates message structures
- `completion(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 `ui` and `visibility` options
- **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](MCP_APPS.md)** for the complete guide.
## Usage & Testing
### Testing Tools
```elixir
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
end
```
### Testing Prompts
```elixir
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
```