Packages
ex_mcp
1.4.0
1.7.0
1.6.2
1.6.1
retired
1.6.0
retired
1.5.0
retired
1.4.0
retired
1.3.0
retired
1.2.0
retired
1.1.1
retired
1.1.0
retired
1.0.0
retired
1.0.0-rc.8
retired
1.0.0-rc.7
retired
1.0.0-rc.6
retired
1.0.0-rc.5
retired
1.0.0-rc.4
retired
1.0.0-rc.2
retired
1.0.0-rc.1
retired
1.0.0-rc.0
retired
0.12.0
retired
0.11.0
retired
0.10.0
retired
0.9.2
retired
0.9.1
retired
0.9.0
retired
0.8.4
retired
0.8.3
retired
0.8.2
retired
0.8.1
retired
0.8.0
retired
0.7.4
retired
0.7.3
retired
0.7.1
retired
0.7.0
retired
0.1.0
retired
Elixir implementation of MCP and ACP. Build MCP clients/servers with tools, resources, prompts over stdio, HTTP/SSE, and BEAM. Control coding agents via ACP with adapters for Claude Code, Codex, and more.
Retired package: Security issue - Request cancellation not session-scoped (GHSA-22m7-hr37-pxg3). Upgrade to ex_mcp 1.6.2.
Current section
Files
Jump to
Current section
Files
docs/DSL_GUIDE.md
# ExMCP Server DSL Guide
ExMCP's server DSL defines MCP tools, resources, resource templates, and prompts
next to the functions that handle them. Use it with `ExMCP.Server.Handler`:
```elixir
defmodule MyServer do
use ExMCP.Server.Handler
use ExMCP.Server.DSL, name: "my-server", version: "1.0.0"
tool "echo", "Echo back the input" do
title "Echo"
param :message, :string, required: true, description: "Message to echo"
run fn %{message: message}, state ->
{:ok, "Echo: #{message}", state}
end
end
end
```
This generates the standard `ExMCP.Server.Handler` callbacks for listing and
dispatching declared capabilities. The generated `start_link/1` supports
`:beam`, `:test`, `:stdio`, and `:http` transports. Modern HTTP SSE streams are
owned by the POST request and require no server transport flag. The deprecated
2024-11-05 two-endpoint transport remains available with
`legacy_http_sse: true` throughout ExMCP 1.x.
## Tools
Tools declare input metadata and a `run` handler:
```elixir
tool "add", "Adds two numbers" do
title "Add"
param :a, :number, required: true
param :b, :number, required: true
annotations readOnlyHint: true
output_schema %{
type: "object",
properties: %{sum: %{type: "number"}},
required: ["sum"]
}
run fn %{a: a, b: b}, state ->
sum = a + b
{:ok, ToolResult.structured("#{sum}", %{sum: sum}), state}
end
end
```
### Param types
| DSL type | JSON Schema |
|----------|-------------|
| `:string` | `{"type": "string"}` |
| `:integer` | `{"type": "integer"}` |
| `:number` | `{"type": "number"}` |
| `:boolean` | `{"type": "boolean"}` |
| `:object` / `:map` | `{"type": "object"}` |
| `{:array, item_type}` | `{"type": "array", "items": ...}` |
```elixir
param :tags, {:array, :string}, default: []
param :scores, {:array, :number}, required: true
```
Bare `:array` is **not** valid — the item type is required so the generated
`inputSchema` is correct.
You can also pass a full JSON Schema with `input_schema` (DSL instruction,
snake_case). That becomes the MCP `inputSchema` field on the wire.
`input_schema` and `output_schema` are **JSON Schema 2020-12** documents
(`https://json-schema.org/draft/2020-12/schema`).
Declared params are normalized so handlers can use atom keys and defaults.
### Response helpers and normalization
`ToolResult` is an **alias** for `ExMCP.Server.DSL.Result`, injected only inside
modules that `use ExMCP.Server.DSL`. Outside those modules, use the fully
qualified module:
```elixir
ExMCP.Server.DSL.Result.structured("done", %{count: 1})
```
`ToolResult` provides `text/1`, `error/1`, and `structured/2`. The DSL also
normalizes several plain return shapes from `run` / `read` / `render`:
| Return from handler | Normalized result |
|---------------------|-------------------|
| `"hello"` | text content |
| `%{text: "hello"}` | text content |
| `%{content: [...]}` | used as-is (plus structured key cleanup) |
| `ToolResult.structured(text, map)` | text + `structuredContent` |
| `{:error, reason}` | tool/resource/prompt error shape |
| `{:ok, result}` or `{:ok, result, state}` | both accepted |
### Image, audio, and embedded resource results
A `run` handler may return several content blocks. Build image, audio, and
embedded-resource items with `ExMCP.Content` (base64 payload plus MIME type):
```elixir
tool "preview", "Return a thumbnail, clip, and attached spec" do
run fn _args, state ->
image = File.read!("priv/preview.png") |> Base.encode64()
audio = File.read!("priv/clip.mp3") |> Base.encode64()
{:ok,
%{
content: [
ExMCP.Content.image(image, "image/png"),
ExMCP.Content.audio(audio, "audio/mp3"),
ExMCP.Content.resource(%{
uri: "file:///spec.pdf",
name: "Spec",
mimeType: "application/pdf"
})
]
}, state}
end
end
{:ok, result} = ExMCP.Client.call_tool(client, "preview", %{})
```
`ExMCP.Content.image/2`, `ExMCP.Content.audio/2`, and `ExMCP.Content.resource/1` are protocol content
builders, not image-processing APIs.
## Compile-time checks
Invalid DSL declarations fail at **compile time** with file/line and a fix hint:
```elixir
# Missing handler
tool "echo" do
param :message, :string
end
# => tool "echo" must define `run` or `handle`, e.g. run fn args, state -> ...
# Bare :array
param :data, :array
# => Invalid param type :array ... Use {:array, item_type}, e.g. {:array, :string}
# Wrong instruction for the declaration kind
tool "echo" do
arg :message # arg is only valid on prompts
run fn _, s -> {:ok, "ok", s} end
end
# Duplicates
tool "echo" do ... end
tool "echo" do ... end
# => Duplicate tool "echo" declared 2 times
```
Other checks include unknown instructions (with suggestions for common
mistakes like `inputSchema` → `input_schema`), non-literal types, empty
names/URIs, and using `run`/`read`/`render`/`mime_type` in the wrong block.
## Resources
Static resources use `resource` and a `read` handler:
```elixir
resource "config://app", "Application configuration" do
title "App Config"
mime_type "application/json"
read fn %{uri: uri}, state ->
{:ok, %{uri: uri, text: Jason.encode!(%{enabled: true})}, state}
end
end
```
### Binary (blob) resources
Return `blob` (base64) instead of `text` for binary bodies. The DSL copies
`uri` and `mimeType` onto a `%{blob: ...}` map the same way it does for text:
```elixir
resource "asset://logo.png", "Product logo" do
mime_type "image/png"
read fn %{uri: uri}, state ->
blob = File.read!("priv/logo.png") |> Base.encode64()
{:ok, %{uri: uri, blob: blob, mimeType: "image/png"}, state}
end
end
{:ok, contents} = ExMCP.Client.read_resource(client, "asset://logo.png")
```
Resource templates use URI variables and optional typed params:
```elixir
resource_template "file:///{path}", "File contents" do
title "File"
mime_type "text/plain"
param :path, :string
read fn %{path: path}, state ->
{:ok, "contents for #{path}", state}
end
end
```
Template variables are available as atom and string keys.
## Prompts
Prompts declare arguments and a `render` handler:
```elixir
prompt "code_review", "Review code" do
title "Code Review"
arg :code, required: true, description: "Code to review"
render fn %{code: code}, state ->
{:ok,
%{
messages: [
%{role: "user", content: %{type: "text", text: "Review this code:\n#{code}"}}
]
}, state}
end
end
```
Returning a string creates a single user text message.
### Image and embedded resource prompt messages
`render` may put image or embedded-resource content on a message, not only
text:
```elixir
prompt "review_screenshot", "Review a screenshot" do
render fn _args, state ->
image = File.read!("priv/shot.png") |> Base.encode64()
{:ok,
%{
messages: [
%{role: "user", content: ExMCP.Content.image(image, "image/png")},
%{
role: "user",
content:
ExMCP.Content.resource(%{
uri: "file:///notes.md",
name: "Notes",
mimeType: "text/markdown"
})
}
]
}, state}
end
end
```
### Getting a prompt with no arguments
`ExMCP.Client.get_prompt/2` defaults arguments to `%{}`:
```elixir
{:ok, prompt} = ExMCP.Client.get_prompt(client, "review_screenshot")
{:ok, prompt} = ExMCP.Client.get_prompt(client, "code_review", %{"code" => "def add(a, b), do: a + b"})
```
## Metadata
The DSL supports spec-aligned metadata on declarations:
```elixir
tool "search", "Search documents" do
title "Search"
icons [%{src: "https://example.com/search.svg", mimeType: "image/svg+xml"}]
annotations readOnlyHint: true
meta %{"owner" => "docs"}
param :query, :string, required: true
run fn %{query: query}, state -> {:ok, "Searching #{query}", state} end
end
```
Use `title` for display names. Custom extension data belongs under `_meta` via
`meta`.
## Starting Servers
For the generated DSL server:
```elixir
{:ok, pid} = MyServer.start_link(transport: :test)
{:ok, pid} = MyServer.start_link(transport: :stdio)
{:ok, pid} = MyServer.start_link(transport: :http, port: 4000)
```
For a hand-written handler without the DSL:
```elixir
{:ok, pid} =
ExMCP.Server.HandlerServer.start_link(
transport: :test,
handler: MyHandler
)
```
`ExMCP.start_server/1` is also available as a top-level convenience wrapper for
`ExMCP.Server.HandlerServer.start_link/1`.
**Fast verification tip:** After `mix compile`, `mix examples.getting_started` runs a quick in-process demo of the DSL + client patterns shown throughout this guide (and in QUICKSTART.md).
## Deprecated: `ExMCP.Server.Tools`
`ExMCP.Server.Tools` and `ExMCP.Server.Tools.Simplified` are **deprecated** and
will be retained throughout 1.x, with removal planned for **2.0.0**. They only covered tools (not resources/prompts)
and overlapped with this DSL.
| Old (`Server.Tools`) | New (`Server.DSL`) |
|----------------------|--------------------|
| `use ExMCP.Server.Tools` | `use ExMCP.Server.DSL, name: "...", version: "..."` |
| `tool "name" do ... handle fn ... end end` | `tool "name" do ... run fn ... end end` |
| `handle fn args, state -> ... end` | `run fn args, state -> ... end` |
| (tools only) | also `resource`, `resource_template`, `prompt` |
Using the old modules prints a compile-time deprecation warning.
## Migration From The Removed Legacy DSL
The former `use ExMCP.Server` macro and `deftool`, `defresource`, and
`defprompt` declarations have been removed. Migrate by:
1. Replacing `use ExMCP.Server` with `use ExMCP.Server.Handler` and
`use ExMCP.Server.DSL`.
2. Replacing `deftool` blocks with `tool` blocks and colocated `run` handlers.
3. Replacing `defresource` blocks with `resource` or `resource_template` blocks
and colocated `read` handlers.
4. Replacing `defprompt` blocks with `prompt` blocks and colocated `render`
handlers.
5. Replacing the removed `ExMCP.Server.start_link` helper with `MyServer.start_link/1`,
`ExMCP.Server.HandlerServer.start_link/1`, or `ExMCP.start_server/1`.
Old generated getters such as `get_tools/0`, `get_resources/0`, and
`get_prompts/0` are no longer part of the server API. Use the standard handler
callbacks instead.