# `ConduitMcp.DSL`
[🔗](https://github.com/nyo16/conduit_mcp/blob/v0.10.1/lib/conduit_mcp/dsl.ex#L1)

DSL for defining MCP servers with a clean, declarative syntax.

The DSL provides macros for defining tools, prompts, and resources
without manually building JSON schemas and callback functions.

## Example

    defmodule MyApp.MCPServer do
      use ConduitMcp.Server

      tool "greet", "Greets a person" do
        param :name, :string, "Name to greet", 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

      tool "calculate", "Math operations" do
        param :op, :string, "Operation", enum: ~w(add sub mul div), required: true
        param :a, :number, "First number", required: true
        param :b, :number, "Second number", required: true

        handle MyMath, :calculate  # MFA
      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
    end

The DSL automatically generates:
- Tool/prompt/resource schemas
- Input validation (JSON Schema)
- handle_list_* callbacks
- handle_call_tool/handle_get_prompt/handle_read_resource callbacks

# `annotations`
*macro* 

Sets annotations for a tool.

Annotations provide hints about tool behavior to clients.

## Options

- `:read_only` - Tool doesn't modify state (default: not set)
- `:destructive` - Tool may perform destructive operations (default: not set)
- `:idempotent` - Calling multiple times has same effect (default: not set)
- `:open_world` - Tool may interact with external systems (default: not set)

## Example

    tool "delete_user", "Deletes a user" do
      annotations destructive: true, idempotent: true
      param :id, :string, "User ID", required: true
      handle fn _conn, %{"id" => id} -> ... end
    end

# `app`
*macro* 

Defines an MCP App — a tool with a linked UI resource.

The `app` macro is a convenience that registers both:
1. A **tool** with `_meta.ui.resourceUri` pointing to a `ui://` resource
2. A **resource** that serves the HTML file at that URI

Use `view/1` inside the block to specify the HTML file path.
The `ui://` URI is derived automatically from the tool name and filename.

## Example

    app "dashboard", "Health dashboard" do
      param :format, :string, "Output format", default: "json"
      view "priv/mcp_apps/dashboard.html"

      handle fn _conn, params ->
        json(%{cpu: 42, memory: 128})
      end
    end

This is equivalent to:

    tool "dashboard", "Health dashboard" do
      ui "ui://dashboard/dashboard.html"
      param :format, :string, "Output format", default: "json"
      handle fn _conn, params -> json(%{cpu: 42, memory: 128}) end
    end

    resource "ui://dashboard/dashboard.html" do
      description "UI for dashboard"
      mime_type "text/html;profile=mcp-app"
      read fn _conn, _params, _opts ->
        app_html(File.read!("priv/mcp_apps/dashboard.html"))
      end
    end

> **Note:** The `view` path is read with `File.read!/1` at runtime. For OTP
> releases where the working directory differs, use a manual `tool` + `resource`
> pair with `Application.app_dir/2` instead.

# `arg`
*macro* 

Defines a prompt argument.

## Examples

    arg :code, :string, "Code to review", required: true
    arg :language, :string, "Programming language", default: "elixir"

# `compile_uri_template`

Pre-compiles a URI template into a regex and parameter name list.

Returns `{param_names, compiled_regex}` suitable for passing to
`extract_uri_params_compiled/3`. Call this at compile time and store
the result to avoid re-compiling the regex on every request.

# `complete`
*macro* 

Defines an autocomplete handler for a prompt argument.

## Example

    complete :language, fn _conn, prefix ->
      ~w(elixir python javascript rust)
      |> Enum.filter(&String.starts_with?(&1, prefix))
    end

# `description`
*macro* 

Sets the description for a resource.

## Example

    resource "user://{id}" do
      description "User profile information"
      read MyUsers, :read
    end

# `extract_uri_params_compiled`

Matches a URI against a pre-compiled template regex.

Uses the output of `compile_uri_template/1` to avoid runtime regex compilation.

# `field`
*macro* 

Defines a nested field within an object parameter.

## Examples

    param :user, :object, "User data" do
      field :name, :string, "Name", required: true
      field :email, :string, "Email", required: true
      field :address, :object, "Address" do
        field :city, :string, "City"
        field :zip, :string, "Zip code"
      end
    end

# `field`
*macro* 

# `get`
*macro* 

Defines the get handler for a prompt.

The handler should return a list of message objects.

## Examples

    get fn _conn, args ->
      [
        system("You are helpful"),
        user("Question: #{args["question"]}")
      ]
    end

    get MyPrompts, :get_review

# `get`
*macro* 

# `handle`
*macro* 

Defines the handler function for a tool.

Accepts either an anonymous function or an MFA tuple.

## Examples

    # Anonymous function
    handle fn _conn, params ->
      text("Result: #{params["input"]}")
    end

    # Module, function
    handle MyModule, :my_function

    # Function capture
    handle &MyModule.my_function/2

# `handle`
*macro* 

# `icons`
*macro* 

Sets icon descriptors for a tool. Added in MCP spec 2025-11-25.

Accepts a list of icon maps, each with `src` (URL or data URI),
optional `mimeType`, `width`, `height`. Clients use these to render
the tool in their UI.

## Example

    tool "search", "Search the catalog" do
      icons [%{"src" => "https://example.com/search.svg", "mimeType" => "image/svg+xml"}]
      handle MyCatalog, :search
    end

# `items`
*macro* 

Defines the item type for an array parameter.

Item schemas are published to clients in the JSON Schema but are **not**
enforced server-side — NimbleOptions cannot attach a nested schema to a list
element type. Validate item contents in your handler.

## Examples

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

    param :users, :array, "Users" do
      items :object do
        field :name, :string, "Name"
        field :email, :string, "Email"
      end
    end

# `items`
*macro* 

# `meta`
*macro* 

Attaches arbitrary `_meta` metadata to a tool definition.

The `_meta` field is an open extension point in the MCP spec. Any map
can be passed — atom keys are automatically converted to string keys
in the JSON output.

## Example

    tool "dashboard", "Health dashboard" do
      meta %{ui: %{resourceUri: "ui://dashboard/app.html"}}
      handle fn _conn, _params -> json(%{ok: true}) end
    end

The tool's `tools/list` entry will include:

    %{
      "name" => "dashboard",
      "_meta" => %{"ui" => %{"resourceUri" => "ui://dashboard/app.html"}}
    }

# `mime_type`
*macro* 

Sets the MIME type for a resource.

## Example

    resource "file://{path}" do
      mime_type "text/plain"
      read MyFiles, :read
    end

# `output_schema`
*macro* 

Sets the JSON Schema for the tool's structured output.

When a tool returns a structured payload (via `structured/2` helper
or by including `"structuredContent"` in the result map), this schema
describes its shape so clients can render/validate it. Added in MCP
spec 2025-11-25.

## Example

    tool "get_user", "Fetch a user" do
      param :id, :string, "User id", required: true
      output_schema %{
        "type" => "object",
        "properties" => %{
          "id" => %{"type" => "string"},
          "email" => %{"type" => "string"}
        },
        "required" => ["id", "email"]
      }
      handle MyUsers, :get
    end

# `param`
*macro* 

Defines a parameter for a tool.

## Basic Options

- `:required` - Mark parameter as required (default: false)
- `:enum` - List of allowed values
- `:default` - Default value if not provided

## Enhanced Validation Options (NimbleOptions)

- `:min` - Minimum value for numbers (inclusive)
- `:max` - Maximum value for numbers (inclusive)
- `:min_length` - Minimum string length
- `:max_length` - Maximum string length
- `:validator` - Custom validation function `fn(value) -> boolean()`

## Object Options

- `:additional_properties` - Whether keys the block did not declare are
  accepted. Defaults to `false` when the object declares fields and `true`
  when it does not. Also drives `"additionalProperties"` in the generated
  JSON Schema, so the published schema always matches what is enforced.

## Object and Array Parameters

Pass a block to declare structure. Nested fields are enforced at runtime to
any depth — required, types, and every constraint above — and undeclared keys
are rejected. `opts` may be omitted; `param :bag, :object do ... end` and
`param :bag, :object, "desc" do ... end` both work.

    # Nested object, enforced
    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

    # Open object — any keys, nothing enforced
    param :metadata, :object, "Arbitrary metadata"

    # Declared fields enforced, everything else passed through
    param :bag, :object, "Bag", additional_properties: true do
      field :name, :string, "Name", required: true
    end

    # Array item type
    param :rows, :array, "Rows" do
      items :object do
        field :id, :integer, "Row id", required: true
      end
    end

Array item schemas are published for clients but not enforced server-side:
NimbleOptions cannot attach a nested schema to a list element type.

Handlers always receive string keys, at every depth.

## Examples

    # Basic validation
    param :name, :string, "User name", required: true
    param :role, :string, "User role", enum: ["admin", "user", "guest"]
    param :active, :boolean, "Active status", default: true

    # Enhanced validation
    param :age, :integer, "Age in years", min: 0, max: 150, required: true
    param :username, :string, "Username", min_length: 3, max_length: 30
    param :score, :number, "Score", min: 0.0, max: 100.0, default: 50.0
    param :email, :string, "Email", validator: &ConduitMcp.Validation.Validators.email/1

    # Complex validation with multiple constraints
    param :priority, :string, "Task priority",
      required: true,
      enum: ["low", "medium", "high", "critical"],
      validator: &MyApp.validate_priority/1

    # Array with validation
    param :tags, {:array, :string}, "Tags", max_length: 10

# `param`
*macro* 

Defines a parameter with nested fields (for objects) or items (for arrays).

# `precompile_template_regex`

Stores a compiled URI template regex in `:persistent_term` so that it can
be looked up at request time without paying the per-process
`Regex.recompile/1` cost incurred when a `Regex` struct is embedded via
`Macro.escape/1`.

Called at module load time from generated `@on_load` hooks.

# `prompt`
*macro* 

Defines an MCP prompt.

## Example

    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 code: #{args["code"]}")
        ]
      end

      complete :language, fn _conn, prefix ->
        ~w(elixir python javascript) |> Enum.filter(&String.starts_with?(&1, prefix))
      end
    end

