Vancouver - easily add MCP tools to your phoenix/bandit server

@tyr0 one major suggestion, something I’d ideally need to make AshAI use this, is to make it like plug which accepts opts, and all functions accept those options as their first argument, post initialization.

In practice, it looks like this:

defmodule MyApp.Tools.CalculateSum do
  use Vancouver.Tool

  # imagine (for whatever reason) you wanted a "fast" and a "slow" version of `sum`
  def init(opts) do
    # validate opts
    {:ok, %{speed: opts[:speed]}}
  end

  def name(%{speed: :slow}), do: "calculate_sum"
  def name(%{speed: :fast}), do: "calculate_sum_quickly"

  def description(opts) do
    description = "Add two numbers together"
    if opts.speed == :fast do
      """
      #{description}

      This costs extra money, only use when you need to 
      add many numbers together quickly.
      """
    else 
      description
    end
  end

  def input_schema(_) do
    %{
      "type" => "object",
      "properties" => %{
        "a" => %{"type" => "number"},
        "b" => %{"type" => "number"}
      },
      "required" => ["a", "b"]
    }
  end

  def run(conn, opts, %{"a" => a, "b" => b}) do
    if opts.speed == :fast do
      send_text(conn, "#{sum_fast(a, b)}")
    else
      send_text(conn, "#{sum_slow(a, b)}")
    end
  end
end

and then in the router:

forward "/mcp", Vancouver.Router, tools: [
  {MyApp.Tools.CalculateSum, speed: :fast}, 
  {MyApp.Tools.CalculationSum, speed: :slow}
]

And finally, add a concept of extensions that can dynamically add tools.

defmodule SomeExtensions do
  use Vancouver.Extension

  def add_tools(opts) do
     [...new_tools]
   end
end

which would then look like this:

forward "/mcp", Vancouver.Router, tools: [
  {MyApp.Tools.CalculateSum, speed: :fast}, 
  {MyApp.Tools.CalculationSum, speed: :slow}
], extensions: [{AshAi.Mcp, otp_app: :otp_app}]

This allows layers to be built dynamically on top of Vancouver. If that happens then I’ll be able to consider using it as a basis for AshAI’s MCP tooling (which is not something I’m saying you should care about :laughing:, just letting you know what would make it work for my use case).

1 Like