McpServer.Router (HTTP MCP Server v0.6.0)
View SourceA Domain-Specific Language (DSL) for defining Model Context Protocol (MCP) servers.
McpServer.Router provides a declarative way to define MCP tools, prompts, and resources
with automatic validation, schema generation, and request routing. It implements the
McpServer behaviour and generates the necessary callback implementations at compile time.
Overview
The Router DSL allows you to define three main MCP capabilities:
- Tools - Callable functions with typed input/output schemas and validation
- Prompts - Interactive message templates with argument completion support
- Resources - Data sources with URI-based access and optional templating
All controller functions receive a McpServer.Conn struct as their first parameter,
providing access to session information and connection context.
Usage
To create an MCP server, use McpServer.Router in your module and define your capabilities:
defmodule MyApp.Router do
use McpServer.Router
# Define tools
tool "calculator", "Performs arithmetic operations", MyApp.Calculator, :calculate do
input_field("operation", "The operation to perform", :string,
required: true,
enum: ["add", "subtract", "multiply", "divide"])
input_field("a", "First operand", :number, required: true)
input_field("b", "Second operand", :number, required: true)
output_field("result", "The calculation result", :number)
end
# Define prompts
prompt "code_review", "Generates a code review prompt" do
argument("language", "Programming language", required: true)
argument("code", "Code to review", required: true)
get MyApp.Prompts, :get_code_review
complete MyApp.Prompts, :complete_code_review
end
# Define resources
resource "config", "file:///app/config/{name}.json" do
description "Application configuration files"
mimeType "application/json"
read MyApp.Resources, :read_config
complete MyApp.Resources, :complete_config
end
endConnection Context
All controller functions receive a McpServer.Conn struct as their first parameter:
def my_tool(conn, args) do
# Access session ID
session_id = conn.session_id
# Access private data stored in the connection
user = McpServer.Conn.get_private(conn, :user)
# Your tool logic here
endThe connection provides:
session_id- Unique identifier for the current sessionprivate- A map for storing custom data (accessible via helper functions)
Tools
Tools are functions that clients can invoke with validated inputs. Each tool requires:
- A unique name
- A description
- A controller module and function (arity 2: conn, args)
- Input/output field definitions
Tool Definition
tool "name", "description", ControllerModule, :function_name do
input_field("param", "Parameter description", :type, opts)
output_field("result", "Result description", :type)
endSupported Field Types
:string- Text values:integer- Whole numbers:number- Numeric values (integers and floats):boolean- True/false values:array- Lists of values (supports nested items):object- Nested structures (supports nested properties)
Field Options
required: true/false- Whether the field is mandatory (default: false)enum: [...]- Restrict values to a specific setdefault: value- Default value if not provideditems: :type- For arrays, specify the type of items (e.g.,items: :string)
Nested Structures
Tools support deeply nested object and array schemas using do-blocks:
Nested Objects
tool "create_user", "Creates a user", UserController, :create do
input_field("user", "User data", :object, required: true) do
field("name", "Full name", :string, required: true)
field("email", "Email address", :string, required: true)
field("address", "Mailing address", :object) do
field("street", "Street address", :string)
field("city", "City", :string, required: true)
field("country", "Country code", :string, required: true)
end
end
endArrays with Simple Items
tool "process_tags", "Process tags", TagController, :process do
input_field("tags", "List of tags", :array, required: true, items: :string)
input_field("scores", "Score values", :array, items: :number)
endArrays with Complex Items
tool "batch_create", "Batch create users", UserController, :batch do
input_field("users", "List of users", :array, required: true) do
items :object do
field("name", "User name", :string, required: true)
field("email", "Email", :string, required: true)
field("roles", "User roles", :array, items: :string)
end
end
endComplex Nested Example
tool "create_project", "Creates a project", ProjectController, :create do
input_field("project", "Project data", :object, required: true) do
field("name", "Project name", :string, required: true)
field("owner", "Project owner", :object, required: true) do
field("id", "User ID", :string, required: true)
field("name", "User name", :string)
end
field("team", "Team members", :array) do
items :object do
field("user_id", "User ID", :string, required: true)
field("role", "Role", :string, enum: ["admin", "developer", "viewer"])
field("permissions", "Permission flags", :array, items: :string)
end
end
field("metadata", "Metadata", :object) do
field("tags", "Tags", :array, items: :string)
field("settings", "Settings", :object) do
field("private", "Is private", :boolean, default: false)
end
end
end
endTool Hints
Tools can include behavioral hints for clients:
tool "read_file", "Reads a file", FileController, :read,
title: "File Reader",
hints: [:read_only, :idempotent, :closed_world] do
# fields...
endAvailable hints:
:read_only- Tool doesn't modify state:non_destructive- Tool is safe to call:idempotent- Tool can be called repeatedly with same result:closed_world- Tool only works with known/predefined data
Controller Implementation
defmodule MyApp.Calculator do
def calculate(conn, %{"operation" => op, "a" => a, "b" => b}) do
# Access session info if needed
IO.inspect(conn.session_id)
case op do
"add" -> a + b
"subtract" -> a - b
"multiply" -> a * b
"divide" when b != 0 -> a / b
"divide" -> {:error, "Division by zero"}
end
end
end
# Controller for nested structures
defmodule MyApp.UserController do
def create(conn, %{"user" => user_data}) do
# user_data is a nested map matching your schema
%{
"name" => name,
"email" => email,
"address" => %{
"city" => city,
"country" => country
}
} = user_data
# Your creation logic here
{:ok, %{"id" => "user_123", "created" => true}}
end
endPrompts
Prompts are interactive message templates that help structure conversations. They support argument completion for improved user experience.
Prompt Definition
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 (arity 2: conn, args)
Returns a list of messages for the conversation:
defmodule MyApp.Prompts do
import McpServer.Controller, only: [message: 3]
def get_code_review(conn, %{"language" => lang, "code" => code}) do
[
message("system", "text",
"You are an expert " <> lang <> " code reviewer."),
message("user", "text",
"Please review this code:\n\n" <> code)
]
end
endComplete Function (arity 3: conn, argument_name, prefix)
Provides completion suggestions for prompt arguments:
defmodule MyApp.Prompts do
import McpServer.Controller, only: [completion: 2]
def complete_code_review(conn, "language", prefix) do
languages = ["elixir", "python", "javascript", "rust", "go"]
filtered = Enum.filter(languages, &String.starts_with?(&1, prefix))
completion(filtered, total: length(languages), has_more: false)
end
def complete_code_review(_conn, _arg, _prefix), do: completion([], [])
endResources
Resources represent data sources that clients can read. They support:
- Static URIs for fixed resources
- URI templates with variables (e.g.,
{id}) for dynamic resources - Optional completion for template variables
Static Resource
resource "readme", "file:///app/README.md" do
description "Project README file"
mimeType "text/markdown"
read MyApp.Resources, :read_readme
endTemplated Resource
resource "user", "https://api.example.com/users/{id}" do
description "User profile data"
mimeType "application/json"
title "User Profile"
read MyApp.Resources, :read_user
complete MyApp.Resources, :complete_user_id
endController Implementation
Read Function (arity 2: conn, params)
For static resources, params is typically an empty map. For templated resources, params contains the template variable values:
defmodule MyApp.Resources do
import McpServer.Controller, only: [content: 3]
def read_user(conn, %{"id" => user_id}) do
user_data = fetch_user_from_database(user_id)
%{
"contents" => [
content(
"User " <> user_id,
"https://api.example.com/users/" <> user_id,
mimeType: "application/json",
text: Jason.encode!(user_data)
)
]
}
end
endComplete Function (arity 3: conn, variable_name, prefix)
Provides completion suggestions for URI template variables:
defmodule MyApp.Resources do
import McpServer.Controller, only: [completion: 2]
def complete_user_id(conn, "id", prefix) do
# Fetch matching user IDs from your data source
matching_ids = search_user_ids(prefix)
completion(matching_ids, total: 1000, has_more: true)
end
endGenerated Functions
Using McpServer.Router generates the following functions in your module:
list_tools/1- Returns all defined tools with their schemascall_tool/3- Executes a tool by name with argumentsprompts_list/1- Returns all defined promptsget_prompt/3- Gets prompt messages for given argumentscomplete_prompt/4- Gets completion suggestions for prompt argumentslist_resources/1- Returns all static resourceslist_templates_resource/1- Returns all templated resourcesread_resource/3- Reads a resource by namecomplete_resource/4- Gets completion suggestions for resource URIs
All generated functions require a McpServer.Conn as their first parameter.
Validation
The Router performs compile-time validation:
- Controller modules must exist
- Controller functions must be exported with correct arity
- Tool/prompt/resource names must be unique
- Field names within a tool must be unique
- Required fields must be properly defined
- Resource templates with completion must be valid
Validation errors are raised as CompileError with helpful messages.
Example: Complete Router
defmodule MyApp.MCP do
use McpServer.Router
# Simple echo tool
tool "echo", "Echoes back the input", MyApp.Tools, :echo do
input_field("message", "Message to echo", :string, required: true)
output_field("response", "Echoed message", :string)
end
# Tool with hints and validation
tool "database_query", "Queries the database", MyApp.Tools, :query,
hints: [:closed_world, :idempotent] do
input_field("table", "Table name", :string,
required: true,
enum: ["users", "posts", "comments"])
input_field("limit", "Max results", :integer, default: 10)
output_field("results", "Query results", :array)
end
# Greeting prompt
prompt "greet", "A friendly greeting" do
argument("name", "Person's name", required: true)
get MyApp.Prompts, :get_greeting
complete MyApp.Prompts, :complete_name
end
# Static resource
resource "config", "file:///etc/app/config.json" do
description "Application configuration"
mimeType "application/json"
read MyApp.Resources, :read_config
end
# Dynamic resource
resource "document", "file:///docs/{category}/{id}.md" do
description "Documentation files"
mimeType "text/markdown"
read MyApp.Resources, :read_document
complete MyApp.Resources, :complete_document_path
end
endSee Also
McpServer- The behaviour implemented by routersMcpServer.Conn- Connection context structureMcpServer.Controller- Helper functions for controllersMcpServer.HttpPlug- HTTP transport for MCP servers
Summary
Functions
Defines a nested field within an object or array block.
Defines the item schema for an array field.
Defines a prompt
Defines a resource with a URI and an optional block to describe metadata and handlers.
Same as tool/6 but with no options provided such as title or hints.
Defines a tool
Functions
Defines a nested field within an object or array block.
Examples
# Simple nested field
field("name", "User name", :string, required: true)
# Nested object
field("address", "Address", :object) do
field("city", "City", :string)
field("country", "Country", :string)
end
# Nested array with simple items
field("tags", "Tags", :array, items: :string)
# Nested array with complex items
field("contacts", "Contact list", :array) do
items :object do
field("type", "Contact type", :string)
field("value", "Contact value", :string)
end
end
Defines the item schema for an array field.
Examples
# Simple item type
items :string
# Complex item type with nested structure
items :object do
field("id", "Item ID", :string)
field("value", "Item value", :number)
end
# Nested array items
items :array, items: :string
Defines a prompt
Example
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
Defines a resource with a URI and an optional block to describe metadata and handlers.
Example
resource "users", "https://example.com/users/{id}" do
description "List of users"
mimeType "application/json"
title "User resource"
read MyApp.ResourceController, :read_user
complete MyApp.ResourceController, :complete_user
end
Same as tool/6 but with no options provided such as title or hints.
Defines a tool
Example
tool "echo", "Echoes back the input", EchoController, :echo,
title: "Echo",
hints: [:read_only, :non_destructive, :idempotent, :closed_world] do
input_field("message", "The message to echo", :string, required: true)
output_field("message", "The echoed message", :string)
end