feat: alignment MCP translation tools

This commit is contained in:
2026-05-01 18:29:51 +02:00
parent 661bc0037c
commit 6e6a751db0
4 changed files with 142 additions and 1 deletions

View File

@@ -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.

View File

@@ -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!()

View File

@@ -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 {

View File

@@ -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,