Tools, Resources & Prompts
View SourceThe Model Context Protocol defines three core primitives for exposing functionality to AI assistants. barrel_mcp provides a simple, consistent API for all three.
Tools
Tools are functions that the AI can call to perform actions or retrieve information.
Registering a Tool
-module(my_tools).
-export([search/1, calculate/1]).
%% Simple tool returning text
search(Args) ->
Query = maps:get(<<"query">>, Args),
%% Return a binary string
<<"Results for: ", Query/binary>>.
%% Tool returning structured data (auto-converted to JSON)
calculate(Args) ->
A = maps:get(<<"a">>, Args),
B = maps:get(<<"b">>, Args),
Op = maps:get(<<"op">>, Args, <<"add">>),
Result = case Op of
<<"add">> -> A + B;
<<"sub">> -> A - B;
<<"mul">> -> A * B;
<<"div">> -> A / B
end,
#{<<"result">> => Result, <<"operation">> => Op}.Register with full schema:
barrel_mcp:reg_tool(<<"search">>, my_tools, search, #{
description => <<"Search for information">>,
input_schema => #{
<<"type">> => <<"object">>,
<<"properties">> => #{
<<"query">> => #{
<<"type">> => <<"string">>,
<<"description">> => <<"Search query">>
}
},
<<"required">> => [<<"query">>]
}
}).
barrel_mcp:reg_tool(<<"calculate">>, my_tools, calculate, #{
description => <<"Perform arithmetic operations">>,
input_schema => #{
<<"type">> => <<"object">>,
<<"properties">> => #{
<<"a">> => #{<<"type">> => <<"number">>},
<<"b">> => #{<<"type">> => <<"number">>},
<<"op">> => #{
<<"type">> => <<"string">>,
<<"enum">> => [<<"add">>, <<"sub">>, <<"mul">>, <<"div">>],
<<"default">> => <<"add">>
}
},
<<"required">> => [<<"a">>, <<"b">>]
}
}).Tool Return Values
Tools can return any of:
%% Binary -> single text content block.
text_tool(_Args) ->
<<"Hello, World!">>.
%% Map -> JSON-encoded text content block.
json_tool(_Args) ->
#{<<"key">> => <<"value">>, <<"count">> => 42}.
%% List of content blocks -> verbatim.
multi_tool(_Args) ->
[
#{<<"type">> => <<"text">>, <<"text">> => <<"First result">>},
#{<<"type">> => <<"text">>, <<"text">> => <<"Second result">>}
].
%% Image content block.
image_tool(_Args) ->
ImageData = base64:encode(read_image_file()),
#{
<<"type">> => <<"image">>,
<<"data">> => ImageData,
<<"mimeType">> => <<"image/png">>
}.
%% Tool-level error: rendered as `{ "isError": true, "content": [...] }'
%% on the wire. Use this for failures that are part of the tool's
%% domain (validation, business rules) rather than infrastructure.
flaky_tool(_Args) ->
{tool_error, [#{<<"type">> => <<"text">>,
<<"text">> => <<"Quota exceeded">>}]}.
%% Structured output: machine-readable data plus optional human-readable
%% content blocks. Surfaces as `structuredContent' on the wire.
%% Pair with the `output_schema' option to validate the data shape.
weather(_Args) ->
{structured, #{<<"tempF">> => 72, <<"sky">> => <<"clear">>},
[#{<<"type">> => <<"text">>, <<"text">> => <<"72°F, clear">>}]}.{tool_error, Content} and {structured, Data, Content} are the
recommended shapes. Plain returns still work; raised exceptions are
caught and surfaced as a JSON-RPC error to the client.
Async handlers (Ctx-aware)
Tools may be exported as arity 2 instead of arity 1. The second argument is a context map the runtime fills in:
%% (Args, Ctx) -> Result. Ctx holds:
%% session_id :: binary() | undefined,
%% request_id :: integer() | binary(),
%% progress_token :: binary() | undefined,
%% meta :: map(), %% inbound _meta from the request
%% emit_progress :: fun((Done, Total, Message | undefined) -> ok)
download(Args, Ctx) ->
Url = maps:get(<<"url">>, Args),
Emit = maps:get(emit_progress, Ctx),
Emit(0.0, 1.0, undefined),
{ok, Body} = fetch(Url),
Emit(1.0, 1.0, undefined),
Body.Arity-2 handlers are needed for tools that:
- emit
notifications/progressupdates, - cooperate with
notifications/cancelled(the worker receives{cancel, RequestId}in its mailbox), - need the calling session id for server→client primitives,
- read or echo the request's
_metaextension hook (available asmaps:get(meta, Ctx, #{})).
Arity-1 handlers continue to work; pick whichever arity you need.
Returning _meta on the response
Tool handlers may attach a _meta map to the response envelope
by returning one of the meta-bearing tuples:
{result_meta, Result, MetaMap}— plain result +_meta.{structured_meta, Data, Content, MetaMap}—structuredContent+_meta.{tool_error, Content, MetaMap}— error result +_meta.
Empty maps are omitted from the wire. The plain
{tool_error, Content} and {structured, Data, Content}
shapes still work; _meta is opt-in.
Long-running tools (tasks)
Set long_running => true on reg_tool/4 and the tool returns
immediately to the client with a taskId. The handler keeps
running in the background and the runtime stores its eventual
outcome on the task. Clients track progress via tasks/get,
tasks/list, or notifications/tasks/status.
barrel_mcp:reg_tool(<<"render_video">>, my_tools, render_video, #{
long_running => true,
description => <<"Render a video on the GPU farm">>
}).The same handler shape (arity 1 or arity 2) applies. Long-running handlers may emit progress just like any other tool.
Schema validation
Two opt-in flags on reg_tool/4:
barrel_mcp:reg_tool(<<"search">>, my_tools, search, #{
input_schema => #{<<"type">> => <<"object">>,
<<"required">> => [<<"query">>]},
output_schema => #{<<"type">> => <<"object">>,
<<"required">> => [<<"results">>]},
validate_input => true,
validate_output => true
}).validate_input checks the call's arguments against
input_schema before invoking the handler. validate_output
checks the structured Data from {structured, Data, _} returns
against output_schema. Failures surface to the client as
isError: true content. The validator subset is documented under
barrel_mcp_schema.
Metadata: title and icons
Every registration accepts a human-readable title and a list of
icons (each #{src, sizes?, mime_type?}). Both surface in the
matching */list response. Empty fields are omitted from the
wire.
barrel_mcp:reg_tool(<<"search">>, my_tools, search, #{
title => <<"Knowledge Base Search">>,
icons => [#{<<"src">> => <<"https://example.com/icon.png">>,
<<"sizes">> => <<"32x32">>}]
}).Managing Tools
%% List all tools
Tools = barrel_mcp:list_tools().
%% Call a tool locally (for testing)
Result = barrel_mcp:call_tool(<<"search">>, #{<<"query">> => <<"test">>}).
%% Unregister a tool
barrel_mcp:unreg_tool(<<"search">>).Resources
Resources expose data that the AI can read, like files or configuration.
Registering a Resource
-module(my_resources).
-export([get_config/1, get_users/1]).
%% Text resource
get_config(_Args) ->
<<"app_name=MyApp\nversion=1.0.0\ndebug=false">>.
%% JSON resource (map auto-encoded)
get_users(_Args) ->
#{
<<"users">> => [
#{<<"id">> => 1, <<"name">> => <<"Alice">>},
#{<<"id">> => 2, <<"name">> => <<"Bob">>}
]
}.Register resources:
barrel_mcp:reg_resource(<<"config">>, my_resources, get_config, #{
name => <<"Application Configuration">>,
uri => <<"config://app/settings">>,
description => <<"Current application settings">>,
mime_type => <<"text/plain">>
}).
barrel_mcp:reg_resource(<<"users">>, my_resources, get_users, #{
name => <<"User List">>,
uri => <<"app://users/list">>,
description => <<"All registered users">>,
mime_type => <<"application/json">>
}).Binary Resources
For binary data like images or files:
get_logo(_Args) ->
#{
blob => read_file("logo.png"),
mimeType => <<"image/png">>
}.Register:
barrel_mcp:reg_resource(<<"logo">>, my_resources, get_logo, #{
name => <<"Company Logo">>,
uri => <<"assets://logo">>,
mime_type => <<"image/png">>
}).Managing Resources
%% List all resources
Resources = barrel_mcp:list_resources().
%% Read a resource locally
Content = barrel_mcp:read_resource(<<"config">>).
%% Unregister
barrel_mcp:unreg_resource(<<"config">>).Prompts
Prompts are pre-defined conversation templates that the AI can use.
Registering a Prompt
-module(my_prompts).
-export([summarize/1, translate/1]).
summarize(Args) ->
Content = maps:get(<<"content">>, Args),
Style = maps:get(<<"style">>, Args, <<"concise">>),
#{
description => <<"Summarize the provided content">>,
messages => [
#{
role => <<"user">>,
content => #{
type => <<"text">>,
text => <<"Please summarize the following in a ",
Style/binary, " style:\n\n", Content/binary>>
}
}
]
}.
translate(Args) ->
Text = maps:get(<<"text">>, Args),
TargetLang = maps:get(<<"target_language">>, Args),
#{
description => <<"Translate text to another language">>,
messages => [
#{
role => <<"system">>,
content => #{
type => <<"text">>,
text => <<"You are a professional translator.">>
}
},
#{
role => <<"user">>,
content => #{
type => <<"text">>,
text => <<"Translate the following to ",
TargetLang/binary, ":\n\n", Text/binary>>
}
}
]
}.Register prompts:
barrel_mcp:reg_prompt(<<"summarize">>, my_prompts, summarize, #{
description => <<"Summarize content in various styles">>,
arguments => [
#{
name => <<"content">>,
description => <<"The content to summarize">>,
required => true
},
#{
name => <<"style">>,
description => <<"Summary style: concise, detailed, or bullet">>,
required => false
}
]
}).
barrel_mcp:reg_prompt(<<"translate">>, my_prompts, translate, #{
description => <<"Translate text to another language">>,
arguments => [
#{
name => <<"text">>,
description => <<"Text to translate">>,
required => true
},
#{
name => <<"target_language">>,
description => <<"Target language (e.g., Spanish, French)">>,
required => true
}
]
}).Multi-Turn Prompts
Create prompts with conversation history:
code_review(Args) ->
Code = maps:get(<<"code">>, Args),
Language = maps:get(<<"language">>, Args, <<"unknown">>),
#{
description => <<"Interactive code review session">>,
messages => [
#{
role => <<"system">>,
content => #{
type => <<"text">>,
text => <<"You are a senior ", Language/binary,
" developer performing a code review.">>
}
},
#{
role => <<"user">>,
content => #{
type => <<"text">>,
text => <<"Please review this code:\n\n```",
Language/binary, "\n", Code/binary, "\n```">>
}
},
#{
role => <<"assistant">>,
content => #{
type => <<"text">>,
text => <<"I'll analyze this code for:\n",
"1. Correctness\n2. Performance\n",
"3. Security\n4. Best practices\n\n",
"Let me start the review...">>
}
}
]
}.Managing Prompts
%% List all prompts
Prompts = barrel_mcp:list_prompts().
%% Get a prompt with arguments
PromptResult = barrel_mcp:get_prompt(<<"summarize">>, #{
<<"content">> => <<"Long text here...">>,
<<"style">> => <<"bullet">>
}).
%% Unregister
barrel_mcp:unreg_prompt(<<"summarize">>).Resource Templates
URI templates (RFC 6570) advertise families of resources without enumerating every URI. Register one handler per template:
barrel_mcp:reg_resource_template(<<"file">>, my_resources, read_file_uri, #{
name => <<"File Reader">>,
uri_template => <<"file:///{path}">>,
description => <<"Read any file on the local FS">>,
mime_type => <<"text/plain">>
}).resources/read against a templated URI is matched and routed
to the template handler automatically — RFC 6570 Level 1
substitutions (simple {var} expansion) cover what the spec's
reference examples use. The substituted variables are merged
into the handler's Args map under their template names:
%% file:///{path} matched against file:///etc/hosts
read_file_uri(#{<<"path">> := Path} = _Args) ->
file:read_file(<<"/", Path/binary>>).barrel_mcp:list_resource_templates/0 lists registrations.
Templates also surface on the wire via
resources/templates/list.
Completions
Completion handlers suggest values for a prompt argument or a resource-template argument. Register them keyed by the parent plus the argument name:
suggest_lengths(<<"sh">>, _Ctx) -> {ok, [<<"short">>]};
suggest_lengths(_, _Ctx) -> {ok, [<<"short">>, <<"medium">>, <<"long">>]}.
barrel_mcp:reg_completion(
{prompt, <<"summarize">>, <<"length">>},
my_completions, suggest_lengths, #{}).Handlers are arity 2: (PartialValue, Ctx). Return one of:
{ok, [Suggestion]}— full list.{ok, [Suggestion], #{has_more => true}}— more available; the client can issue anothercompletion/completeto drill in.
The completions capability is advertised in initialize as soon
as at least one completion handler is registered.
Tasks (long-running operations)
The barrel_mcp_tasks module backs the tasks/list, tasks/get,
tasks/cancel, and tasks/result MCP methods, plus the
notifications/tasks/status notifications.
You don't usually call this module directly: registering a tool
with long_running => true (see above) wires the lifecycle for
you. The collector process records the worker's eventual outcome
as a task transition (working → completed | failed | cancelled)
and emits the matching notification on the session's SSE channel.
Status values match the MCP 2025-11-25 wire vocabulary, and
createdAt / lastUpdatedAt are emitted as RFC 3339 strings.
Hosts that drive their own long-running operations outside the tool path can use the public API:
{ok, TaskId} = barrel_mcp_tasks:create(SessionId, <<"reindex">>, #{}),
%% later:
ok = barrel_mcp_tasks:finish(SessionId, TaskId, #{<<"reindexed">> => 12000}).Tasks are evicted from memory one hour after they reach a terminal state (completed / failed / cancelled).
Server → client notifications
Every notification the server can emit goes through the session's SSE channel:
| Notification | Façade |
|---|---|
notifications/resources/updated | barrel_mcp:notify_resource_updated/1,2. Subscriptions are scoped to the calling Mcp-Session-Id — when a client re-initializes (or its session expires) the new session id has no carry-over subscriptions, and the host must subscribe again. This matches the spec's session-lifecycle model. |
notifications/tools/list_changed<br>notifications/resources/list_changed<br>notifications/prompts/list_changed | barrel_mcp:notify_list_changed/1 (tool, resource, prompt). reg_tool/4/unreg_tool/1 and friends emit it automatically; call the façade if you mutate the catalogue out of band. |
notifications/progress | barrel_mcp:notify_progress/3,4 (or via Ctx from an arity-2 tool handler). |
notifications/tasks/status | Emitted by barrel_mcp_tasks on every status transition. |
notifications/message (logging) | Emitted by barrel_mcp_session when a host calls logger-style helpers. |
Handler Best Practices
1. Validate Input
my_tool(Args) ->
case maps:find(<<"required_field">>, Args) of
{ok, Value} when is_binary(Value), Value =/= <<>> ->
process(Value);
_ ->
error({invalid_input, <<"required_field is mandatory">>})
end.2. Handle Errors Gracefully
my_tool(Args) ->
try
do_risky_operation(Args)
catch
error:Reason ->
%% Log for debugging
logger:error("Tool failed: ~p", [Reason]),
%% Return user-friendly error
error({tool_error, <<"Operation failed, please try again">>})
end.3. Use Authentication Info
my_tool(Args) ->
case maps:get(<<"_auth">>, Args, undefined) of
#{subject := UserId, scopes := Scopes} ->
case lists:member(<<"admin">>, Scopes) of
true -> admin_operation(UserId, Args);
false -> user_operation(UserId, Args)
end;
undefined ->
public_operation(Args)
end.4. Return Consistent Types
Pick one return type per tool and document it:
%% @doc Always returns a map with status and data
my_tool(Args) ->
case process(Args) of
{ok, Data} ->
#{<<"status">> => <<"success">>, <<"data">> => Data};
{error, Reason} ->
#{<<"status">> => <<"error">>, <<"message">> => Reason}
end.