lib/mix/tasks/bb_mcp.install.ex

# SPDX-FileCopyrightText: 2026 James Harton
#
# SPDX-License-Identifier: Apache-2.0

if Code.ensure_loaded?(Igniter) do
  defmodule Mix.Tasks.BbMcp.Install do
    @shortdoc "Installs BB.MCP into an application"
    @moduledoc """
    #{@shortdoc}

    Configures `:bb_mcp, :robots` from the supplied `--robot` module(s),
    then wires up an MCP transport:

      * If the host app uses Phoenix, the installer locates the
        application's router and adds the `BB.MCP.Router.bb_mcp/2`
        mount (defaulting to `/mcp`).
      * Otherwise it adds `BB.MCP.Server` as a Streamable HTTP child to
        the application's supervision tree, listening on `--port`.

    ## Example

    ```bash
    mix igniter.install bb_mcp
    mix igniter.install bb_mcp --robot MyApp.Robot
    mix igniter.install bb_mcp --robot MyApp.WX200 --robot MyApp.SO101
    mix igniter.install bb_mcp --path /mcp --port 4001
    ```

    ## Options

      * `--robot` — Robot module to expose. May be supplied multiple
        times. Defaults to `{AppPrefix}.Robot`.
      * `--path` — URL path for the Phoenix mount (default `/mcp`).
      * `--port` — Streamable HTTP port for the non-Phoenix mount
        (default `4000`).
    """

    use Igniter.Mix.Task

    alias Igniter.Libs.Phoenix
    alias Igniter.Project.Application, as: ProjectApplication
    alias Igniter.Project.Config, as: ProjectConfig
    alias Igniter.Project.Formatter, as: ProjectFormatter
    alias Igniter.Project.Module, as: ProjectModule

    @impl Igniter.Mix.Task
    def info(_argv, _parent) do
      %Igniter.Mix.Task.Info{
        schema: [
          robot: :keep,
          path: :string,
          port: :integer
        ],
        aliases: [r: :robot, p: :path]
      }
    end

    @impl Igniter.Mix.Task
    def igniter(igniter) do
      options = igniter.args.options
      path = Keyword.get(options, :path, "/mcp")
      port = Keyword.get(options, :port, 4000)
      robots = resolve_robots(igniter, options)

      igniter
      |> ProjectFormatter.import_dep(:bb_mcp)
      |> ProjectConfig.configure(
        "config.exs",
        :bb_mcp,
        [:robots],
        {:code, robots_list_ast(robots)}
      )
      |> wire_transport(path, port)
    end

    defp wire_transport(igniter, path, port) do
      case Phoenix.select_router(igniter) do
        {igniter, nil} -> add_standalone_server(igniter, port)
        {igniter, router} -> mount_into_phoenix(igniter, router, path)
      end
    end

    defp mount_into_phoenix(igniter, router, path) do
      # Do not pass :arg2 — bb_mcp must live in an unaliased scope so
      # Phoenix doesn't rewrite the underlying Anubis Plug module.
      # BB.MCP.Server still needs to be running for the plug to find it,
      # so add it to the supervision tree. Phoenix's endpoint handles the
      # HTTP listener; Anubis just needs the server's transport process.
      igniter
      |> maybe_add_router_scope(router, path)
      |> ProjectApplication.add_new_child({BB.MCP.Server, transport: :streamable_http})
      |> Igniter.add_notice("""
      bb_mcp: mounted at #{inspect(path)} on #{inspect(router)}. The MCP
      Streamable HTTP endpoint will be served by your Phoenix endpoint;
      BB.MCP.Server has been added to the application supervision tree.
      """)
    end

    defp maybe_add_router_scope(igniter, router, path) do
      if router_already_mounts_bb_mcp?(igniter, router) do
        igniter
      else
        Phoenix.add_scope(igniter, "/", router_scope(path), router: router)
      end
    end

    defp router_already_mounts_bb_mcp?(igniter, router) do
      case ProjectModule.find_module(igniter, router) do
        {:ok, {_igniter, source, _zipper}} ->
          source |> Rewrite.Source.get(:content) |> String.contains?("BB.MCP.Router")

        _ ->
          false
      end
    end

    defp router_scope(path) do
      """
      import BB.MCP.Router
      bb_mcp #{inspect(path)}
      """
    end

    defp add_standalone_server(igniter, port) do
      igniter
      |> ProjectApplication.add_new_child(
        {BB.MCP.Server, transport: :streamable_http, streamable_http: [port: port]}
      )
      |> Igniter.add_notice("""
      bb_mcp: BB.MCP.Server added to the supervision tree, serving Streamable
      HTTP on http://localhost:#{port}/. No Phoenix dep detected — the
      server runs standalone via Anubis' built-in HTTP transport.
      """)
    end

    defp resolve_robots(igniter, options) do
      case options |> Keyword.get_values(:robot) |> List.flatten() do
        [] -> [ProjectModule.module_name(igniter, "Robot")]
        names -> Enum.map(names, &ProjectModule.parse/1)
      end
    end

    defp robots_list_ast(robots) do
      Enum.map(robots, &module_ast/1)
    end

    defp module_ast(module) do
      {:__aliases__, [alias: false], module |> Module.split() |> Enum.map(&String.to_atom/1)}
    end
  end
else
  defmodule Mix.Tasks.BbMcp.Install do
    @shortdoc "Installs BB.MCP into an application"
    @moduledoc false
    use Mix.Task

    def run(_argv) do
      Mix.shell().error("""
      The bb_mcp.install task requires igniter.

          mix igniter.install bb_mcp
      """)

      exit({:shutdown, 1})
    end
  end
end