144 lines
4.5 KiB
Elixir
144 lines
4.5 KiB
Elixir
defmodule BDS.MCPServerTest do
|
|
use ExUnit.Case, async: false
|
|
|
|
setup do
|
|
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
|
|
|
|
temp_dir =
|
|
Path.join(System.tmp_dir!(), "bds-mcp-server-#{System.unique_integer([:positive])}")
|
|
|
|
File.mkdir_p!(temp_dir)
|
|
on_exit(fn -> File.rm_rf(temp_dir) end)
|
|
|
|
{:ok, project} = BDS.Projects.create_project(%{name: "MCP Server", data_path: temp_dir})
|
|
{:ok, _active} = BDS.Projects.set_active_project(project.id)
|
|
|
|
%{project: project, temp_dir: temp_dir}
|
|
end
|
|
|
|
test "HTTP MCP server binds localhost, answers initialize, and exposes tool capabilities" do
|
|
:inets.start()
|
|
|
|
assert {:ok, server} = BDS.MCP.Server.start(0)
|
|
assert server.host == "127.0.0.1"
|
|
assert server.port > 0
|
|
|
|
initialize_body =
|
|
Jason.encode!(%{
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "initialize",
|
|
params: %{
|
|
protocolVersion: "2025-03-26",
|
|
capabilities: %{},
|
|
clientInfo: %{name: "test-client", version: "1.0.0"}
|
|
}
|
|
})
|
|
|
|
assert {:ok, {{_version, 200, _reason}, headers, body}} =
|
|
:httpc.request(
|
|
:post,
|
|
{to_charlist("http://127.0.0.1:#{server.port}/mcp"),
|
|
[{~c"content-type", ~c"application/json"}], ~c"application/json",
|
|
initialize_body},
|
|
[],
|
|
body_format: :binary
|
|
)
|
|
|
|
assert Enum.any?(headers, fn {name, value} ->
|
|
String.downcase(to_string(name)) == "access-control-allow-methods" and
|
|
to_string(value) =~ "POST"
|
|
end)
|
|
|
|
decoded = Jason.decode!(body)
|
|
assert decoded["result"]["serverInfo"]["name"] == "Blogging Desktop Server"
|
|
assert decoded["result"]["capabilities"]["tools"] == %{}
|
|
|
|
assert :ok = BDS.MCP.Server.stop()
|
|
end
|
|
|
|
test "HTTP MCP server rejects non-local origins and can list tools" do
|
|
:inets.start()
|
|
assert {:ok, server} = BDS.MCP.Server.start(0)
|
|
|
|
initialize_body =
|
|
Jason.encode!(%{jsonrpc: "2.0", id: 1, method: "initialize", params: %{}})
|
|
|
|
assert {:ok, {{_version, 403, _reason}, _headers, _body}} =
|
|
:httpc.request(
|
|
:post,
|
|
{to_charlist("http://127.0.0.1:#{server.port}/mcp"),
|
|
[
|
|
{~c"content-type", ~c"application/json"},
|
|
{~c"origin", ~c"https://evil.example"}
|
|
], ~c"application/json", initialize_body},
|
|
[],
|
|
body_format: :binary
|
|
)
|
|
|
|
tools_body = Jason.encode!(%{jsonrpc: "2.0", id: 2, method: "tools/list", params: %{}})
|
|
|
|
assert {:ok, {{_version, 200, _reason}, _headers, body}} =
|
|
:httpc.request(
|
|
:post,
|
|
{to_charlist("http://127.0.0.1:#{server.port}/mcp"),
|
|
[{~c"content-type", ~c"application/json"}], ~c"application/json", tools_body},
|
|
[],
|
|
body_format: :binary
|
|
)
|
|
|
|
decoded = Jason.decode!(body)
|
|
tool_names = Enum.map(decoded["result"]["tools"], & &1["name"])
|
|
assert "check_term" in tool_names
|
|
assert "draft_post" in tool_names
|
|
|
|
assert :ok = BDS.MCP.Server.stop()
|
|
end
|
|
|
|
test "HTTP MCP server returns media image resources as blob contents", %{
|
|
project: project,
|
|
temp_dir: temp_dir
|
|
} do
|
|
:inets.start()
|
|
|
|
source_path = Path.join(temp_dir, "server-image.png")
|
|
image_bytes = <<137, 80, 78, 71, 13, 10, 26, 10, "server-bytes">>
|
|
File.write!(source_path, image_bytes)
|
|
|
|
assert {:ok, media} =
|
|
BDS.Media.import_media(%{
|
|
project_id: project.id,
|
|
source_path: source_path,
|
|
title: "Server Image"
|
|
})
|
|
|
|
assert {:ok, server} = BDS.MCP.Server.start(0)
|
|
|
|
read_body =
|
|
Jason.encode!(%{
|
|
jsonrpc: "2.0",
|
|
id: 3,
|
|
method: "resources/read",
|
|
params: %{uri: "bds://media/#{media.id}/image"}
|
|
})
|
|
|
|
assert {:ok, {{_version, 200, _reason}, _headers, body}} =
|
|
:httpc.request(
|
|
:post,
|
|
{to_charlist("http://127.0.0.1:#{server.port}/mcp"),
|
|
[{~c"content-type", ~c"application/json"}], ~c"application/json", read_body},
|
|
[],
|
|
body_format: :binary
|
|
)
|
|
|
|
decoded = Jason.decode!(body)
|
|
assert [content] = decoded["result"]["contents"]
|
|
assert content["uri"] == "bds://media/#{media.id}/image"
|
|
assert content["mimeType"] == "image/png"
|
|
assert Base.decode64!(content["blob"]) == image_bytes
|
|
refute Map.has_key?(content, "text")
|
|
|
|
assert :ok = BDS.MCP.Server.stop()
|
|
end
|
|
end
|