defmodule Corex.MCP.Tools.Installation do @moduledoc false def tools do [ %{ name: "installation_guide", description: """ Return copy-paste steps and commands for installing Corex in a new Phoenix app (mix corex.new) or an existing app (deps, Esbuild ESM, JS hooks, layout, use Corex). Read-only; does not run shell commands. """, inputSchema: %{ type: "object", properties: %{ scenario: %{ type: "string", enum: ["new_project", "existing_project", "all"], description: "new_project: archives and generator only; existing_project: minimal manual steps plus optional MCP plug; all: both (default)." } } }, annotations: %{"readOnlyHint" => true, "idempotentHint" => true}, callback: &installation_guide/1 } ] end def installation_guide(args) do scenario = case Map.get(args || %{}, "scenario") do s when s in ["new_project", "existing_project", "all"] -> s nil -> "all" _ -> "all" end payload = case scenario do "new_project" -> Map.put(new_project_section(), :scenario, scenario) "existing_project" -> Map.put(existing_project_section(), :scenario, scenario) "all" -> full_guide() end {:ok, Corex.Json.encode!(payload)} end defp full_guide do %{ scenario: "all", new_project: new_project_section(), existing_project: existing_project_section(), reference_urls: %{ hexdocs_corex: "https://hexdocs.pm/corex", manual_installation: "https://hexdocs.pm/corex/manual_installation.html", repository: "https://github.com/corex-ui/corex" } } end defp new_project_section do %{ intent: "Create a new Phoenix application with Corex preconfigured.", prerequisites: [ "Install the phx_new archive once per machine.", "Install the corex_new archive for mix corex.new." ], commands: [ %{step: 1, run: "mix archive.install hex phx_new"}, %{step: 2, run: "mix archive.install hex corex_new"}, %{step: 3, run: "mix corex.new my_app"} ], update_generator: %{ command: "mix local.corex", note: "Updates the corex.new archive before generating a project." }, pipeline: "corex.new runs mix phx.new --no-install with forwarded Phoenix flags, then renders Corex-owned files from templates directly into the generated app. No Igniter.", mix_help: "mix help corex.new", task_module: "Mix.Tasks.Corex.New" } end defp existing_project_section do %{ intent: "Add Corex to an existing Phoenix project.", approach: "Apply minimal_steps in order. Replace my_app with your OTP application name where paths use lib/my_app_web/. Full narrative and optional sections (design, toast, theming) are on Hexdocs.", guide: "https://hexdocs.pm/corex/manual_installation.html", corex_version_constraint: "~> #{corex_hex_version()}", minimal_steps: minimal_steps_existing_project(), mcp_mount_optional_dev: mcp_mount_snippet(), design_assets: %{ command: "mix corex.design", note: "Copies priv/design into assets/corex/. Pass --designex to also copy the token source tree. Pass --force to overwrite existing files." } } end defp corex_hex_version do case Application.spec(:corex, :vsn) do nil -> "0.1.0-beta.2" v when is_list(v) -> List.to_string(v) v when is_binary(v) -> v v -> to_string(v) end end defp minimal_steps_existing_project do ver = corex_hex_version() [ %{ step: 1, title: "Add the dependency", edit_files: ["mix.exs"], run_commands: ["mix deps.get"], snippet: """ # In mix.exs deps: {:corex, \"~> #{ver}\"} """ }, %{ step: 2, title: "Configure Esbuild for ESM and code splitting", edit_files: ["config/config.exs"], snippet: """ # Under config :esbuild, my_app: [ args: ~w(...) ], ensure the main target includes: # --bundle --format=esm --splitting --target=es2022 # --outdir=../priv/static/assets/js # --external:/fonts/* --external:/images/* --alias:@=. # and cd: Path.expand(\"../assets\", __DIR__), # env: %{\"NODE_PATH\" => [Path.expand(\"../deps\", __DIR__), Mix.Project.build_path()]} config :esbuild, version: \"0.25.4\", my_app: [ args: ~w(js/app.js --bundle --format=esm --splitting --target=es2022 --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/* --alias:@=.), cd: Path.expand(\"../assets\", __DIR__), env: %{\"NODE_PATH\" => [Path.expand(\"../deps\", __DIR__), Mix.Project.build_path()]} ] """ }, %{ step: 3, title: "Import Corex hooks in assets/js/app.js", edit_files: ["assets/js/app.js"], snippet: """ import corex from \"corex\" const liveSocket = new LiveSocket(\"/live\", Socket, { longPollFallbackMs: 2500, params: { _csrf_token: csrfToken }, hooks: { ...colocatedHooks, ...corex } }) """ }, %{ step: 4, title: "Load app.js as an ES module in the root layout", edit_files: ["lib/my_app_web/components/layouts/root.html.heex"], snippet: """ """ }, %{ step: 5, title: "Import Corex in the web layer", edit_files: ["lib/my_app_web.ex"], snippet: """ defp html_helpers do quote do use Gettext, backend: MyAppWeb.Gettext import Phoenix.HTML import MyAppWeb.CoreComponents use Corex alias Phoenix.LiveView.JS alias MyAppWeb.Layouts unquote(verified_routes()) end end """ }, %{ step: 6, title: "Verify compile and assets", run_commands: ["mix compile", "mix assets.build"] }, %{ step: 7, title: "Optional: mount Corex MCP in dev (AI tools)", edit_files: ["lib/my_app_web/endpoint.ex"], note: "Served on the same port as Phoenix; never enable in prod.", snippet: mcp_mount_snippet()[:plug_block] } ] end defp mcp_mount_snippet do %{ plug_block: """ if Mix.env() == :dev do plug Corex.MCP end """, placement: "Insert in lib/my_app_web/endpoint.ex before the `if code_reloading? do` block, after Plug.Static (and sockets) so routing stays correct.", routes: %{ landing: "GET /corex", config_json: "GET /corex/config", mcp_rpc: "POST /corex/mcp" }, cursor_mcp_example: %{ servers: %{ corex: %{url: "http://localhost:4000/corex/mcp"} } } } end end