feat: alignment with mcp ressource cursor paging
This commit is contained in:
@@ -10,12 +10,18 @@ defmodule BDS.MCP do
|
||||
@typedoc "Resource descriptor returned by `list_resources/0`."
|
||||
@type resource_descriptor :: Resources.descriptor()
|
||||
|
||||
@typedoc "Resource template descriptor returned by `list_resource_templates/0`."
|
||||
@type resource_template_descriptor :: Resources.template_descriptor()
|
||||
|
||||
@spec list_tools() :: [tool_descriptor()]
|
||||
defdelegate list_tools(), to: Tools, as: :list
|
||||
|
||||
@spec list_resources() :: [resource_descriptor()]
|
||||
defdelegate list_resources(), to: Resources, as: :list
|
||||
|
||||
@spec list_resource_templates() :: [resource_template_descriptor()]
|
||||
defdelegate list_resource_templates(), to: Resources, as: :templates
|
||||
|
||||
@spec call_tool(String.t(), map()) :: {:ok, term()} | {:error, term()}
|
||||
defdelegate call_tool(name, params), to: Tools, as: :call
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ defmodule BDS.MCP.Resources do
|
||||
@typedoc "Resource descriptor returned by `list/0`."
|
||||
@type descriptor :: %{name: String.t(), uri: String.t()}
|
||||
|
||||
@typedoc "Resource template descriptor returned by `templates/0`."
|
||||
@type template_descriptor :: %{name: String.t(), uriTemplate: String.t()}
|
||||
|
||||
@spec list() :: [descriptor()]
|
||||
def list do
|
||||
[
|
||||
@@ -24,13 +27,21 @@ defmodule BDS.MCP.Resources do
|
||||
]
|
||||
end
|
||||
|
||||
@spec templates() :: [template_descriptor()]
|
||||
def templates do
|
||||
[
|
||||
%{name: "posts", uriTemplate: "bds://posts{?cursor}"},
|
||||
%{name: "media", uriTemplate: "bds://media{?cursor}"}
|
||||
]
|
||||
end
|
||||
|
||||
@spec read(String.t()) :: {:ok, term()} | {:error, term()}
|
||||
def read(uri) when is_binary(uri) do
|
||||
ProposalStore.ensure_started()
|
||||
|
||||
case URI.parse(uri) do
|
||||
%URI{scheme: "bds", host: "posts", path: nil} -> {:ok, posts_resource(0)}
|
||||
%URI{scheme: "bds", host: "media", path: nil} -> {:ok, media_resource(0)}
|
||||
%URI{scheme: "bds", host: "posts", path: nil, query: query} -> posts_resource(query)
|
||||
%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)
|
||||
@@ -39,7 +50,13 @@ defmodule BDS.MCP.Resources do
|
||||
end
|
||||
end
|
||||
|
||||
defp posts_resource(offset) do
|
||||
defp posts_resource(query) do
|
||||
with {:ok, offset} <- cursor_offset(query) do
|
||||
{:ok, posts_page(offset)}
|
||||
end
|
||||
end
|
||||
|
||||
defp posts_page(offset) do
|
||||
project = Queries.active_project!()
|
||||
page_size = Queries.page_size()
|
||||
{:ok, result} = Search.search_posts(project.id, "", %{offset: offset, limit: page_size})
|
||||
@@ -48,12 +65,18 @@ defmodule BDS.MCP.Resources do
|
||||
"items" => Enum.map(result.posts, &Queries.post_summary/1),
|
||||
"total" => result.total,
|
||||
"offset" => result.offset,
|
||||
"limit" => result.limit,
|
||||
"has_more" => result.offset + result.limit < result.total
|
||||
"limit" => result.limit
|
||||
}
|
||||
|> maybe_put_next_cursor(result)
|
||||
end
|
||||
|
||||
defp media_resource(offset) do
|
||||
defp media_resource(query) do
|
||||
with {:ok, offset} <- cursor_offset(query) do
|
||||
{:ok, media_page(offset)}
|
||||
end
|
||||
end
|
||||
|
||||
defp media_page(offset) do
|
||||
project = Queries.active_project!()
|
||||
page_size = Queries.page_size()
|
||||
{:ok, result} = Search.search_media(project.id, "", %{offset: offset, limit: page_size})
|
||||
@@ -62,9 +85,45 @@ defmodule BDS.MCP.Resources do
|
||||
"items" => Enum.map(result.media, &Util.sanitize/1),
|
||||
"total" => result.total,
|
||||
"offset" => result.offset,
|
||||
"limit" => result.limit,
|
||||
"has_more" => result.offset + result.limit < result.total
|
||||
"limit" => result.limit
|
||||
}
|
||||
|> maybe_put_next_cursor(result)
|
||||
end
|
||||
|
||||
defp cursor_offset(nil), do: {:ok, 0}
|
||||
|
||||
defp cursor_offset(query) do
|
||||
case URI.decode_query(query) do
|
||||
%{"cursor" => cursor} when cursor != "" -> decode_cursor(cursor)
|
||||
%{"cursor" => ""} -> {:error, :invalid_cursor}
|
||||
_params -> {:ok, 0}
|
||||
end
|
||||
end
|
||||
|
||||
defp decode_cursor(cursor) do
|
||||
with {:ok, decoded} <- Base.url_decode64(cursor, padding: false),
|
||||
{:ok, %{"offset" => offset}} when is_integer(offset) and offset >= 0 <-
|
||||
Jason.decode(decoded) do
|
||||
{:ok, offset}
|
||||
else
|
||||
_other -> {:error, :invalid_cursor}
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_put_next_cursor(resource, result) do
|
||||
next_offset = result.offset + result.limit
|
||||
|
||||
if next_offset < result.total do
|
||||
Map.put(resource, "nextCursor", encode_cursor(next_offset))
|
||||
else
|
||||
resource
|
||||
end
|
||||
end
|
||||
|
||||
defp encode_cursor(offset) do
|
||||
%{"offset" => offset}
|
||||
|> Jason.encode!()
|
||||
|> Base.url_encode64(padding: false)
|
||||
end
|
||||
|
||||
defp tags_resource do
|
||||
|
||||
@@ -188,6 +188,10 @@ defmodule BDS.MCP.Server do
|
||||
"resources/list" ->
|
||||
{:ok, success_response(id, %{"resources" => BDS.MCP.list_resources()})}
|
||||
|
||||
"resources/templates/list" ->
|
||||
{:ok,
|
||||
success_response(id, %{"resourceTemplates" => BDS.MCP.list_resource_templates()})}
|
||||
|
||||
"resources/read" ->
|
||||
read_resource(id, params)
|
||||
|
||||
@@ -230,6 +234,9 @@ defmodule BDS.MCP.Server do
|
||||
|
||||
{:error, :not_found} ->
|
||||
{:error, error_response(id, -32004, "Not found")}
|
||||
|
||||
{:error, :invalid_cursor} ->
|
||||
{:error, error_response(id, -32602, "Invalid cursor")}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -76,6 +76,18 @@ defmodule BDS.MCP.Stdio do
|
||||
%{"jsonrpc" => "2.0", "id" => id, "result" => %{"resources" => BDS.MCP.list_resources()}}
|
||||
end
|
||||
|
||||
defp handle_payload(%{
|
||||
"jsonrpc" => "2.0",
|
||||
"id" => id,
|
||||
"method" => "resources/templates/list"
|
||||
}) do
|
||||
%{
|
||||
"jsonrpc" => "2.0",
|
||||
"id" => id,
|
||||
"result" => %{"resourceTemplates" => BDS.MCP.list_resource_templates()}
|
||||
}
|
||||
end
|
||||
|
||||
defp handle_payload(%{
|
||||
"jsonrpc" => "2.0",
|
||||
"id" => id,
|
||||
|
||||
Reference in New Issue
Block a user