feat: alignment on missing MCP ressources

This commit is contained in:
2026-05-01 18:37:33 +02:00
parent 6e6a751db0
commit d92d05de92
6 changed files with 248 additions and 11 deletions

View File

@@ -1,12 +1,16 @@
defmodule BDS.MCP.Resources do
@moduledoc false
import Ecto.Query
alias BDS.MCP.ProposalStore
alias BDS.MCP.Queries
alias BDS.MCP.Util
alias BDS.Media.Media, as: MediaAsset
alias BDS.Metadata
alias BDS.Posts.PostMedia
alias BDS.Posts.Post
alias BDS.Projects
alias BDS.Repo
alias BDS.Search
alias BDS.Tags
@@ -23,7 +27,8 @@ defmodule BDS.MCP.Resources do
%{name: "posts", uri: "bds://posts"},
%{name: "media", uri: "bds://media"},
%{name: "tags", uri: "bds://tags"},
%{name: "categories", uri: "bds://categories"}
%{name: "categories", uri: "bds://categories"},
%{name: "stats", uri: "bds://stats"}
]
end
@@ -31,7 +36,9 @@ defmodule BDS.MCP.Resources do
def templates do
[
%{name: "posts", uriTemplate: "bds://posts{?cursor}"},
%{name: "media", uriTemplate: "bds://media{?cursor}"}
%{name: "media", uriTemplate: "bds://media{?cursor}"},
%{name: "post media", uriTemplate: "bds://posts/{id}/media"},
%{name: "media image", uriTemplate: "bds://media/{id}/image"}
]
end
@@ -44,8 +51,9 @@ defmodule BDS.MCP.Resources do
%URI{scheme: "bds", host: "media", path: nil, query: query} -> media_resource(query)
%URI{scheme: "bds", host: "tags", path: nil} -> {:ok, tags_resource()}
%URI{scheme: "bds", host: "categories", path: nil} -> {:ok, categories_resource()}
%URI{scheme: "bds", host: "posts", path: "/" <> id} -> read_post_resource(id)
%URI{scheme: "bds", host: "media", path: "/" <> id} -> read_media_resource(id)
%URI{scheme: "bds", host: "stats", path: nil} -> {:ok, stats_resource()}
%URI{scheme: "bds", host: "posts", path: path} -> read_posts_path(path)
%URI{scheme: "bds", host: "media", path: path} -> read_media_path(path)
_other -> {:error, :not_found}
end
end
@@ -155,6 +163,40 @@ defmodule BDS.MCP.Resources do
}
end
defp stats_resource do
project = Queries.active_project!()
{:ok, posts} = Search.search_posts(project.id, "", %{offset: 0, limit: 1})
{:ok, media} = Search.search_media(project.id, "", %{offset: 0, limit: 1})
{:ok, metadata} = Metadata.get_project_metadata(project.id)
%{
"posts" => posts.total,
"media" => media.total,
"tags" => length(Tags.list_tags(project.id)),
"categories" => length(metadata.categories)
}
end
defp read_posts_path("/" <> path) do
case String.split(path, "/", trim: true) do
[id] -> read_post_resource(id)
[id, "media"] -> read_post_media_resource(id)
_parts -> {:error, :not_found}
end
end
defp read_posts_path(_path), do: {:error, :not_found}
defp read_media_path("/" <> path) do
case String.split(path, "/", trim: true) do
[id] -> read_media_resource(id)
[id, "image"] -> read_media_image_resource(id)
_parts -> {:error, :not_found}
end
end
defp read_media_path(_path), do: {:error, :not_found}
defp read_post_resource(id) do
case Repo.get(Post, id) do
%Post{} = post -> {:ok, Queries.post_detail(post)}
@@ -168,4 +210,47 @@ defmodule BDS.MCP.Resources do
nil -> {:error, :not_found}
end
end
defp read_post_media_resource(post_id) do
case Repo.get(Post, post_id) do
nil ->
{:error, :not_found}
%Post{} ->
items =
Repo.all(
from media in MediaAsset,
join: post_media in PostMedia,
on: post_media.media_id == media.id,
where: post_media.post_id == ^post_id,
order_by: [asc: post_media.sort_order, asc: media.updated_at],
select: media
)
{:ok, %{"items" => Enum.map(items, &Util.sanitize/1)}}
end
end
defp read_media_image_resource(id) do
case Repo.get(MediaAsset, id) do
nil ->
{:error, :not_found}
%MediaAsset{} = media ->
project = Projects.get_project!(media.project_id)
full_path = Path.join(Projects.project_data_dir(project), media.file_path || "")
case File.read(full_path) do
{:ok, bytes} ->
{:ok,
%{
"mimeType" => media.mime_type || "application/octet-stream",
"blob" => Base.encode64(bytes)
}}
{:error, _reason} ->
{:error, :not_found}
end
end
end
end

View File

@@ -189,8 +189,7 @@ defmodule BDS.MCP.Server do
{:ok, success_response(id, %{"resources" => BDS.MCP.list_resources()})}
"resources/templates/list" ->
{:ok,
success_response(id, %{"resourceTemplates" => BDS.MCP.list_resource_templates()})}
{:ok, success_response(id, %{"resourceTemplates" => BDS.MCP.list_resource_templates()})}
"resources/read" ->
read_resource(id, params)
@@ -227,9 +226,7 @@ defmodule BDS.MCP.Server do
{:ok, result} ->
{:ok,
success_response(id, %{
"contents" => [
%{"uri" => uri, "mimeType" => "application/json", "text" => Jason.encode!(result)}
]
"contents" => [resource_content(uri, result)]
})}
{:error, :not_found} ->
@@ -242,6 +239,15 @@ defmodule BDS.MCP.Server do
defp read_resource(id, _params), do: {:error, error_response(id, -32602, "Invalid params")}
defp resource_content(uri, %{"blob" => blob, "mimeType" => mime_type})
when is_binary(blob) and is_binary(mime_type) do
%{"uri" => uri, "mimeType" => mime_type, "blob" => blob}
end
defp resource_content(uri, result) do
%{"uri" => uri, "mimeType" => "application/json", "text" => Jason.encode!(result)}
end
defp success_response(id, result), do: %{"jsonrpc" => "2.0", "id" => id, "result" => result}
defp error_response(id, code, message) do