From 302ffab0f060feb4d4f50804481c1e5e560e7c39 Mon Sep 17 00:00:00 2001 From: Hashrocket Workstation Date: Thu, 30 Oct 2025 15:34:19 -0400 Subject: [PATCH] Fix MCP multi user server Co-authored-by: Vinicius Negrisolo Co-authored-by: Gabriel Reis --- lib/tilex/mcp/new_post.ex | 15 +++++++-- lib/tilex/mcp/server.ex | 23 ------------- test/tilex/mcp/new_post_test.exs | 33 ++++++++++++------ test/tilex/mcp/server_test.exs | 58 -------------------------------- 4 files changed, 35 insertions(+), 94 deletions(-) delete mode 100644 test/tilex/mcp/server_test.exs diff --git a/lib/tilex/mcp/new_post.ex b/lib/tilex/mcp/new_post.ex index 070b579c..17fa1c09 100644 --- a/lib/tilex/mcp/new_post.ex +++ b/lib/tilex/mcp/new_post.ex @@ -9,12 +9,14 @@ defmodule Tilex.MCP.NewPost do import Ecto.Query, only: [from: 2] - alias Ecto.Changeset alias Anubis.Server.Response + alias Ecto.Changeset alias Tilex.Blog.Channel alias Tilex.Blog.Developer + alias Tilex.Blog.Developer alias Tilex.Blog.Post alias Tilex.Repo + alias Tilex.Repo alias TilexWeb.Endpoint alias TilexWeb.Router.Helpers, as: Routes @@ -57,7 +59,16 @@ defmodule Tilex.MCP.NewPost do end defp get_current_user(frame) do - case Map.get(frame.assigns, :current_user) do + headers = Enum.into(frame.transport.req_headers, %{}) + signed_token = headers["x-api-key"] + + with "" <> _ <- signed_token, + {:ok, mcp_api_key} <- Developer.verify_mcp_api_key(TilexWeb.Endpoint, signed_token) do + Repo.one(from d in Developer, where: d.mcp_api_key == ^mcp_api_key) + else + _ -> nil + end + |> case do nil -> {:error, "User is not authenticated to create TILs"} %Developer{} = user -> {:ok, user} end diff --git a/lib/tilex/mcp/server.ex b/lib/tilex/mcp/server.ex index 560a23e8..fa0c8431 100644 --- a/lib/tilex/mcp/server.ex +++ b/lib/tilex/mcp/server.ex @@ -1,29 +1,6 @@ defmodule Tilex.MCP.Server do use Anubis.Server, name: "TIL", version: "1.0.0", capabilities: [:resources, :tools] - import Ecto.Query, only: [from: 2] - - alias Tilex.Repo - alias Tilex.Blog.Developer - component(Tilex.MCP.ListChannels) component(Tilex.MCP.NewPost) - - @impl true - def init(_arg, frame) do - headers = Enum.into(frame.transport.req_headers, %{}) - user = get_current_user(headers["x-api-key"]) - assigns = Map.put(frame.assigns || %{}, :current_user, user) - frame = Map.put(frame, :assigns, assigns) - {:ok, frame} - end - - defp get_current_user(signed_token) do - with "" <> _ <- signed_token, - {:ok, mcp_api_key} <- Developer.verify_mcp_api_key(TilexWeb.Endpoint, signed_token) do - Repo.one(from d in Developer, where: d.mcp_api_key == ^mcp_api_key) - else - _ -> nil - end - end end diff --git a/test/tilex/mcp/new_post_test.exs b/test/tilex/mcp/new_post_test.exs index 13369da9..aa2e5f87 100644 --- a/test/tilex/mcp/new_post_test.exs +++ b/test/tilex/mcp/new_post_test.exs @@ -2,14 +2,28 @@ defmodule Tilex.MCP.NewPostTest do use Tilex.DataCase, async: false alias Anubis.Server.Response + alias Tilex.Blog.Developer alias Tilex.Blog.Post alias Tilex.Factory alias Tilex.MCP.NewPost alias Tilex.Repo describe "execute/2" do - test "creates post successfully with valid data and authenticated user" do - developer = Factory.insert!(:developer) + setup do + %{ + mcp_api_key: mcp_api_key, + signed_token: signed_token + } = Developer.generate_mcp_api_key(TilexWeb.Endpoint) + + developer = Factory.insert!(:developer, mcp_api_key: mcp_api_key) + + [developer: developer, signed_token: signed_token] + end + + test "creates post successfully with valid data and authenticated user", %{ + developer: developer, + signed_token: signed_token + } do channel = Factory.insert!(:channel, name: "elixir") title = "My First TIL" @@ -23,7 +37,7 @@ defmodule Tilex.MCP.NewPostTest do body: body } - frame = %{assigns: %{current_user: developer}} + frame = %{transport: %{req_headers: %{"x-api-key" => signed_token}}} assert {:reply, response, returned_frame} = NewPost.execute(input, frame) @@ -64,7 +78,7 @@ defmodule Tilex.MCP.NewPostTest do body: body } - frame = %{assigns: %{}} + frame = %{transport: %{req_headers: %{}}} assert {:reply, response, returned_frame} = NewPost.execute(input, frame) @@ -82,9 +96,7 @@ defmodule Tilex.MCP.NewPostTest do } = response end - test "raises error when channel does not exist" do - developer = Factory.insert!(:developer) - + test "raises error when channel does not exist", %{signed_token: signed_token} do title = "My First TIL" body = "Today I learned something amazing about Elixir." @@ -94,7 +106,7 @@ defmodule Tilex.MCP.NewPostTest do body: body } - frame = %{assigns: %{current_user: developer}} + frame = %{transport: %{req_headers: %{"x-api-key" => signed_token}}} assert {:reply, response, returned_frame} = NewPost.execute(input, frame) @@ -112,8 +124,7 @@ defmodule Tilex.MCP.NewPostTest do } = response end - test "returns validation error" do - developer = Factory.insert!(:developer) + test "returns validation error", %{signed_token: signed_token} do channel = Factory.insert!(:channel, name: "elixir") title = String.duplicate("a", 51) @@ -125,7 +136,7 @@ defmodule Tilex.MCP.NewPostTest do body: body } - frame = %{assigns: %{current_user: developer}} + frame = %{transport: %{req_headers: %{"x-api-key" => signed_token}}} assert {:reply, response, returned_frame} = NewPost.execute(input, frame) diff --git a/test/tilex/mcp/server_test.exs b/test/tilex/mcp/server_test.exs deleted file mode 100644 index 51fa6452..00000000 --- a/test/tilex/mcp/server_test.exs +++ /dev/null @@ -1,58 +0,0 @@ -defmodule Tilex.MCP.ServerTest do - use Tilex.DataCase, async: true - - alias Tilex.Blog.Developer - alias Tilex.Factory - alias Tilex.MCP.Server - - describe "MCP server authentication" do - test "valid API key authenticates developer" do - %{ - mcp_api_key: mcp_api_key, - signed_token: signed_token - } = Developer.generate_mcp_api_key(TilexWeb.Endpoint) - - developer = Factory.insert!(:developer, mcp_api_key: mcp_api_key) - developer_id = developer.id - - frame = %{ - transport: %{ - req_headers: [{"x-api-key", signed_token}] - }, - assigns: %{} - } - - assert {:ok, frame} = Server.init(nil, frame) - - assert %{ - current_user: %Tilex.Blog.Developer{ - id: ^developer_id - } - } = frame.assigns - end - - test "invalid API key fails authentication" do - %{ - mcp_api_key: _old_mcp_api_key, - signed_token: old_signed_token - } = Developer.generate_mcp_api_key(TilexWeb.Endpoint) - - %{ - mcp_api_key: mcp_api_key, - signed_token: _signed_token - } = Developer.generate_mcp_api_key(TilexWeb.Endpoint) - - Factory.insert!(:developer, mcp_api_key: mcp_api_key) - - frame = %{ - transport: %{ - req_headers: [{"x-api-key", old_signed_token}] - }, - assigns: %{} - } - - assert {:ok, frame} = Server.init(nil, frame) - assert %{current_user: nil} = frame.assigns - end - end -end