Behaviour for a repository of prompts served by a GenMCP.Suite.
Prompts are reusable message templates a client can fetch and replay to an
LLM. A prompt repository groups related prompts under a common name prefix,
answers prompts/list to advertise them, and answers prompts/get to build a
concrete prompt from the arguments the client supplies.
The prefix is what ties the two requests together. The Suite routes a
prompts/get to the repository whose prefix the requested name starts with,
so every name a repository lists must begin with its prefix, and the prefixes
of the configured repositories must not collide.
A repository is a thin adapter: keep the prompt text and argument shapes in a
plain module of your own, and let the callbacks route to it. The three
required callbacks answer one question each, which prefix the prompts live
under (prefix/1), which prompts exist (list/3), and how to build one
(get/4).
Minimal implementation
A repository that serves a single greeting prompt under the support/
prefix. list/3 advertises it, and get/4 fills in the customer name the
client passed, building the result with the GenMCP.MCP.V2607 helpers:
defmodule MyApp.SupportPrompts do
@behaviour GenMCP.Suite.PromptRepo
alias GenMCP.MCP.V2607, as: MCP
@impl true
def prefix(_arg), do: "support/"
@impl true
def list(_cursor, _channel, _arg) do
prompts = [
%{
name: "support/greeting",
description: "Greet a customer by name",
arguments: [%{name: "name", description: "Customer name", required: true}]
}
]
{prompts, nil}
end
@impl true
def get("support/greeting", %{"name" => name}, _channel, _arg) do
result =
MCP.get_prompt_result(
description: "Greet a customer by name",
text: "Greet #{name} warmly and ask how you can help."
)
{:ok, result}
end
def get(_name, _arguments, _channel, _arg), do: {:error, :not_found}
endWiring a repository into the server
A repository is given to the Suite through its :prompts option. Because the
Suite is the default :server, those options are passed straight to the
transport plug in your router. Each entry is a bare module or a {module, arg}
tuple, where arg is handed back to every callback as the trailing argument:
# In your router
forward "/mcp", GenMCP.Transport.StreamableHTTP,
server_name: "My App",
server_version: "1.0.0",
prompts: [MyApp.SupportPrompts]Provider arguments
The arguments the callbacks receive follow the conventions shared by all Suite
providers, documented in GenMCP.Suite:
channelis the request-scopedGenMCP.Mux.Channel.t/0, carrying read-only clientmetaand auth assigns.list/3andget/4receive it as the second-to-last argument, so a repository can tailor what it exposes to the caller.argis the value configured alongside the module as{module, arg}(a bare module is treated as{module, []}). It is the trailing argument of every callback, letting one generic repository module be configured differently in different Suites.
Summary
Callbacks
Returns the cache hint for this repository's prompt listing.
Builds the prompt identified by name for prompts/get.
Lists the prompts this repository advertises for prompts/list.
Returns the name prefix shared by this repository's prompts.
Types
@type arg() :: term()
@type prompt_item() :: %{ :name => String.t(), optional(:title) => String.t(), optional(:description) => String.t(), optional(:arguments) => [prompt_argument()] }
@type prompt_repo() :: module() | {module(), arg()} | prompt_repo_descriptor()
Callbacks
@callback cache_control(arg()) :: {:public | :private, non_neg_integer()}
Returns the cache hint for this repository's prompt listing.
This optional callback sets how prompts/list results from the repository may
be cached. Return {scope, ttl_ms}, where scope is :public or :private
and ttl_ms is a non-negative lifetime in milliseconds. When the callback is
not implemented, the Suite uses the no-cache default from
GenMCP.MCP.V2607.default_cache_control/0.
@impl true
def cache_control(_arg), do: {:public, 60_000}
@callback get( name :: String.t(), arguments :: %{required(binary()) => term()}, GenMCP.Mux.Channel.t(), arg() ) :: {:ok, GenMCP.MCP.V2607.GetPromptResult.t()} | {:error, :not_found | String.t()}
Builds the prompt identified by name for prompts/get.
name is the full name the client requested, including the repository prefix.
arguments is the map of argument values the client supplied, keyed by string.
The arguments are passed through as is, they are not validated against the
prompt's declared arguments, so match and check the keys you need.
Return {:ok, result} where result is a
GenMCP.MCP.V2607.GetPromptResult.t/0 built with
GenMCP.MCP.V2607.get_prompt_result/1. Return {:error, :not_found} when the
name does not match a prompt this repository serves, or {:error, message}
with a string message to report an invalid request to the client.
channel is the request-scoped GenMCP.Mux.Channel.t/0, and arg is the
configured value.
Match the name and required arguments in the head, and build the messages with
the GenMCP.MCP.V2607 helpers:
@impl true
def get("support/greeting", %{"name" => name}, _channel, _arg) do
result =
GenMCP.MCP.V2607.get_prompt_result(
description: "Greet a customer by name",
text: "Greet #{name} warmly and ask how you can help."
)
{:ok, result}
end
def get(_name, _arguments, _channel, _arg), do: {:error, :not_found}
@callback list(pagination_token :: String.t() | nil, GenMCP.Mux.Channel.t(), arg()) :: {[prompt_item()], next_cursor :: term() | nil}
Lists the prompts this repository advertises for prompts/list.
Returns a {prompts, next_cursor} tuple. Each element of prompts is a
prompt_item/0 map describing one prompt, whose :name must begin with the
repository's prefix/1. next_cursor carries pagination: return nil on
the last page, or an opaque token that the Suite hands back as the
pagination_token of the next call to fetch the following page.
The pagination_token is nil on the first call. channel is the
request-scoped GenMCP.Mux.Channel.t/0, and arg is the configured value.
A single-page repository ignores the token and returns nil as the cursor:
@impl true
def list(_cursor, _channel, _arg) do
prompts = [
%{name: "support/greeting", description: "Greet a customer by name"}
]
{prompts, nil}
endTo paginate, return a token on every page but the last, and resume from it on the next call:
@impl true
def list(nil, _channel, _arg), do: {first_page(), "page-2"}
def list("page-2", _channel, _arg), do: {second_page(), nil}
Returns the name prefix shared by this repository's prompts.
Every name that list/3 returns must start with this prefix, because the
Suite uses it to route a prompts/get to the right repository: it picks the
repository whose prefix the requested name starts with. The prefixes of the
configured repositories must therefore be distinct. arg is the value
configured alongside the module.
@impl true
def prefix(_arg), do: "support/"
Functions
Returns the cache hint for the repository's prompt listing.
Given a descriptor from expand/1, calls the optional cache_control/1
callback when the repository module exports it, returning its {scope, ttl_ms}
hint. When the callback is not implemented, returns the no-cache default from
GenMCP.MCP.V2607.default_cache_control/0. GenMCP.Suite uses this to set the
cache hints on a prompts/list result.
@spec expand(prompt_repo()) :: prompt_repo_descriptor()
Normalizes a prompt repository spec into a descriptor map.
Accepts the three forms a repository may be configured as and returns a
prompt_repo_descriptor/0, the %{mod: module, arg: term, prefix: binary}
shape the Suite works with internally:
- a bare
module, treated as{module, []}, - a
{module, arg}tuple, - an already-built descriptor map, returned unchanged.
For the bare module and tuple forms the module is loaded with
Code.ensure_loaded!/1 and its prefix/1 is called to fill in the
descriptor's prefix. This raises if the module does not exist, or if
prefix/1 returns a value that is not a string.
GenMCP.Suite calls this when it gathers the prompt repositories to serve.
Invokes the repository's get/4 to build the prompt named name.
Given a descriptor from expand/1, calls the repository module's get/4
with name, the client arguments, the request channel, and the
descriptor's arg, then normalizes the result. A {:error, :not_found} from
the callback becomes {:error, {:prompt_not_found, name}}; a string error and
an invalid-params error are passed through. GenMCP.Suite uses this to answer
a prompts/get.
Invokes the repository's list/3 to list its prompts.
Given a descriptor from expand/1, calls the repository module's list/3
with the pagination cursor, the request channel, and the descriptor's
arg, and returns the {prompts, next_cursor} tuple it produces.
GenMCP.Suite uses this to answer a prompts/list.