# `read`
*macro* 

Defines the read handler for a resource.

Handler signature: `(conn, uri_params, opts) -> result`

## Examples

    read fn _conn, %{"id" => id}, _opts ->
      user = MyApp.Users.get!(id)
      json(user)
    end

    read MyFiles, :read

# `read`
*macro* 

# `resource`
*macro* 

Defines an MCP resource.

## Examples

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

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

    resource "file://{path}" do
      mime_type "text/plain"
      read MyFiles, :read
      complete :path, &MyFiles.autocomplete/2
    end

# `scope`
*macro* 

Sets a required OAuth scope for a tool.

When OAuth authentication is enabled, the handler will check that the
token contains the required scope before executing the tool.

## Example

    tool "delete_user", "Deletes a user" do
      scope "users:write"
      param :id, :string, "User ID", required: true
      handle fn _conn, %{"id" => id} -> ... end
    end

Multiple scopes can be required by calling scope multiple times or
passing a space-separated string:

    tool "admin_action", "Admin only" do
      scope "admin users:write"
      handle fn _conn, _params -> ... end
    end

# `task_support`
*macro* 

Declares the tool's support for asynchronous task execution.

Values:

- `:none` (default, not emitted) — tool always returns synchronously
- `:supported` — tool MAY return a task id for long-running operations;
  clients can poll `tasks/get` and receive results via `tasks/result`
