8000 Testing tools by jameslong · Pull Request #7 · jameslong/vancouver · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Testing tools #7

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jun 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension < 8000 label class="SelectMenu-item" role="menuitem"> .exs  (3)

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,7 @@ vancouver-*.tar
# Temporary files, for example, from tests.
/tmp/

.DS_Store
.DS_Store

# VSCode config
launch.json
7 changes: 2 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,15 @@ 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

In `router.ex`:

```elixir
forward "/mcp", Vancouver.Router
forward "/mcp", Vancouver.Router, tools: [MyApp.Tools.CalculateSum]
```

### 5. (Optional) Add to your MCP client
Expand Down
2 changes: 1 addition & 1 deletion lib/vancouver/plugs/dispatch.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions lib/vancouver/plugs/pipeline.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ defmodule Vancouver.Plugs.Pipeline do
@moduledoc false

use Plug.Builder

alias Vancouver.Plugs

plug(Plug.Parsers, parsers: [:json], pass: ["application/json"], json_decoder: JSON)
Expand Down
11 changes: 3 additions & 8 deletions lib/vancouver/router.ex
Original file line number Diff line number Diff line change
@@ -1,18 +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)

post "/" do
Pipeline.call(conn, Pipeline.init([]))
end

match _ do
send_resp(conn, 404, "Not found")
end
post("/", do: Pipeline.call(conn, Pipeline.init([])))
match(_, do: send_resp(conn, 404, "Not found"))
end
168 changes: 168 additions & 0 deletions lib/vancouver/test/tool_test.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
defmodule Vancouver.ToolTest do
@moduledoc """
Conveniences for testing Vancouver tools.
"""

@doc """
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()) :: map()
def call_request(tool_name, arguments) do
%{
"jsonrpc" => "2.0",
"id" => 1,
"method" => "tools/call",
"params" => %{
"name" => tool_name,
"arguments" => arguments
}
}
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)

response
|> check_success()
|> check_type("audio")
|> 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)

response
|> check_error()
|> check_type("text")
|> 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)

response
|> check_success()
|> check_type("image")
|> 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)

response
|> check_success()
|> check_type("text")
|> 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)

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
36 changes: 18 additions & 18 deletions lib/vancouver/tool.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
41 changes: 41 additions & 0 deletions lib/vancouver/tools/test_response.ex
Original file line number Diff line number Diff line change
@@ -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
Loading
0