The DSL mode uses compile-time macros to define tools, resources, and prompts inline in a single server module. Schemas, validation, and handler routing are generated automatically.

Setup

defmodule MyApp.MCPServer do
  use ConduitMcp.Server
  # Everything goes here
end

Defining Tools

tool "create_user", "Create a new user" do
  param :name, :string, "Full name", required: true, min_length: 2, max_length: 50
  param :age, :integer, "Age", min: 0, max: 150
  param :role, :string, "User role", enum: ["admin", "user", "guest"], default: "user"
  param :score, :number, "Score", min: 0.0, max: 100.0, default: 50.0

  handle fn _conn, params ->
    # params is a string-keyed map: %{"name" => "Alice", "age" => 30, ...}
    text("Created user: #{params["name"]}")
  end
end

Handler Styles

# Anonymous function
handle fn _conn, params -> text("ok") end

# Function capture
handle &MyModule.my_function/2

# Module + function reference
handle MyModule, :my_function

Tool Annotations

tool "fetch_data", "Fetch data" do
  annotations destructive: false, idempotent: true, readOnlyHint: true
  param :url, :string, "URL", required: true
  handle fn _conn, params -> text("fetched") end
end

OAuth Scopes

tool "delete_user", "Delete a user" do
  scope "users:delete"
  param :id, :string, "User ID", required: true
  handle fn _conn, %{"id" => id} -> text("deleted #{id}") end
end

Defining Prompts

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 #{args["language"]} code reviewer"),
      user("Review this code:\n#{args["code"]}")
    ]
  end
end

Defining Resources

resource "user://{id}" do
  description "User profile by ID"
  mime_type "application/json"

  read fn _conn, params, _opts ->
    user = MyApp.Users.get!(params["id"])
    json(user)
  end
end

resource "static://readme" do
  description "Project README"
  mime_type "text/markdown"

  read fn _conn, _params, _opts ->
    text(File.read!("README.md"))
  end
end

Response Helpers

All of these are macros imported automatically:

HelperReturnsExample
text(string){:ok, %{"content" => [%{"type" => "text", ...}]}}text("Hello!")
json(data)JSON-encoded text contentjson(%{status: "ok"})
error(msg){:error, %{"code" => -32000, ...}}error("Not found")
error(msg, code)Custom error codeerror("Bad params", -32602)
image(url)Image contentimage(base64_data)
audio(data, mime)Audio contentaudio(data, "audio/wav")
raw(data)Bypass MCP wrappingraw(%{"custom" => true})
system(text)System role messageFor prompts
user(text)User role messageFor prompts
assistant(text)Assistant role messageFor prompts

Validation

Validation is generated automatically from param options. See the README for the full constraints table.

Object and Array Parameters

tool "create_user", "Create a user" do
  param :user, :object, "User data", required: true do
    field :name, :string, "Full name", required: true
    field :address, :object, "Address" do
      field :city, :string, "City", required: true
    end
  end

  param :tags, :array, "Tags" do
    items :string
  end

  handle fn _conn, params ->
    text(params["user"]["address"]["city"])
  end
end

Nested fields are enforced to any depth and undeclared keys are rejected. Drop the block for an open object (any keys accepted), or pass additional_properties: true to enforce declared fields and pass the rest through. Array item schemas are published for clients but not enforced server-side. Handlers receive string keys at every depth.

Custom Validators

# Function
param :email, :string, "Email", validator: fn email ->
  String.match?(email, ~r/@/)
end

# Built-in
param :email, :string, "Email", validator: &ConduitMcp.Validation.Validators.email/1

# MFA tuple
param :priority, :string, "Priority", validator: {MyApp.Validators, :valid_priority?}