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.
|
||||
- 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`.
|
||||
- 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("count_posts", 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("propose_script", false),
|
||||
tool("propose_template", false),
|
||||
@@ -46,6 +49,9 @@ defmodule BDS.MCP.Tools do
|
||||
"search_posts" -> {:ok, search_posts(params)}
|
||||
"count_posts" -> {:ok, count_posts(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)
|
||||
"propose_script" -> propose_script(params)
|
||||
"propose_template" -> propose_template(params)
|
||||
@@ -165,6 +171,47 @@ defmodule BDS.MCP.Tools do
|
||||
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
|
||||
project = Queries.active_project!()
|
||||
|
||||
|
||||
@@ -83,6 +83,9 @@ surface McpAutomationSurface {
|
||||
McpToolInvoked("search_posts", params)
|
||||
McpToolInvoked("count_posts", params)
|
||||
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("propose_script", params)
|
||||
McpToolInvoked("propose_template", params)
|
||||
@@ -204,6 +207,30 @@ rule ReadPostBySlug {
|
||||
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)
|
||||
|
||||
rule DraftPost {
|
||||
|
||||
@@ -29,6 +29,9 @@ defmodule BDS.MCPTest do
|
||||
assert "search_posts" in tool_names
|
||||
assert "count_posts" 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 "propose_script" in tool_names
|
||||
assert "propose_template" in tool_names
|
||||
@@ -72,6 +75,70 @@ defmodule BDS.MCPTest do
|
||||
assert read_result["post"]["slug"] == "travel-notes"
|
||||
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",
|
||||
%{
|
||||
project: project,
|
||||
|
||||
Reference in New Issue
Block a user