Skip to main content

guides/tools-resources-prompts.md

# Tools, Resources & Prompts

The 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

```erlang
-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:

```erlang
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:

```erlang
%% 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:

```erlang
%% (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/progress` updates,
- 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 `_meta` extension hook (available
  as `maps: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`.

```erlang
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`:

```erlang
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.

```erlang
barrel_mcp:reg_tool(<<"search">>, my_tools, search, #{
    title => <<"Knowledge Base Search">>,
    icons => [#{<<"src">> => <<"https://example.com/icon.png">>,
                 <<"sizes">> => <<"32x32">>}]
}).
```

### Managing Tools

```erlang
%% 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

```erlang
-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:

```erlang
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:

```erlang
get_logo(_Args) ->
    #{
        blob => read_file("logo.png"),
        mimeType => <<"image/png">>
    }.
```

Register:

```erlang
barrel_mcp:reg_resource(<<"logo">>, my_resources, get_logo, #{
    name => <<"Company Logo">>,
    uri => <<"assets://logo">>,
    mime_type => <<"image/png">>
}).
```

### Managing Resources

```erlang
%% 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

```erlang
-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:

```erlang
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:

```erlang
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

```erlang
%% 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:

```erlang
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:

```erlang
%% 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:

```erlang
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 another `completion/complete` to 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:

```erlang
{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

```erlang
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

```erlang
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

```erlang
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:

```erlang
%% @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.
```