- `:required` — tool ALWAYS returns a task id; the immediate response
  contains no result, only a task id to poll

Added in MCP spec 2025-11-25.

## Example

    tool "render_video", "Render a video from a script" do
      task_support :supported
      param :script, :string, "Script source", required: true
      handle MyRenderer, :start
    end

# `template_regex`

Fetches a `{param_names, regex}` tuple previously stored by
`precompile_template_regex/2`, falling back to recompiling on cache miss
(defensive — should not normally happen).

# `title`
*macro* 

Sets a human-readable display title for a tool.

Distinct from `name` (the machine identifier used in `tools/call`),
`title` is shown to end users by MCP clients. Added in MCP spec
2025-11-25.

## Example

    tool "create_user", "Create a user account" do
      title "Create User"
      param :email, :string, "Email address", required: true
      handle MyUsers, :create
    end

# `tool`
*macro* 

Defines an MCP tool.

## Examples

    # Simple tool with inline handler
    tool "greet", "Greets someone" do
      param :name, :string, "Name", required: true
      handle fn _conn, %{"name" => n} -> text("Hello #{n}!") end
    end

    # Tool with MFA handler
    tool "calculate", "Calculator" do
      param :a, :number, required: true
      param :b, :number, required: true
      handle MyMath, :add
    end

    # Tool with nested object
    tool "create_user", "Creates user" do
      param :user, :object, "User data", required: true do
        field :name, :string, "Full name", required: true
        field :email, :string, "Email", required: true
      end
      handle MyUsers, :create
    end

# `ui`
*macro* 

Shortcut for declaring a UI resource URI on a tool (MCP Apps).

This is sugar for `meta %{ui: %{resourceUri: uri}}`. It links the tool
to an interactive HTML resource that MCP Apps-compatible hosts will
render as a sandboxed iframe.

## Example

    tool "dashboard", "Health dashboard" do
      ui "ui://dashboard/app.html"
      handle fn _conn, _params -> json(%{ok: true}) end
    end

See the [MCP Apps guide](guides/mcp_apps.md) for full details.

# `view`
*macro* 

Specifies the HTML file path for an MCP App.

Used inside an `app` block to set the HTML file that will be served
as the `ui://` resource.

## Example

    app "dashboard", "Health dashboard" do
      view "priv/mcp_apps/dashboard.html"
      handle fn _conn, _params -> json(%{ok: true}) end
    end

---

*Consult [api-reference.md](api-reference.md) for complete listing*
