ExMCP Server DSL Guide
View SourceExMCP's server DSL defines MCP tools, resources, resource templates, and prompts
next to the functions that handle them. Use it with ExMCP.Server.Handler:
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
endThis 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. Use transport: :http
with sse_enabled: true when serving HTTP responses with SSE streaming.
Tools
Tools declare input metadata and a run handler:
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
endTool handlers can return a string, %{text: text}, a full MCP tool result, or
{:error, reason}. Declared params are normalized so handlers can use atom keys
and defaults.
Resources
Static resources use resource and a read handler:
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
endResource templates use URI variables and optional typed params:
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
endTemplate variables are available as atom and string keys.
Prompts
Prompts declare arguments and a render handler:
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
endReturning a string creates a single user text message.
Metadata
The DSL supports spec-aligned metadata on declarations:
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
endUse title for display names. Custom extension data belongs under _meta via
meta.
Starting Servers
For the generated DSL server:
{: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:
{: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.
Migration From The Removed Legacy DSL
The former use ExMCP.Server macro and deftool, defresource, and
defprompt declarations have been removed. Migrate by:
- Replacing
use ExMCP.Serverwithuse ExMCP.Server.Handleranduse ExMCP.Server.DSL. - Replacing
deftoolblocks withtoolblocks and colocatedrunhandlers. - Replacing
defresourceblocks withresourceorresource_templateblocks and colocatedreadhandlers. - Replacing
defpromptblocks withpromptblocks and colocatedrenderhandlers. - Replacing the removed
ExMCP.Server.start_linkhelper withMyServer.start_link/1,ExMCP.Server.HandlerServer.start_link/1, orExMCP.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.