While GenMCP describes a behaviour to handle MCP request and notifications,
it can be complicated to organize your code and keep track of all incoming
HTTP connections for a single session.
GenMCP.Suite provides the following capabilities:
- An extensible behaviour system to define tools, resources repositories and prompts repositories.
- Support for extensions to accept third party modules (or your own) to provide predefined sets for tools and repositories.
- Automatic tracking of incoming HTTP connections and RPC message identifiers, with support for custom state through channel assigns.
- Direct support for asynchronous tools.
- Sampling and Elicitation are yet to come, but the foundation is ready.
Configuration
These options are passed to the transport plug
GenMCP.Transport.StreamableHTTP.
:server_name(String.t/0) - Required.:server_version(String.t/0) - Required.:server_title(String.t/0):tools- The list ofGenMCP.Suite.Toolimplementations that will be available in the server. List items can be either module names,{module, arg}tuples or a descriptor map. The default value is[].:resources- The list ofGenMCP.Suite.ResourceRepoimplementations to serve resources from. List items can be either module names,{module, arg}tuples or a descriptor map. The default value is[].:prompts- A list ofGenMCP.Suite.PromptRepoimplementations to generate prompts with. List items can be either module names,{module, arg}tuples or a descriptor map. The default value is[].:extensions- A listGenMCP.Suite.Extensionimplementations to add more tools, resource repositories and prompt repositories. List items can be either module names,{module, arg}tuples or a descriptor map. The default value is[].:session_controller- AGenMCP.Suite.SessionControllerimplementation The default value isnil.
Basic Example
# router.ex
forward "/", GenMCP.Transport.StreamableHTTP,
server_name: "My Server",
server_version: "1.0.0",
tools: [MyAppMCP.Tools.SomeTool],
resources: [MyAppMCP.Resources.UsersRepo]Support for reusable behaviour implementations
Tools, resource repositories, prompt repositories, and extensions are modules that must implement the corresponding behaviour.
The following values are accepted:
- A
{module, arg}tuple:{MyTool, opt: :foo}. Theargtype is not limited to keyword lists and can be anything. - A module name:
MyTool, it is equivalent to{MyTool, []}. - A map: tool_descriptor, resource_repo_descriptor, prompt_repo_descriptor or extension_descriptor.
All callbacks from those behaviours will receive the additional argument. It is then possible and encouraged to use generic implementations:
resource_repo = MyAppMCP.Resources.EctoResourceRepo
plug GenMCP.Transport.StreamableHTTP,
server_name: "MyServer",
server_version: "1.0.0",
resources: [
{resource_repo, schema: MyApp.Shop.Article, prefix: "myapp://articles"},
{resource_repo, schema: MyApp.Blog.Post, prefix: "myapp://posts"}
]Tools
Tools are the primary way to expose functionality to the client.
Defining a Tool
Use use GenMCP.Suite.Tool to define a tool. This macro provides default implementations for common callbacks and handles JSON schema validation.
defmodule MyApp.Tools.Calculator do
use GenMCP.Suite.Tool,
name: "calculator",
description: "Performs basic arithmetic operations",
input_schema: %{
type: :object,
properties: %{
operation: %{type: :string, enum: ["add", "subtract"]},
a: %{type: :number},
b: %{type: :number}
},
required: [:operation, :a, :b]
}
alias GenMCP.MCP
@impl true
def call(req, channel, _arg) do
%{"operation" => op, "a" => a, "b" => b} = req.params.arguments
result = calculate(op, a, b)
{:result, MCP.call_tool_result(text: "Result: #{result}"), channel}
end
defp calculate("add", a, b), do: a + b
defp calculate("subtract", a, b), do: a - b
endAsynchronous Tools
Tools can perform long-running operations asynchronously. The call/3 callback can return an {:async, {tag, task}, channel} tuple.
defmodule MyApp.Tools.LongRunning do
use GenMCP.Suite.Tool,
name: "long_running",
input_schema: %{type: :object}
@impl true
def call(_req, channel, _arg) do
task =
Task.async(fn ->
Process.sleep(1000)
"Done"
end)
{:async, {:my_task, task}, channel}
end
@impl true
def continue({:my_task, {:ok, result}}, channel, _arg) do
{:result, GenMCP.MCP.call_tool_result(text: result), channel}
end
endResources
Resources expose data to the client. They are organized in repositories.
Defining a Resource Repository
Implement the GenMCP.Suite.ResourceRepo behaviour.
Prefixes should be unique
Use unique URI schemes like myapp://, file+myapp:// or ui://myapp/ to
avoid collisions with other repositories.
defmodule MyApp.Resources.MyRepo do
@behaviour GenMCP.Suite.ResourceRepo
alias GenMCP.MCP
@impl true
def prefix(_arg), do: "myapp://"
@impl true
def list(_cursor, _channel, _arg) do
resources = [
%{uri: "myapp://config", name: "Configuration"}
]
{resources, _new_cursor = nil}
end
@impl true
def read("myapp://config", _channel, _arg) do
result =
MCP.read_resource_result(
uri: "myapp://config",
text: "{\"debug\": true}",
mimeType: "application/json"
)
{:ok, result}
end
def read(_uri, _channel, _arg), do: {:error, :not_found}
endPrompts
Prompts are reusable templates for LLM interactions. They are organized in repositories like resources.
Defining a Prompt Repository
Implement the GenMCP.Suite.PromptRepo behaviour.
Prefixes should be unique
Use unique prefixes like my-app- to avoid collisions.
defmodule MyApp.Prompts.MyRepo do
@behaviour GenMCP.Suite.PromptRepo
alias GenMCP.MCP
@impl true
def prefix(_arg), do: "myapp-"
@impl true
def list(_cursor, _channel, _arg) do
prompts = [
%{name: "myapp-greeting", description: "Greets the user"}
]
{prompts, nil}
end
@impl true
def get("myapp-greeting", _prompt_args, _channel, _arg) do
result =
MCP.get_prompt_result(
description: "Helps prompt writing",
assistant: "Hello, how can I help you?",
user: "I would like to write a prompt...",
assistant: "Sure, ..."
)
{:ok, result}
end
endChannels & Assigns
The GenMCP.Mux.Channel struct represents the communication channel with the client. It holds state in its assigns field.
- Assigns are initialized from the
:assignsoption passed to the plug. - For each incoming request, connection assigns (from
Plug.Conn) are merged into the channel's assigns according to the:copy_assignsoption. - Assigns are scoped to the request channel. Modifying them in a tool only affects that specific request flow.
- Async tools retain the channel (and its assigns) from the original request, ensuring context is preserved.
Extensions & Priority
The :tools, :resources, and :prompts options in the configuration form a "self extension".
- Extensions are loaded in the order they are defined in the
:extensionslist. - Components given directly to
GenMPC.Suitein the:tools,:resourcesand:promptsoptions are bundled in a virtual extension called the "self extension". - For resources and prompts, the first repository with a matching prefix handles the request. The self extension is always tried first, so you can override prefixes from third party extensions.
Extension Authoring
Extensions allow bundling tools, resources, and prompts together.
Defining an Extension
Implement the GenMCP.Suite.Extension behaviour.
defmodule MyExtension do
@behaviour GenMCP.Suite.Extension
@impl true
def tools(_channel, _arg), do: [MyExtension.Tools.SomeTool]
@impl true
def resources(_channel, _arg), do: [MyExtension.Resources.SomeRepo]
@impl true
def prompts(_channel, _arg), do: []
endPrefixes should be unique
When authoring extensions, ensure your resource and prompt prefixes are unique to avoid conflicts with other extensions or the user's own configuration.