An Elixir implementation of the Model Context Protocol (MCP) specification. Build MCP servers to expose tools, resources, and prompts to LLM applications.
- Clean DSL - Declarative tool definitions with automatic schema generation
- Runtime Validation - Parameter validation with NimbleOptions, type coercion, and custom constraints
- Stateless Architecture - Pure functions, no processes, maximum concurrency
- Flexible Authentication - Bearer tokens, API keys, custom verification
- Full MCP Spec - Tools, resources, prompts, and all MCP 2025-06-18 features
- Phoenix Ready - Drop-in integration with Phoenix applications
- Production Ready - Comprehensive tests, telemetry, CORS support
def deps do
[
{:conduit_mcp, "~> 0.6.1"}
]
enddefmodule MyApp.MCPServer do
use ConduitMcp.Server
tool "greet", "Greet someone" do
param :name, :string, "Person's name", required: true
param :style, :string, "Greeting style", enum: ["formal", "casual"]
handle fn _conn, params ->
name = params["name"]
style = params["style"] || "casual"
greeting = if style == "formal", do: "Good day", else: "Hey"
text("#{greeting}, #{name}!")
end
end
prompt "code_review", "Code review assistant" do
arg :code, :string, "Code to review", required: true
arg :language, :string, "Language", default: "elixir"
get fn _conn, args ->
[
system("You are a code reviewer"),
user("Review this #{args["language"]} code:\n#{args["code"]}")
]
end
end
resource "user://{id}" do
description "User profile"
mime_type "application/json"
read fn _conn, params, _opts ->
user = MyApp.Users.get!(params["id"])
json(user)
end
end
enddefmodule MyApp.MCPServer do
use ConduitMcp.Server
tool "create_user", "Create a new user with validation" do
# Basic types with constraints
param :name, :string, "Full name", required: true, min_length: 2, max_length: 50
param :age, :integer, "Age", min: 0, max: 150, required: true
param :email, :string, "Email address", validator: &ConduitMcp.Validation.Validators.email/1
# Enum validation with default
param :role, :string, "User role", enum: ["admin", "user", "guest"], default: "user"
# Numeric constraints with defaults
param :score, :number, "Performance score", min: 0.0, max: 100.0, default: 50.0
handle &UserService.create/2
end
tool "calculate", "Math with validation" do
param :operation, :string, "Math operation", enum: ~w(add subtract multiply divide), required: true
param :a, :number, "First number", required: true
param :b, :number, "Second number", required: true
param :precision, :integer, "Decimal places", min: 0, max: 10, default: 2
handle fn _conn, params ->
result = case params["operation"] do
"add" -> params["a"] + params["b"]
"subtract" -> params["a"] - params["b"]
"multiply" -> params["a"] * params["b"]
"divide" -> params["a"] / params["b"]
end
formatted = Float.round(result, params["precision"])
text("Result: #{formatted}")
end
end
endHelper functions available:
text(string)- Text responsejson(data)- JSON responseraw(data)- Raw data response (bypasses MCP wrapping, for debugging)error(message)orerror(message, code)- Error responsesystem(content),user(content),assistant(content)- Prompt messages
defmodule MyApp.MCPServer do
use ConduitMcp.Server, dsl: false
@tools [
%{
"name" => "greet",
"description" => "Greet someone",
"inputSchema" => %{
"type" => "object",
"properties" => %{
"name" => %{"type" => "string"}
},
"required" => ["name"]
}
}
]
@impl true
def handle_list_tools(_conn) do
{:ok, %{"tools" => @tools}}
end
@impl true
def handle_call_tool(_conn, "greet", %{"name" => name}) do
{:ok, %{"content" => [%{"type" => "text", "text" => "Hello, #{name}!"}]}}
end
end# lib/my_app/application.ex
def start(_type, _args) do
children = [
{Bandit,
plug: {ConduitMcp.Transport.StreamableHTTP, server_module: MyApp.MCPServer},
port: 4001}
]
Supervisor.start_link(children, strategy: :one_for_one)
end# lib/my_app/mcp_server.ex
defmodule MyApp.MCPServer do
use ConduitMcp.Server
alias MyApp.Accounts
tool "get_user", "Get user from database" do
param :user_id, :string, "User ID", required: true
handle fn _conn, %{"user_id" => id} ->
user = Accounts.get_user!(id)
json(%{id: user.id, name: user.name, email: user.email})
end
end
tool "search", "Search users" do
param :query, :string, "Search query", required: true
param :limit, :number, "Max results", default: 10
handle Accounts, :search_users
end
end
# lib/my_app_web/router.ex
defmodule MyAppWeb.Router do
use MyAppWeb, :router
scope "/mcp" do
forward "/", ConduitMcp.Transport.StreamableHTTP,
server_module: MyApp.MCPServer,
auth: [
strategy: :bearer_token,
token: System.get_env("MCP_AUTH_TOKEN")
]
end
endConduitMCP v0.6.0+ includes comprehensive runtime parameter validation using NimbleOptions. Validation includes type checking, constraints, and automatic type coercion.
tool "create_user", "Create a new user" do
# Basic types with constraints
param :name, :string, "Full name", required: true, min_length: 2, max_length: 50
param :age, :integer, "Age", min: 0, max: 150
param :email, :string, "Email address", validator: &ConduitMcp.Validation.Validators.email/1
# Enum validation
param :role, :string, "User role", enum: ["admin", "user", "guest"], default: "user"
# Numeric constraints
param :score, :number, "Performance score", min: 0.0, max: 100.0, default: 50.0
handle &UserService.create/2
endAutomatic type conversion when type_coercion: true (enabled by default):
# Client sends: {"age": "25", "active": "true", "score": "85.5"}
# Server receives: %{"age" => 25, "active" => true, "score" => 85.5}Use built-in validators or create your own:
# Built-in validators
param :email, :string, "Email", validator: &ConduitMcp.Validation.Validators.email/1
param :url, :string, "Website", validator: &ConduitMcp.Validation.Validators.url/1
# Custom validator function
param :username, :string, "Username", validator: fn username ->
String.match?(username, ~r/^[a-zA-Z0-9_]{3,20}$/)
end
# Module function validator
param :priority, :string, "Priority", validator: {MyApp.Validators, :valid_priority?}Configure validation behavior in your application config:
# config/config.exs
config :conduit_mcp, :validation,
runtime_validation: true, # Enable validation (default: true)
strict_mode: true, # Fail on validation errors (default: true)
type_coercion: true, # Auto-convert types (default: true)
log_validation_errors: false # Log failures (default: false)Validation failures return detailed error information:
{
"error": {
"code": -32602,
"message": "Parameter validation failed",
"data": {
"errors": [
{
"parameter": "age",
"value": -5,
"message": "must be greater than or equal to 0"
},
{
"parameter": "email",
"value": "invalid-email",
"message": "must be a valid email address"
}
]
}
}
}| Constraint | Types | Description | Example |
|---|---|---|---|
required: true |
All | Parameter must be present | required: true |
min: N |
number, integer | Minimum value (inclusive) | min: 0 |
max: N |
number, integer | Maximum value (inclusive) | max: 100 |
min_length: N |
string | Minimum string length | min_length: 3 |
max_length: N |
string | Maximum string length | max_length: 255 |
enum: [...] |
All | Value must be in list | enum: ["red", "green", "blue"] |
default: value |
All | Default if not provided | default: "guest" |
validator: fun |
All | Custom validation function | validator: &valid_email?/1 |
Configure authentication in transport options:
# No auth (development)
auth: [enabled: false]
# Static bearer token
auth: [
strategy: :bearer_token,
token: "your-secret-token"
]
# Static API key
auth: [
strategy: :api_key,
api_key: "your-api-key",
header: "x-api-key"
]
# Custom verification
auth: [
strategy: :function,
verify: fn token ->
case MyApp.Auth.verify(token) do
{:ok, user} -> {:ok, user}
_ -> {:error, "Invalid token"}
end
end
]
# Database lookup
auth: [
strategy: :function,
verify: fn token ->
case MyApp.Repo.get_by(ApiToken, token: token) do
%ApiToken{user: user} -> {:ok, user}
nil -> {:error, "Invalid token"}
end
end
]Access authenticated user in tools:
tool "profile", "Get profile" do
handle fn conn, _params ->
case conn.assigns[:current_user] do
nil -> error("Not authenticated")
user -> json(user)
end
end
end{
"mcpServers": {
"my-app": {
"url": "http://localhost:4001/",
"headers": {
"Authorization": "Bearer your-token"
}
}
}
}{
"mcpServers": {
"my-app": {
"command": "elixir",
"args": ["/path/to/your/server.exs"]
}
}
}ConduitMCP emits telemetry events for monitoring:
[:conduit_mcp, :request, :stop]- All MCP requests[:conduit_mcp, :tool, :execute]- Tool executions[:conduit_mcp, :resource, :read]- Resource reads[:conduit_mcp, :prompt, :get]- Prompt retrievals[:conduit_mcp, :auth, :verify]- Authentication attempts
Example handler:
:telemetry.attach(
"mcp-logger",
[:conduit_mcp, :tool, :execute],
fn _event, %{duration: duration}, %{tool_name: name}, _config ->
ms = System.convert_time_unit(duration, :native, :millisecond)
Logger.info("Tool #{name} executed in #{ms}ms")
end,
nil
)ConduitMCP includes an optional PromEx plugin for Prometheus monitoring.
Add :prom_ex to your dependencies:
def deps do
[
{:conduit_mcp, "~> 0.5.0"},
{:prom_ex, "~> 1.11"}
]
endAdd the ConduitMCP plugin to your PromEx configuration:
defmodule MyApp.PromEx do
use PromEx, otp_app: :my_app
@impl true
def plugins do
[
PromEx.Plugins.Application,
PromEx.Plugins.Beam,
{ConduitMcp.PromEx, otp_app: :my_app}
]
end
@impl true
def dashboard_assigns do
[
datasource_id: "prometheus",
default_selected_interval: "30s"
]
end
endAdd to your supervision tree:
def start(_type, _args) do
children = [
MyApp.PromEx,
# ... other children ...
]
Supervisor.start_link(children, strategy: :one_for_one)
endAll metrics are prefixed with {otp_app}_conduit_mcp_:
Request Metrics:
request_total{method, status}- Total MCP requestsrequest_duration_milliseconds{method, status}- Request duration distribution
Tool Metrics:
tool_execution_total{tool_name, status}- Total tool executionstool_duration_milliseconds{tool_name, status}- Tool execution duration
Resource Metrics:
resource_read_total{status}- Total resource readsresource_read_duration_milliseconds{status}- Read duration
Prompt Metrics:
prompt_get_total{prompt_name, status}- Total prompt retrievalsprompt_get_duration_milliseconds{prompt_name, status}- Retrieval duration
Auth Metrics:
auth_verify_total{strategy, status}- Total auth attemptsauth_verify_duration_milliseconds{strategy, status}- Verification duration
Request rate by method:
rate(myapp_conduit_mcp_request_total[5m])
Error rate percentage:
100 * (
rate(myapp_conduit_mcp_request_total{status="error"}[5m])
/
rate(myapp_conduit_mcp_request_total[5m])
)
P95 tool execution duration:
histogram_quantile(0.95,
rate(myapp_conduit_mcp_tool_duration_milliseconds_bucket[5m])
)
Authentication success rate:
100 * (
rate(myapp_conduit_mcp_auth_verify_total{status="ok"}[5m])
/
rate(myapp_conduit_mcp_auth_verify_total[5m])
)
See ConduitMcp.PromEx module documentation for complete details and alert examples.
Apache License 2.0
