From 55bf98f4b7f1907c6fa930f97ee13041b4a9fcf3 Mon Sep 17 00:00:00 2001 From: jameslong <846867+jameslong@users.noreply.github.com> Date: Tue, 10 Jun 2025 16:27:58 -0700 Subject: [PATCH 01/12] Git ignore VSCode config file --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9e94237..ee3776f 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,7 @@ vancouver-*.tar # Temporary files, for example, from tests. /tmp/ -.DS_Store \ No newline at end of file +.DS_Store + +# VSCode config +launch.json \ No newline at end of file From ab54c6305a685d1638353db30eef9ec642b8daac Mon Sep 17 00:00:00 2001 From: jameslong <846867+jameslong@users.noreply.github.com> Date: Tue, 10 Jun 2025 19:35:01 -0700 Subject: [PATCH 02/12] Add ToolTest module --- lib/vancouver/test/tool_test.ex | 110 ++++++++++++++++ test/vancouver/test/tool_test.exs | 201 ++++++++++++++++++++++++++++++ 2 files changed, 311 insertions(+) create mode 100644 lib/vancouver/test/tool_test.ex create mode 100644 test/vancouver/test/tool_test.exs diff --git a/lib/vancouver/test/tool_test.ex b/lib/vancouver/test/tool_test.ex new file mode 100644 index 0000000..ce308b6 --- /dev/null +++ b/lib/vancouver/test/tool_test.ex @@ -0,0 +1,110 @@ +defmodule Vancouver.ToolTest do + @moduledoc """ + Conveniences for testing Vancouver tools. + """ + + def call_request(tool_name, arguments, id \\ "1") do + %{ + "jsonrpc" => "2.0", + "id" => id, + "method" => "tools/call", + "params" => %{ + "name" => tool_name, + "arguments" => arguments + } + } + end + + def audio_response(conn) do + response = JSON.decode!(conn.resp_body) + + response + |> check_success() + |> check_type("audio") + |> get_content() + end + + def error_response(conn) do + response = JSON.decode!(conn.resp_body) + + response + |> check_error() + |> check_type("text") + |> get_text() + end + + def image_response(conn) do + response = JSON.decode!(conn.resp_body) + + response + |> check_success() + |> check_type("image") + |> get_content() + end + + def json_response(conn) do + response = JSON.decode!(conn.resp_body) + + response + |> check_success() + |> check_type("text") + |> get_json() + end + + def text_response(conn) do + response = JSON.decode!(conn.resp_body) + + response + |> check_success() + |> check_type("text") + |> get_text() + end + + defp check_success(response) do + if error?(response) do + raise "expected success response, got: error" + else + response + end + end + + defp check_error(response) do + if error?(response) do + response + else + raise "expected error response, got: success" + end + end + + defp error?(response), do: response["result"]["isError"] || false + + defp check_type(response, expected_type) do + type = get_content_type(response) + + if expected_type == type do + response + else + raise "expected response with content type #{expected_type}, got: #{type}" + end + end + + defp get_json(response) do + text = get_text(response) + + case JSON.decode(get_text(response)) do + {:ok, json} -> json + {:error, _} -> raise "expected response content with valid JSON, got: #{text}" + end + end + + defp get_text(response), do: get_content(response)["text"] + + defp get_content(response) do + case response["result"]["content"] do + [content] -> content + content -> raise "expected response with single content item, got: #{inspect(content)}" + end + end + + defp get_content_type(response), do: get_content(response)["type"] +end diff --git a/test/vancouver/test/tool_test.exs b/test/vancouver/test/tool_test.exs new file mode 100644 index 0000000..d6cc84f --- /dev/null +++ b/test/vancouver/test/tool_test.exs @@ -0,0 +1,201 @@ +defmodule Vancouver.Test.ToolTest do + use ExUnit.Case, async: true + + import Plug.Conn + import Plug.Test + + alias Vancouver.JsonRpc2 + alias Vancouver.ToolTest + + describe "call_request/3" do + test "creates a valid call request" do + tool_name = "test_tool" + arguments = %{"arg1" => "value1", "arg2" => "value2"} + id = "123" + + assert ToolTest.call_request(tool_name, arguments, id) == %{ + "jsonrpc" => "2.0", + "id" => id, + "method" => "tools/call", + "params" => %{ + "name" => tool_name, + "arguments" => arguments + } + } + end + end + + describe "audio_response/1" do + test "with valid conn returns content" do + content = [%{"type" => "audio", "data" => "base64-audio-data", "mimeType" => "audio/wav"}] + conn = build_success_conn(content) + + response = ToolTest.audio_response(conn) + assert response["data"] == "base64-audio-data" + assert response["mimeType"] == "audio/wav" + end + + test "with invalid conn raises error" do + assert_raise RuntimeError, "expected response with single content item, got: []", fn -> + content = [] + conn = build_success_conn(content) + ToolTest.audio_response(conn) + end + + assert_raise RuntimeError, "expected response with content type audio, got: text", fn -> + content = [%{"type" => "text", "text" => "text"}] + conn = build_success_conn(content) + ToolTest.audio_response(conn) + end + + assert_raise RuntimeError, "expected success response, got: error", fn -> + content = [%{"type" => "text", "text" => "oops"}] + conn = build_error_conn(content) + ToolTest.audio_response(conn) + end + end + end + + describe "error_response/1" do + test "with valid conn returns content" do + content = [%{"type" => "text", "text" => "oops"}] + conn = build_error_conn(content) + + assert ToolTest.error_response(conn) == "oops" + end + + test "with invalid conn raises error" do + assert_raise RuntimeError, "expected response with single content item, got: []", fn -> + content = [] + conn = build_error_conn(content) + ToolTest.error_response(conn) + end + + assert_raise RuntimeError, "expected response with content type text, got: image", fn -> + content = [%{"type" => "image", "data" => "base64-image-data", "mimeType" => "image/png"}] + conn = build_error_conn(content) + ToolTest.error_response(conn) + end + + assert_raise RuntimeError, "expected error response, got: success", fn -> + content = [%{"type" => "text", "text" => "Hello, world!"}] + conn = build_success_conn(content) + ToolTest.error_response(conn) + end + end + end + + describe "image_response/1" do + test "with valid conn returns content" do + content = [%{"type" => "image", "data" => "base64-image-data", "mimeType" => "image/png"}] + conn = build_success_conn(content) + + response = ToolTest.image_response(conn) + assert response["data"] == "base64-image-data" + assert response["mimeType"] == "image/png" + end + + test "with invalid conn raises error" do + assert_raise RuntimeError, "expected response with single content item, got: []", fn -> + content = [] + conn = build_success_conn(content) + ToolTest.image_response(conn) + end + + assert_raise RuntimeError, "expected response with content type image, got: text", fn -> + content = [%{"type" => "text", "text" => "Hello, world!"}] + conn = build_success_conn(content) + ToolTest.image_response(conn) + end + + assert_raise RuntimeError, "expected success response, got: error", fn -> + content = [%{"type" => "text", "text" => "oops"}] + conn = build_error_conn(content) + ToolTest.image_response(conn) + end + end + end + + describe "json_response/1" do + test "with valid conn returns content" do + content = [%{"type" => "text", "text" => "{\"key\": \"value\"}"}] + conn = build_success_conn(content) + + assert %{"key" => "value"} = ToolTest.json_response(conn) + end + + test "with invalid conn raises error" do + assert_raise RuntimeError, "expected response with single content item, got: []", fn -> + content = [] + conn = build_success_conn(content) + ToolTest.json_response(conn) + end + + assert_raise RuntimeError, "expected response with content type text, got: image", fn -> + content = [%{"type" => "image", "data" => "base64-image-data", "mimeType" => "image/png"}] + conn = build_success_conn(content) + ToolTest.json_response(conn) + end + + assert_raise RuntimeError, "expected response content with valid JSON, got: %{}", fn -> + content = [%{"type" => "text", "text" => "%{}"}] + conn = build_success_conn(content) + ToolTest.json_response(conn) + end + + assert_raise RuntimeError, "expected success response, got: error", fn -> + content = [%{"type" => "text", "text" => "oops"}] + conn = build_error_conn(content) + ToolTest.json_response(conn) + end + end + end + + describe "text_response/1" do + test "with valid conn returns content" do + text = "Hello, world!" + content = [%{"type" => "text", "text" => text}] + conn = build_success_conn(content) + + assert ToolTest.text_response(conn) == text + end + + test "with invalid conn raises error" do + assert_raise RuntimeError, "expected response with single content item, got: []", fn -> + content = [] + conn = build_success_conn(content) + ToolTest.text_response(conn) + end + + assert_raise RuntimeError, "expected response with content type text, got: image", fn -> + content = [%{"type" => "image", "data" => "base64-image-data", "mimeType" => "image/png"}] + conn = build_success_conn(content) + ToolTest.text_response(conn) + end + + assert_raise RuntimeError, "expected success response, got: error", fn -> + content = [%{"type" => "text", "text" => "oops"}] + conn = build_error_conn(content) + ToolTest.text_response(conn) + end + end + end + + defp build_success_conn(content, request_id \\ 1) do + build_response_conn(content, request_id, false) + end + + defp build_error_conn(content, request_id \\ 1) do + build_response_conn(content, request_id, true) + end + + def build_response_conn(content, request_id \\ 1, is_error \\ false) do + result = %{"content" => content, "isError" => is_error} + body = JsonRpc2.success_response(request_id, result) + + conn(:post, "/") + |> put_req_header("content-type", "application/json") + |> Map.put(:resp_body, JSON.encode!(body)) + |> put_status(200) + end +end From 6d8db73b0f2a3cc0df77ffcf4983f54b2b412967 Mon Sep 17 00:00:00 2001 From: jameslong <846867+jameslong@users.noreply.github.com> Date: Tue, 10 Jun 2025 19:44:26 -0700 Subject: [PATCH 03/12] Add typespecs for ToolTest module --- lib/vancouver/test/tool_test.ex | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/vancouver/test/tool_test.ex b/lib/vancouver/test/tool_test.ex index ce308b6..4dc68d7 100644 --- a/lib/vancouver/test/tool_test.ex +++ b/lib/vancouver/test/tool_test.ex @@ -3,6 +3,7 @@ defmodule Vancouver.ToolTest do Conveniences for testing Vancouver tools. """ + @spec call_request(String.t(), map(), String.t()) :: map() def call_request(tool_name, arguments, id \\ "1") do %{ "jsonrpc" => "2.0", @@ -15,6 +16,7 @@ defmodule Vancouver.ToolTest do } end + @spec audio_response(Plug.Conn.t()) :: %{data: String.t(), mimeType: String.t()} def audio_response(conn) do response = JSON.decode!(conn.resp_body) @@ -24,6 +26,7 @@ defmodule Vancouver.ToolTest do |> get_content() end + @spec error_response(Plug.Conn.t()) :: String.t() def error_response(conn) do response = JSON.decode!(conn.resp_body) @@ -33,6 +36,7 @@ defmodule Vancouver.ToolTest do |> get_text() end + @spec image_response(Plug.Conn.t()) :: %{data: String.t(), mimeType: String.t()} def image_response(conn) do response = JSON.decode!(conn.resp_body) @@ -42,6 +46,7 @@ defmodule Vancouver.ToolTest do |> get_content() end + @spec json_response(Plug.Conn.t()) :: term() def json_response(conn) do response = JSON.decode!(conn.resp_body) @@ -51,6 +56,7 @@ defmodule Vancouver.ToolTest do |> get_json() end + @spec text_response(Plug.Conn.t()) :: String.t() def text_response(conn) do response = JSON.decode!(conn.resp_body) From 07ef71c490a224cdd9c40d7a19520a5277afb8b1 Mon Sep 17 00:00:00 2001 From: jameslong <846867+jameslong@users.noreply.github.com> Date: Tue, 10 Jun 2025 19:52:35 -0700 Subject: [PATCH 04/12] Add function docs to ToolTest module --- lib/vancouver/test/tool_test.ex | 52 +++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/lib/vancouver/test/tool_test.ex b/lib/vancouver/test/tool_test.ex index 4dc68d7..ac51108 100644 --- a/lib/vancouver/test/tool_test.ex +++ b/lib/vancouver/test/tool_test.ex @@ -3,6 +3,14 @@ defmodule Vancouver.ToolTest do Conveniences for testing Vancouver tools. """ + @doc """ + Creates a JSON-RPC call request for a tool. + + ## Examples + + body = call_request("calculate_sum", %{"a" => 1, "b" => 2}) + + """ @spec call_request(String.t(), map(), String.t()) :: map() def call_request(tool_name, arguments, id \\ "1") do %{ @@ -16,6 +24,16 @@ defmodule Vancouver.ToolTest do } end + @doc """ + Asserts that the response was successful, and that audio content was returned. + + ## Examples + + content = audio_response(conn) + assert content["data"] == "base64-audio-data" + assert content["mimeType"] == "audio/wav" + + """ @spec audio_response(Plug.Conn.t()) :: %{data: String.t(), mimeType: String.t()} def audio_response(conn) do response = JSON.decode!(conn.resp_body) @@ -26,6 +44,14 @@ defmodule Vancouver.ToolTest do |> get_content() end + @doc """ + Asserts that the response was an error, and returns the error text. + + ## Examples + + assert error_response(conn) == "An error occurred" + + """ @spec error_response(Plug.Conn.t()) :: String.t() def error_response(conn) do response = JSON.decode!(conn.resp_body) @@ -36,6 +62,16 @@ defmodule Vancouver.ToolTest do |> get_text() end + @doc """ + Asserts that the response was successful, and that image content was returned. + + ## Examples + + content = image_response(conn) + assert content["data"] == "base64-image-data" + assert content["mimeType"] == "image/png" + + """ @spec image_response(Plug.Conn.t()) :: %{data: String.t(), mimeType: String.t()} def image_response(conn) do response = JSON.decode!(conn.resp_body) @@ -46,6 +82,14 @@ defmodule Vancouver.ToolTest do |> get_content() end + @doc """ + Asserts that the response was successful, and that JSON content was returned. + + ## Examples + + assert json_response(conn) == %{"key" => "value"} + + """ @spec json_response(Plug.Conn.t()) :: term() def json_response(conn) do response = JSON.decode!(conn.resp_body) @@ -56,6 +100,14 @@ defmodule Vancouver.ToolTest do |> get_json() end + @doc """ + Asserts that the response was successful, and that text content was returned. + + ## Examples + + assert text_response(conn) == "Hello, world!" + + """ @spec text_response(Plug.Conn.t()) :: String.t() def text_response(conn) do response = JSON.decode!(conn.resp_body) From eaf0c5ef7988397d3ac0ae38cc086d54d52a9a9e Mon Sep 17 00:00:00 2001 From: jameslong <846867+jameslong@users.noreply.github.com> Date: Tue, 10 Jun 2025 19:55:34 -0700 Subject: [PATCH 05/12] Remove id argument (not required for now) --- lib/vancouver/test/tool_test.ex | 8 ++++---- test/vancouver/test/tool_test.exs | 5 ++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/vancouver/test/tool_test.ex b/lib/vancouver/test/tool_test.ex index ac51108..bd83a32 100644 --- a/lib/vancouver/test/tool_test.ex +++ b/lib/vancouver/test/tool_test.ex @@ -4,18 +4,18 @@ defmodule Vancouver.ToolTest do """ @doc """ - Creates a JSON-RPC call request for a tool. + Creates a valid request body for a tool call request. ## Examples body = call_request("calculate_sum", %{"a" => 1, "b" => 2}) """ - @spec call_request(String.t(), map(), String.t()) :: map() - def call_request(tool_name, arguments, id \\ "1") do + @spec call_request(String.t(), map()) :: map() + def call_request(tool_name, arguments) do %{ "jsonrpc" => "2.0", - "id" => id, + "id" => 1, "method" => "tools/call", "params" => %{ "name" => tool_name, diff --git a/test/vancouver/test/tool_test.exs b/test/vancouver/test/tool_test.exs index d6cc84f..11cb243 100644 --- a/test/vancouver/test/tool_test.exs +++ b/test/vancouver/test/tool_test.exs @@ -11,11 +11,10 @@ defmodule Vancouver.Test.ToolTest do test "creates a valid call request" do tool_name = "test_tool" arguments = %{"arg1" => "value1", "arg2" => "value2"} - id = "123" - assert ToolTest.call_request(tool_name, arguments, id) == %{ + assert ToolTest.call_request(tool_name, arguments) == %{ "jsonrpc" => "2.0", - "id" => id, + "id" => 1, "method" => "tools/call", "params" => %{ "name" => tool_name, From 47a197921b008a2d32882b8d66a82850ad73857c Mon Sep 17 00:00:00 2001 From: jameslong <846867+jameslong@users.noreply.github.com> Date: Wed, 11 Jun 2025 08:44:44 -0700 Subject: [PATCH 06/12] Use string keys for json rpc messages --- lib/vancouver/tool.ex | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/vancouver/tool.ex b/lib/vancouver/tool.ex index add350e..2ccd4db 100644 --- a/lib/vancouver/tool.ex +++ b/lib/vancouver/tool.ex @@ -91,14 +91,14 @@ defmodule Vancouver.Tool do def send_audio(%Plug.Conn{} = conn, base64_data, mime_type) when is_binary(base64_data) and is_binary(mime_type) do result = %{ - content: [ + "content" => [ %{ - type: "audio", - data: base64_data, - mimeType: mime_type + "type" => "audio", + "data" => base64_data, + "mimeType" => mime_type } ], - isError: false + "isError" => false } send_success(conn, result) @@ -115,13 +115,13 @@ defmodule Vancouver.Tool do @spec send_error(Plug.Conn.t(), binary()) :: Plug.Conn.t() def send_error(%Plug.Conn{} = conn, message) do result = %{ - content: [ + "content" => [ %{ - type: "text", - text: message + "type" => "text", + "text" => message } ], - isError: true + "isError" => true } send_success(conn, result) @@ -139,14 +139,14 @@ defmodule Vancouver.Tool do def send_image(%Plug.Conn{} = conn, base64_data, mime_type) when is_binary(base64_data) and is_binary(mime_type) do result = %{ - content: [ + "content" => [ %{ - type: "image", - data: base64_data, - mimeType: mime_type + "type" => "image", + "data" => base64_data, + "mimeType" => mime_type } ], - isError: false + "isError" => false } send_success(conn, result) @@ -174,13 +174,13 @@ defmodule Vancouver.Tool do @spec send_text(Plug.Conn.t(), binary()) :: Plug.Conn.t() def send_text(%Plug.Conn{} = conn, text) when is_binary(text) do result = %{ - content: [ + "content" => [ %{ - type: "text", - text: text + "type" => "text", + "text" => text } ], - isError: false + "isError" => false } send_success(conn, result) From a464128814266dc51ba42251ea503ed72364ca06 Mon Sep 17 00:00:00 2001 From: jameslong <846867+jameslong@users.noreply.github.com> Date: Wed, 11 Jun 2025 09:07:42 -0700 Subject: [PATCH 07/12] Add tests for CalculateSum tool --- test/vancouver/tools/calculate_sum_test.exs | 30 +++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 test/vancouver/tools/calculate_sum_test.exs diff --git a/test/vancouver/tools/calculate_sum_test.exs b/test/vancouver/tools/calculate_sum_test.exs new file mode 100644 index 0000000..98e1525 --- /dev/null +++ b/test/vancouver/tools/calculate_sum_test.exs @@ -0,0 +1,30 @@ +defmodule Vancouver.Tools.CalculateSumTest do + use ExUnit.Case, async: false + + import Plug.Conn + import Plug.Test + + alias Vancouver.Tools.CalculateSum + alias Vancouver.ToolTest + + describe "run/2" do + setup do + Application.put_env(:vancouver, :tools, [CalculateSum]) + :ok + end + + test "with argunments returns success" do + conn = build_conn("calculate_sum", %{"a" => 1, "b" => 2}) + assert ToolTest.text_response(conn) == "3" + end + end + + defp build_conn(tool_name, tool_arguments) do + body = ToolTest.call_request(tool_name, tool_arguments) + + :post + |> conn("/", JSON.encode!(body)) + |> put_req_header("content-type", "application/json") + |> Vancouver.Router.call(Vancouver.Router.init([])) + end +end From ff0e0eb326aed9e5f213cb8870e48606471f6ee6 Mon Sep 17 00:00:00 2001 From: jameslong <846867+jameslong@users.noreply.github.com> Date: Wed, 11 Jun 2025 09:08:18 -0700 Subject: [PATCH 08/12] Add TestResponse tool Used to test all response helpers. --- lib/vancouver/tools/test_response.ex | 41 +++++++++++++++ test/vancouver/tools/test_response_test.exs | 56 +++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 lib/vancouver/tools/test_response.ex create mode 100644 test/vancouver/tools/test_response_test.exs diff --git a/lib/vancouver/tools/test_response.ex b/lib/vancouver/tools/test_response.ex new file mode 100644 index 0000000..5227f31 --- /dev/null +++ b/lib/vancouver/tools/test_response.ex @@ -0,0 +1,41 @@ +defmodule Vancouver.Tools.TestResponse do + @moduledoc false + + use Vancouver.Tool + + def name, do: "test_response" + def description, do: "Returns success/error responses for all context types" + + def input_schema do + %{ + "type" => "object", + "properties" => %{ + "response_type" => %{ + "type" => "string", + "enum" => ["audio", "error", "image", "json", "text"] + } + }, + "required" => ["response_type"] + } + end + + def run(conn, %{"response_type" => "audio"}) do + send_audio(conn, "base64-encoded-audio-data", "audio/wav") + end + + def run(conn, %{"response_type" => "error"}) do + send_error(conn, "Error message") + end + + def run(conn, %{"response_type" => "image"}) do + send_image(conn, "base64-encoded-data", "image/png") + end + + def run(conn, %{"response_type" => "json"}) do + send_json(conn, %{"key" => "value"}) + end + + def run(conn, %{"response_type" => "text"}) do + send_text(conn, "Success text") + end +end diff --git a/test/vancouver/tools/test_response_test.exs b/test/vancouver/tools/test_response_test.exs new file mode 100644 index 0000000..2e910c6 --- /dev/null +++ b/test/vancouver/tools/test_response_test.exs @@ -0,0 +1,56 @@ +defmodule Vancouver.Tools.TestResponseTest do + use ExUnit.Case, async: false + + import Plug.Conn + import Plug.Test + + alias Vancouver.Tools.TestResponse + alias Vancouver.ToolTest + + describe "run/2" do + setup do + Application.put_env(:vancouver, :tools, [TestResponse]) + :ok + end + + test "with audio response type returns success" do + conn = build_conn("test_response", %{"response_type" => "audio"}) + + audio = ToolTest.audio_response(conn) + assert audio["data"] == "base64-encoded-audio-data" + assert audio["mimeType"] == "audio/wav" + end + + test "with error response type returns error" do + conn = build_conn("test_response", %{"response_type" => "error"}) + assert ToolTest.error_response(conn) == "Error message" + end + + test "with image response type returns success" do + conn = build_conn("test_response", %{"response_type" => "image"}) + + image = ToolTest.image_response(conn) + assert image["data"] == "base64-encoded-data" + assert image["mimeType"] == "image/png" + end + + test "with json response type returns success" do + conn = build_conn("test_response", %{"response_type" => "json"}) + assert ToolTest.json_response(conn) == %{"key" => "value"} + end + + test "with text response type returns success" do + conn = build_conn("test_response", %{"response_type" => "text"}) + assert ToolTest.text_response(conn) == "Success text" + end + end + + defp build_conn(tool_name, tool_arguments) do + body = ToolTest.call_request(tool_name, tool_arguments) + + :post + |> conn("/", JSON.encode!(body)) + |> put_req_header("content-type", "application/json") + |> Vancouver.Router.call(Vancouver.Router.init([])) + end +end From 954cababce959a557b8dc3e7904ada009051fd8d Mon Sep 17 00:00:00 2001 From: jameslong <846867+jameslong@users.noreply.github.com> Date: Wed, 11 Jun 2025 10:45:12 -0700 Subject: [PATCH 09/12] Remove tools from global config This allows for simpler, concurrent testing. --- lib/vancouver/plugs/dispatch.ex | 2 +- lib/vancouver/plugs/pipeline.ex | 3 ++- lib/vancouver/router.ex | 8 +------- test/vancouver/tools/calculate_sum_test.exs | 10 +++------- test/vancouver/tools/test_response_test.exs | 10 +++------- 5 files changed, 10 insertions(+), 23 deletions(-) diff --git a/lib/vancouver/plugs/dispatch.ex b/lib/vancouver/plugs/dispatch.ex index 73c7fe7..cc94f95 100644 --- a/lib/vancouver/plugs/dispatch.ex +++ b/lib/vancouver/plugs/dispatch.ex @@ -8,7 +8,7 @@ defmodule Vancouver.Plugs.Dispatch do def call(%Plug.Conn{} = conn, _opts) do request = conn.body_params method = request["method"] - tools = Application.get_env(:vancouver, :tools, []) + tools = conn.assigns[:vancouver][:tools] || [] case method do "initialize" -> Methods.Initialize.run(conn) diff --git a/lib/vancouver/plugs/pipeline.ex b/lib/vancouver/plugs/pipeline.ex index da78f69..0bba6c6 100644 --- a/lib/vancouver/plugs/pipeline.ex +++ b/lib/vancouver/plugs/pipeline.ex @@ -1,7 +1,8 @@ defmodule Vancouver.Plugs.Pipeline do @moduledoc false - use Plug.Builder + use Plug.Builder, copy_opts_to_assign: :vancouver + alias Vancouver.Plugs plug(Plug.Parsers, parsers: [:json], pass: ["application/json"], json_decoder: JSON) diff --git a/lib/vancouver/router.ex b/lib/vancouver/router.ex index 7097a6a..38de845 100644 --- a/lib/vancouver/router.ex +++ b/lib/vancouver/router.ex @@ -8,11 +8,5 @@ defmodule Vancouver.Router do plug(:match) plug(:dispatch) - post "/" do - Pipeline.call(conn, Pipeline.init([])) - end - - match _ do - send_resp(conn, 404, "Not found") - end + forward("/", to: Pipeline) end diff --git a/test/vancouver/tools/calculate_sum_test.exs b/test/vancouver/tools/calculate_sum_test.exs index 98e1525..d20c16b 100644 --- a/test/vancouver/tools/calculate_sum_test.exs +++ b/test/vancouver/tools/calculate_sum_test.exs @@ -1,18 +1,14 @@ defmodule Vancouver.Tools.CalculateSumTest do - use ExUnit.Case, async: false + use ExUnit.Case, async: true import Plug.Conn import Plug.Test + alias Vancouver.Plugs.Pipeline alias Vancouver.Tools.CalculateSum alias Vancouver.ToolTest describe "run/2" do - setup do - Application.put_env(:vancouver, :tools, [CalculateSum]) - :ok - end - test "with argunments returns success" do conn = build_conn("calculate_sum", %{"a" => 1, "b" => 2}) assert ToolTest.text_response(conn) == "3" @@ -25,6 +21,6 @@ defmodule Vancouver.Tools.CalculateSumTest do :post |> conn("/", JSON.encode!(body)) |> put_req_header("content-type", "application/json") - |> Vancouver.Router.call(Vancouver.Router.init([])) + |> Pipeline.call(tools: [CalculateSum]) end end diff --git a/test/vancouver/tools/test_response_test.exs b/test/vancouver/tools/test_response_test.exs index 2e910c6..1d9566a 100644 --- a/test/vancouver/tools/test_response_test.exs +++ b/test/vancouver/tools/test_response_test.exs @@ -1,18 +1,14 @@ defmodule Vancouver.Tools.TestResponseTest do - use ExUnit.Case, async: false + use ExUnit.Case, async: true import Plug.Conn import Plug.Test + alias Vancouver.Plugs.Pipeline alias Vancouver.Tools.TestResponse alias Vancouver.ToolTest describe "run/2" do - setup do - Application.put_env(:vancouver, :tools, [TestResponse]) - :ok - end - test "with audio response type returns success" do conn = build_conn("test_response", %{"response_type" => "audio"}) @@ -51,6 +47,6 @@ defmodule Vancouver.Tools.TestResponseTest do :post |> conn("/", JSON.encode!(body)) |> put_req_header("content-type", "application/json") - |> Vancouver.Router.call(Vancouver.Router.init([])) + |> Pipeline.call(tools: [TestResponse]) end end From 30b63b58860d39bdbccfd2b24fe301a317a5767e Mon Sep 17 00:00:00 2001 From: jameslong <846867+jameslong@users.noreply.github.com> Date: Wed, 11 Jun 2025 13:13:55 -0700 Subject: [PATCH 10/12] Set assigns at router level --- lib/vancouver/plugs/pipeline.ex | 2 +- lib/vancouver/router.ex | 5 +++-- test/vancouver/tools/calculate_sum_test.exs | 4 ++-- test/vancouver/tools/test_response_test.exs | 4 ++-- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/vancouver/plugs/pipeline.ex b/lib/vancouver/plugs/pipeline.ex index 0bba6c6..cab50aa 100644 --- a/lib/vancouver/plugs/pipeline.ex +++ b/lib/vancouver/plugs/pipeline.ex @@ -1,7 +1,7 @@ defmodule Vancouver.Plugs.Pipeline do @moduledoc false - use Plug.Builder, copy_opts_to_assign: :vancouver + use Plug.Builder alias Vancouver.Plugs diff --git a/lib/vancouver/router.ex b/lib/vancouver/router.ex index 38de845..c6a760b 100644 --- a/lib/vancouver/router.ex +++ b/lib/vancouver/router.ex @@ -1,12 +1,13 @@ defmodule Vancouver.Router do @moduledoc false - use Plug.Router + use Plug.Router, copy_opts_to_assign: :vancouver alias Vancouver.Plugs.Pipeline plug(:match) plug(:dispatch) - forward("/", to: Pipeline) + post("/", do: Pipeline.call(conn, Pipeline.init([]))) + match(_, do: send_resp(conn, 404, "Not found")) end diff --git a/test/vancouver/tools/calculate_sum_test.exs b/test/vancouver/tools/calculate_sum_test.exs index d20c16b..30a84ee 100644 --- a/test/vancouver/tools/calculate_sum_test.exs +++ b/test/vancouver/tools/calculate_sum_test.exs @@ -4,7 +4,7 @@ defmodule Vancouver.Tools.CalculateSumTest do import Plug.Conn import Plug.Test - alias Vancouver.Plugs.Pipeline + alias Vancouver.Router alias Vancouver.Tools.CalculateSum alias Vancouver.ToolTest @@ -21,6 +21,6 @@ defmodule Vancouver.Tools.CalculateSumTest do :post |> conn("/", JSON.encode!(body)) |> put_req_header("content-type", "application/json") - |> Pipeline.call(tools: [CalculateSum]) + |> Router.call(tools: [CalculateSum]) end end diff --git a/test/vancouver/tools/test_response_test.exs b/test/vancouver/tools/test_response_test.exs index 1d9566a..a0f85ec 100644 --- a/test/vancouver/tools/test_response_test.exs +++ b/test/vancouver/tools/test_response_test.exs @@ -4,7 +4,7 @@ defmodule Vancouver.Tools.TestResponseTest do import Plug.Conn import Plug.Test - alias Vancouver.Plugs.Pipeline + alias Vancouver.Router alias Vancouver.Tools.TestResponse alias Vancouver.ToolTest @@ -47,6 +47,6 @@ defmodule Vancouver.Tools.TestResponseTest do :post |> conn("/", JSON.encode!(body)) |> put_req_header("content-type", "application/json") - |> Pipeline.call(tools: [TestResponse]) + |> Router.call(tools: [TestResponse]) end end From 277c039e342221ce89a57a6202411a860b07b69e Mon Sep 17 00:00:00 2001 From: jameslong <846867+jameslong@users.noreply.github.com> Date: Wed, 11 Jun 2025 13:59:45 -0700 Subject: [PATCH 11/12] Update README with new tool option --- README.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 05c286d..b435df8 100644 --- a/README.md +++ b/README.md @@ -49,10 +49,7 @@ In `config.ex`: ```elixir config :vancouver, name: "My MCP Server", - version: "1.0.0", - tools: [ - MyApp.Tools.CalculateSum - ] + version: "1.0.0" ``` ### 4. Add your MCP route @@ -60,7 +57,7 @@ config :vancouver, In `router.ex`: ```elixir -forward "/mcp", Vancouver.Router +forward "/mcp", Vancouver.Router, tools: [MyApp.Tools.CalculateSum] ``` ### 5. (Optional) Add to your MCP client From 55072afc7b7bd4d77aeb2939741f47635bf8aee0 Mon Sep 17 00:00:00 2001 From: James Long <846867+jameslong@users.noreply.github.com> Date: Wed, 11 Jun 2025 14:29:19 -0700 Subject: [PATCH 12/12] Update test/vancouver/tools/calculate_sum_test.exs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- test/vancouver/tools/calculate_sum_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/vancouver/tools/calculate_sum_test.exs b/test/vancouver/tools/calculate_sum_test.exs index 30a84ee..2013f87 100644 --- a/test/vancouver/tools/calculate_sum_test.exs +++ b/test/vancouver/tools/calculate_sum_test.exs @@ -9,7 +9,7 @@ defmodule Vancouver.Tools.CalculateSumTest do alias Vancouver.ToolTest describe "run/2" do - test "with argunments returns success" do + test "with arguments returns success" do conn = build_conn("calculate_sum", %{"a" => 1, "b" => 2}) assert ToolTest.text_response(conn) == "3" end