McpServer.Router (HTTP MCP Server v0.6.0)

View Source

A 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
end

Connection 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
end

The connection provides:

  • session_id - Unique identifier for the current session
  • private - A map for storing custom data (accessible via helper functions)

Tools

Tools are functions that clients can invoke with validated inputs. Each tool requires:

  1. A unique name
  2. A description
  3. A controller module and function (arity 2: conn, args)
  4. 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)
end

Supported 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 set
  • default: value - Default value if not provided
  • items: :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
end

Arrays 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)
end

Arrays 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
end

Complex 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
end

Tool 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...
end

Available 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
end

Prompts

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
end

Controller 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
end

Complete 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([], [])
end

Resources

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
end

Templated 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
end

Controller 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
end

Complete 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
end

Generated Functions

Using McpServer.Router generates the following functions in your module:

  • list_tools/1 - Returns all defined tools with their schemas
  • call_tool/3 - Executes a tool by name with arguments
  • prompts_list/1 - Returns all defined prompts
  • get_prompt/3 - Gets prompt messages for given arguments
  • complete_prompt/4 - Gets completion suggestions for prompt arguments
  • list_resources/1 - Returns all static resources
  • list_templates_resource/1 - Returns all templated resources
  • read_resource/3 - Reads a resource by name
  • complete_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
end

See Also

Summary

Functions

Defines a nested field within an object or array block.

Defines the item schema for an array field.

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.

Functions

field(name, description, type)

(macro)

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

field(name, description, type, opts)

(macro)

field(name, description, type, opts, list)

(macro)

items(type)

(macro)

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

items(type, opts)

(macro)

items(type, opts, list)

(macro)

prompt(name, description, list)

(macro)

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

resource(name, uri)

(macro)

resource(name, uri, list)

(macro)

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

tool(name, description, controller, function, list)

(macro)

Same as tool/6 but with no options provided such as title or hints.

tool(name, description, controller, function, opts, list)

(macro)

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