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 of GenMCP.Suite.Tool implementations 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 of GenMCP.Suite.ResourceRepo implementations 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 of GenMCP.Suite.PromptRepo implementations to generate prompts with. List items can be either module names, {module, arg} tuples or a descriptor map. The default value is [].

  • :extensions - A list GenMCP.Suite.Extension implementations 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 - A GenMCP.Suite.SessionController implementation The default value is nil.

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:

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
end

Asynchronous 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
end

Resources

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}
end

Prompts

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
end

Channels & 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 :assigns option passed to the plug.
  • For each incoming request, connection assigns (from Plug.Conn) are merged into the channel's assigns according to the :copy_assigns option.
  • 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 :extensions list.
  • Components given directly to GenMPC.Suite in the :tools, :resources and :prompts options 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: []
end

Prefixes 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.