feat: alignment MCP translation tools
This commit is contained in:
@@ -19,7 +19,7 @@ Goal: align bDS2 with old bDS behavior. Use the Allium specs as the contract onl
|
|||||||
- Spec: matches old bDS cursor behavior.
|
- Spec: matches old bDS cursor behavior.
|
||||||
- Action: implement cursor parsing/templates for posts and media resources and add tests for first page, next cursor, invalid cursor, and final page.
|
- Action: implement cursor parsing/templates for posts and media resources and add tests for first page, next cursor, invalid cursor, and final page.
|
||||||
|
|
||||||
## P0: MCP Translation Tools
|
## P0: MCP Translation Tools (done)
|
||||||
|
|
||||||
- Old bDS: exposes `get_post_translations`, `get_media_translations`, and app-gated `upsert_media_translation`.
|
- Old bDS: exposes `get_post_translations`, `get_media_translations`, and app-gated `upsert_media_translation`.
|
||||||
- bDS2 now: domain translation functions exist, but MCP tools are missing.
|
- bDS2 now: domain translation functions exist, but MCP tools are missing.
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ defmodule BDS.MCP.Tools do
|
|||||||
tool("search_posts", true),
|
tool("search_posts", true),
|
||||||
tool("count_posts", true),
|
tool("count_posts", true),
|
||||||
tool("read_post_by_slug", true),
|
tool("read_post_by_slug", true),
|
||||||
|
tool("get_post_translations", true),
|
||||||
|
tool("get_media_translations", true),
|
||||||
|
tool("upsert_media_translation", false),
|
||||||
tool("draft_post", false),
|
tool("draft_post", false),
|
||||||
tool("propose_script", false),
|
tool("propose_script", false),
|
||||||
tool("propose_template", false),
|
tool("propose_template", false),
|
||||||
@@ -46,6 +49,9 @@ defmodule BDS.MCP.Tools do
|
|||||||
"search_posts" -> {:ok, search_posts(params)}
|
"search_posts" -> {:ok, search_posts(params)}
|
||||||
"count_posts" -> {:ok, count_posts(params)}
|
"count_posts" -> {:ok, count_posts(params)}
|
||||||
"read_post_by_slug" -> read_post_by_slug(params)
|
"read_post_by_slug" -> read_post_by_slug(params)
|
||||||
|
"get_post_translations" -> get_post_translations(params)
|
||||||
|
"get_media_translations" -> get_media_translations(params)
|
||||||
|
"upsert_media_translation" -> upsert_media_translation(params)
|
||||||
"draft_post" -> draft_post(params)
|
"draft_post" -> draft_post(params)
|
||||||
"propose_script" -> propose_script(params)
|
"propose_script" -> propose_script(params)
|
||||||
"propose_template" -> propose_template(params)
|
"propose_template" -> propose_template(params)
|
||||||
@@ -165,6 +171,47 @@ defmodule BDS.MCP.Tools do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp get_post_translations(params) do
|
||||||
|
post_id = map_get(params, :postId, nil)
|
||||||
|
|
||||||
|
case Posts.get_post(post_id) do
|
||||||
|
nil ->
|
||||||
|
{:error, :not_found}
|
||||||
|
|
||||||
|
%Post{} ->
|
||||||
|
with {:ok, translations} <- Posts.list_post_translations(post_id) do
|
||||||
|
{:ok, %{"translations" => sanitize(translations)}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_media_translations(params) do
|
||||||
|
media_id = map_get(params, :mediaId, nil)
|
||||||
|
|
||||||
|
case Media.get_media(media_id) do
|
||||||
|
nil ->
|
||||||
|
{:error, :not_found}
|
||||||
|
|
||||||
|
%MediaAsset{} ->
|
||||||
|
{:ok, %{"translations" => sanitize(Media.list_media_translations(media_id))}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp upsert_media_translation(params) do
|
||||||
|
media_id = map_get(params, :mediaId, nil)
|
||||||
|
language = params |> map_get(:language, "") |> normalize_term()
|
||||||
|
|
||||||
|
attrs = %{
|
||||||
|
title: map_get(params, :title, nil),
|
||||||
|
alt: map_get(params, :alt, nil),
|
||||||
|
caption: map_get(params, :caption, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
with {:ok, translation} <- Media.upsert_media_translation(media_id, language, attrs) do
|
||||||
|
{:ok, %{"translation" => sanitize(translation)}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp draft_post(params) do
|
defp draft_post(params) do
|
||||||
project = Queries.active_project!()
|
project = Queries.active_project!()
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,9 @@ surface McpAutomationSurface {
|
|||||||
McpToolInvoked("search_posts", params)
|
McpToolInvoked("search_posts", params)
|
||||||
McpToolInvoked("count_posts", params)
|
McpToolInvoked("count_posts", params)
|
||||||
McpToolInvoked("read_post_by_slug", slug, language)
|
McpToolInvoked("read_post_by_slug", slug, language)
|
||||||
|
McpToolInvoked("get_post_translations", post_id)
|
||||||
|
McpToolInvoked("get_media_translations", media_id)
|
||||||
|
McpToolInvoked("upsert_media_translation", params)
|
||||||
McpToolInvoked("draft_post", params)
|
McpToolInvoked("draft_post", params)
|
||||||
McpToolInvoked("propose_script", params)
|
McpToolInvoked("propose_script", params)
|
||||||
McpToolInvoked("propose_template", params)
|
McpToolInvoked("propose_template", params)
|
||||||
@@ -204,6 +207,30 @@ rule ReadPostBySlug {
|
|||||||
ensures: FullPostContent(post)
|
ensures: FullPostContent(post)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rule GetPostTranslations {
|
||||||
|
when: McpToolInvoked("get_post_translations", post_id)
|
||||||
|
-- Lists all available translations for a post.
|
||||||
|
ensures: PostTranslations(post_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
rule GetMediaTranslations {
|
||||||
|
when: McpToolInvoked("get_media_translations", media_id)
|
||||||
|
-- Lists all available translated metadata for a media item.
|
||||||
|
ensures: MediaTranslations(media_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
rule UpsertMediaTranslation {
|
||||||
|
when: McpToolInvoked("upsert_media_translation", params)
|
||||||
|
-- Creates or updates translated media metadata for a language.
|
||||||
|
ensures: media/UpsertMediaTranslationRequested(
|
||||||
|
params.media_id,
|
||||||
|
params.language,
|
||||||
|
params.title,
|
||||||
|
params.alt,
|
||||||
|
params.caption
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
-- Write tools (proposal-based)
|
-- Write tools (proposal-based)
|
||||||
|
|
||||||
rule DraftPost {
|
rule DraftPost {
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ defmodule BDS.MCPTest do
|
|||||||
assert "search_posts" in tool_names
|
assert "search_posts" in tool_names
|
||||||
assert "count_posts" in tool_names
|
assert "count_posts" in tool_names
|
||||||
assert "read_post_by_slug" in tool_names
|
assert "read_post_by_slug" in tool_names
|
||||||
|
assert "get_post_translations" in tool_names
|
||||||
|
assert "get_media_translations" in tool_names
|
||||||
|
assert "upsert_media_translation" in tool_names
|
||||||
assert "draft_post" in tool_names
|
assert "draft_post" in tool_names
|
||||||
assert "propose_script" in tool_names
|
assert "propose_script" in tool_names
|
||||||
assert "propose_template" in tool_names
|
assert "propose_template" in tool_names
|
||||||
@@ -72,6 +75,70 @@ defmodule BDS.MCPTest do
|
|||||||
assert read_result["post"]["slug"] == "travel-notes"
|
assert read_result["post"]["slug"] == "travel-notes"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "translation tools expose post and media translations and upsert media metadata", %{
|
||||||
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
|
assert {:ok, post} =
|
||||||
|
BDS.Posts.create_post(%{
|
||||||
|
project_id: project.id,
|
||||||
|
title: "Translatable Post",
|
||||||
|
content: "Source body",
|
||||||
|
language: "en"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:ok, _post_translation} =
|
||||||
|
BDS.Posts.upsert_post_translation(post.id, "de", %{
|
||||||
|
title: "Ubersetzter Beitrag",
|
||||||
|
excerpt: "Kurzfassung",
|
||||||
|
content: "Ubersetzter Inhalt"
|
||||||
|
})
|
||||||
|
|
||||||
|
source_path = Path.join(temp_dir, "translation-media.txt")
|
||||||
|
File.write!(source_path, "media body")
|
||||||
|
|
||||||
|
assert {:ok, media} =
|
||||||
|
BDS.Media.import_media(%{
|
||||||
|
project_id: project.id,
|
||||||
|
source_path: source_path,
|
||||||
|
title: "Source Media",
|
||||||
|
alt: "Source Alt",
|
||||||
|
caption: "Source Caption",
|
||||||
|
language: "en"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:ok, post_result} =
|
||||||
|
BDS.MCP.call_tool("get_post_translations", %{postId: post.id})
|
||||||
|
|
||||||
|
assert [post_translation] = post_result["translations"]
|
||||||
|
assert post_translation["language"] == "de"
|
||||||
|
assert post_translation["title"] == "Ubersetzter Beitrag"
|
||||||
|
assert post_translation["excerpt"] == "Kurzfassung"
|
||||||
|
assert post_translation["content"] == "Ubersetzter Inhalt"
|
||||||
|
assert post_translation["status"] == "draft"
|
||||||
|
|
||||||
|
assert {:ok, upsert_result} =
|
||||||
|
BDS.MCP.call_tool("upsert_media_translation", %{
|
||||||
|
mediaId: media.id,
|
||||||
|
language: "de",
|
||||||
|
title: "Medientitel",
|
||||||
|
alt: "Medien Alt",
|
||||||
|
caption: "Medien Beschriftung"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert upsert_result["translation"]["language"] == "de"
|
||||||
|
assert upsert_result["translation"]["title"] == "Medientitel"
|
||||||
|
|
||||||
|
assert {:ok, media_result} =
|
||||||
|
BDS.MCP.call_tool("get_media_translations", %{mediaId: media.id})
|
||||||
|
|
||||||
|
assert [media_translation] = media_result["translations"]
|
||||||
|
assert media_translation["language"] == "de"
|
||||||
|
assert media_translation["title"] == "Medientitel"
|
||||||
|
assert media_translation["alt"] == "Medien Alt"
|
||||||
|
assert media_translation["caption"] == "Medien Beschriftung"
|
||||||
|
end
|
||||||
|
|
||||||
test "proposal-backed write tools follow the old app lifecycle for scripts, templates, and metadata",
|
test "proposal-backed write tools follow the old app lifecycle for scripts, templates, and metadata",
|
||||||
%{
|
%{
|
||||||
project: project,
|
project: project,
|
||||||
|
|||||||
Reference in New Issue
Block a user