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.
📢 Upgrading from v0.3.x? See the Migration Guide for a step-by-step upgrade path to v0.4.0's struct-based API.
What's New in v0.4.0
Version 0.4.0 introduces typed structs throughout the library:
- Type-safe structures: All MCP protocol types are now proper Elixir structs with
@enforce_keysvalidation - Better IDE support: Autocomplete and inline documentation for all struct fields
- Compile-time safety: Catch missing fields and typos at compile time, not runtime
- Clearer code: Use
.fieldsyntax instead of["field"]for accessing data - Automatic JSON encoding: All structs implement
Jason.Encoderwith proper camelCase conversion
Breaking Changes:
- Controller functions now require
connas first parameter (arity change) - Router list functions renamed (e.g.,
tools_list()→list_tools(conn)) - All functions return typed structs instead of plain maps
See CHANGELOG_v0.4.0.md for complete details.
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.4.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]
# Tool functions - all receive conn as first parameter
def echo(_conn, args), do: Map.get(args, "message", "default")
def greet(conn, args), do: "Hello, #{Map.get(args, "name", "World")}, you are connected with session #{conn.session_id}!"
def calculate(_conn, args), do: Map.get(args, "a", 0) + Map.get(args, "b", 0)
# 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)
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.
Usage & Testing
Testing Tools
You can call your tools via the router module (note: you need to pass a connection):
iex> conn = %McpServer.Conn{session_id: "test-session"}
iex> {:ok, result} = MyApp.Router.call_tool(conn, "echo", %{"message" => "Hello World"})
iex> result
# => "Hello World"List all tools and their schemas (returns Tool structs):
iex> conn = %McpServer.Conn{session_id: "test-session"}
iex> {:ok, tools} = MyApp.Router.list_tools(conn)
iex> hd(tools).name
# => "echo"
iex> hd(tools).description
# => "Echoes back the input"Testing Prompts
You can get prompt messages (returns Message structs):
iex> conn = %McpServer.Conn{session_id: "test-session"}
iex> {:ok, messages} = MyApp.Router.get_prompt(conn, "greet", %{"user_name" => "Alice"})
iex> hd(messages).role
# => "user"
iex> hd(messages).content.text
# => "Hello Alice! Welcome to our MCP server..."Get completion suggestions for prompt arguments (returns Completion struct):
iex> conn = %McpServer.Conn{session_id: "test-session"}
iex> {:ok, completion} = MyApp.Router.complete_prompt(conn, "greet", "user_name", "A")
iex> completion.values
# => ["Alice"]
iex> completion.total
# => 100
iex> completion.has_more
# => trueList all prompts (returns Prompt structs):
iex> conn = %McpServer.Conn{session_id: "test-session"}
iex> {:ok, prompts} = MyApp.Router.prompts_list(conn)
iex> hd(prompts).name
# => "greet"
iex> hd(prompts).description
# => "A friendly greeting prompt that welcomes users"