Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 224 additions & 0 deletions internal/mcp/handlers_i18n.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
// Package mcp provides the MCP server implementation for ABAP ADT tools.
// handlers_i18n.go contains handlers for translation/internationalization operations.
package mcp

import (
"context"
"encoding/json"
"fmt"

"github.com/mark3labs/mcp-go/mcp"
"github.com/oisee/vibing-steampunk/pkg/adt"
)

// --- i18n Handlers ---

func (s *Server) handleGetObjectTextsInLanguage(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
objectURL, ok := request.Params.Arguments["object_url"].(string)
if !ok || objectURL == "" {
return newToolResultError("object_url is required"), nil
}

lang, ok := request.Params.Arguments["language"].(string)
if !ok || lang == "" {
return newToolResultError("language is required"), nil
}

content, err := s.adtClient.GetObjectTextsInLanguage(ctx, objectURL, lang)
if err != nil {
return newToolResultError(fmt.Sprintf("GetObjectTextsInLanguage failed: %v", err)), nil
}

return mcp.NewToolResultText(content), nil
}

func (s *Server) handleGetDataElementLabels(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
name, ok := request.Params.Arguments["name"].(string)
if !ok || name == "" {
return newToolResultError("name is required"), nil
}

lang, ok := request.Params.Arguments["language"].(string)
if !ok || lang == "" {
return newToolResultError("language is required"), nil
}

labels, err := s.adtClient.GetDataElementLabels(ctx, name, lang)
if err != nil {
return newToolResultError(fmt.Sprintf("GetDataElementLabels failed: %v", err)), nil
}

jsonBytes, err := json.MarshalIndent(labels, "", " ")
if err != nil {
return newToolResultError(fmt.Sprintf("Failed to format result: %v", err)), nil
}

return mcp.NewToolResultText(string(jsonBytes)), nil
}

func (s *Server) handleGetMessageClassTexts(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
name, ok := request.Params.Arguments["name"].(string)
if !ok || name == "" {
return newToolResultError("name is required"), nil
}

lang, ok := request.Params.Arguments["language"].(string)
if !ok || lang == "" {
return newToolResultError("language is required"), nil
}

texts, err := s.adtClient.GetMessageClassTexts(ctx, name, lang)
if err != nil {
return newToolResultError(fmt.Sprintf("GetMessageClassTexts failed: %v", err)), nil
}

if len(texts) == 0 {
return mcp.NewToolResultText("No messages found."), nil
}

jsonBytes, err := json.MarshalIndent(texts, "", " ")
if err != nil {
return newToolResultError(fmt.Sprintf("Failed to format result: %v", err)), nil
}

return mcp.NewToolResultText(string(jsonBytes)), nil
}

func (s *Server) handleWriteMessageClassTexts(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
name, ok := request.Params.Arguments["name"].(string)
if !ok || name == "" {
return newToolResultError("name is required"), nil
}

lang, ok := request.Params.Arguments["language"].(string)
if !ok || lang == "" {
return newToolResultError("language is required"), nil
}

lockHandle, ok := request.Params.Arguments["lock_handle"].(string)
if !ok || lockHandle == "" {
return newToolResultError("lock_handle is required"), nil
}

transport, _ := request.Params.Arguments["transport"].(string)

// Parse texts from arguments
textsRaw, ok := request.Params.Arguments["texts"]
if !ok || textsRaw == nil {
return newToolResultError("texts is required"), nil
}

textsJSON, err := json.Marshal(textsRaw)
if err != nil {
return newToolResultError(fmt.Sprintf("Failed to parse texts: %v", err)), nil
}

var texts []adt.MessageClassMessage
if err := json.Unmarshal(textsJSON, &texts); err != nil {
return newToolResultError(fmt.Sprintf("Failed to parse texts: %v", err)), nil
}

err = s.adtClient.WriteMessageClassTexts(ctx, name, lang, texts, lockHandle, transport)
if err != nil {
return newToolResultError(fmt.Sprintf("WriteMessageClassTexts failed: %v", err)), nil
}

return mcp.NewToolResultText(fmt.Sprintf("Message class %s texts updated successfully in language %s.", name, lang)), nil
}

func (s *Server) handleWriteDataElementLabels(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
name, ok := request.Params.Arguments["name"].(string)
if !ok || name == "" {
return newToolResultError("name is required"), nil
}

lang, ok := request.Params.Arguments["language"].(string)
if !ok || lang == "" {
return newToolResultError("language is required"), nil
}

lockHandle, ok := request.Params.Arguments["lock_handle"].(string)
if !ok || lockHandle == "" {
return newToolResultError("lock_handle is required"), nil
}

transport, _ := request.Params.Arguments["transport"].(string)

labels := &adt.DataElementLabels{}
if short, ok := request.Params.Arguments["short"].(string); ok {
labels.Short = short
}
if medium, ok := request.Params.Arguments["medium"].(string); ok {
labels.Medium = medium
}
if long, ok := request.Params.Arguments["long"].(string); ok {
labels.Long = long
}
if heading, ok := request.Params.Arguments["heading"].(string); ok {
labels.Heading = heading
}

err := s.adtClient.WriteDataElementLabels(ctx, name, lang, labels, lockHandle, transport)
if err != nil {
return newToolResultError(fmt.Sprintf("WriteDataElementLabels failed: %v", err)), nil
}

return mcp.NewToolResultText(fmt.Sprintf("Data element %s labels updated successfully in language %s.", name, lang)), nil
}

func (s *Server) handleGetTextPoolInLanguage(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
programName, ok := request.Params.Arguments["program_name"].(string)
if !ok || programName == "" {
return newToolResultError("program_name is required"), nil
}

lang, ok := request.Params.Arguments["language"].(string)
if !ok || lang == "" {
return newToolResultError("language is required"), nil
}

entries, err := s.adtClient.GetTextPoolInLanguage(ctx, programName, lang)
if err != nil {
return newToolResultError(fmt.Sprintf("GetTextPoolInLanguage failed: %v", err)), nil
}

if len(entries) == 0 {
return mcp.NewToolResultText("No text pool entries found."), nil
}

jsonBytes, err := json.MarshalIndent(entries, "", " ")
if err != nil {
return newToolResultError(fmt.Sprintf("Failed to format result: %v", err)), nil
}

return mcp.NewToolResultText(string(jsonBytes)), nil
}

func (s *Server) handleCompareObjectLanguages(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
objectURL, ok := request.Params.Arguments["object_url"].(string)
if !ok || objectURL == "" {
return newToolResultError("object_url is required"), nil
}

sourceLang, ok := request.Params.Arguments["source_language"].(string)
if !ok || sourceLang == "" {
return newToolResultError("source_language is required"), nil
}

targetLang, ok := request.Params.Arguments["target_language"].(string)
if !ok || targetLang == "" {
return newToolResultError("target_language is required"), nil
}

comparison, err := s.adtClient.CompareObjectLanguages(ctx, objectURL, sourceLang, targetLang)
if err != nil {
return newToolResultError(fmt.Sprintf("CompareObjectLanguages failed: %v", err)), nil
}

jsonBytes, err := json.MarshalIndent(comparison, "", " ")
if err != nil {
return newToolResultError(fmt.Sprintf("Failed to format result: %v", err)), nil
}

return mcp.NewToolResultText(string(jsonBytes)), nil
}
142 changes: 142 additions & 0 deletions internal/mcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,11 @@ func (s *Server) registerTools(mode string, disabledGroups string, toolsConfig m
"G": { // Git/abapGit tools (via ZADT_VSP WebSocket)
"GitTypes", "GitExport",
},
"N": { // i18N/Translation tools
"GetObjectTextsInLanguage", "GetDataElementLabels", "GetMessageClassTexts",
"WriteMessageClassTexts", "WriteDataElementLabels",
"GetTextPoolInLanguage", "CompareObjectLanguages",
},
"R": { // Report execution tools (via ZADT_VSP WebSocket)
"RunReport", "GetVariants", "GetTextElements", "SetTextElements",
},
Expand Down Expand Up @@ -399,6 +404,13 @@ func (s *Server) registerTools(mode string, disabledGroups string, toolsConfig m
"GitTypes": true, // List 158 supported object types
"GitExport": true, // Export packages/objects to abapGit ZIP

// i18n/Translation (read-only in focused mode, write in expert)
"GetObjectTextsInLanguage": true, // Get object source in specific language
"GetDataElementLabels": true, // Get data element labels in language
"GetMessageClassTexts": true, // Get message class texts in language
"GetTextPoolInLanguage": true, // Get program text pool in language
"CompareObjectLanguages": true, // Compare texts between two languages

// Report Execution (via ZADT_VSP WebSocket)
"RunReport": true, // Execute reports with params/variants, capture ALV
"RunReportAsync": true, // Background report execution with polling
Expand Down Expand Up @@ -2207,6 +2219,136 @@ func (s *Server) registerTools(mode string, disabledGroups string, toolsConfig m
), s.handleDeleteTransport)
}

// --- i18n/Translation Tools ---

if shouldRegister("GetObjectTextsInLanguage") {
s.mcpServer.AddTool(mcp.NewTool("GetObjectTextsInLanguage",
mcp.WithDescription("Get the source/content of an ABAP object in a specific language. Overrides the global SAP language for this request."),
mcp.WithString("object_url",
mcp.Required(),
mcp.Description("ADT source URL (e.g., /sap/bc/adt/programs/programs/ZTEST/source/main)"),
),
mcp.WithString("language",
mcp.Required(),
mcp.Description("SAP language code (e.g., EN, FR, DE)"),
),
), s.handleGetObjectTextsInLanguage)
}

if shouldRegister("GetDataElementLabels") {
s.mcpServer.AddTool(mcp.NewTool("GetDataElementLabels",
mcp.WithDescription("Get the text labels (short, medium, long, heading) of a data element in a specific language."),
mcp.WithString("name",
mcp.Required(),
mcp.Description("Data element name"),
),
mcp.WithString("language",
mcp.Required(),
mcp.Description("SAP language code (e.g., EN, FR, DE)"),
),
), s.handleGetDataElementLabels)
}

if shouldRegister("GetMessageClassTexts") {
s.mcpServer.AddTool(mcp.NewTool("GetMessageClassTexts",
mcp.WithDescription("Get all messages of a message class in a specific language."),
mcp.WithString("name",
mcp.Required(),
mcp.Description("Message class name"),
),
mcp.WithString("language",
mcp.Required(),
mcp.Description("SAP language code (e.g., EN, FR, DE)"),
),
), s.handleGetMessageClassTexts)
}

if shouldRegister("WriteMessageClassTexts") {
s.mcpServer.AddTool(mcp.NewTool("WriteMessageClassTexts",
mcp.WithDescription("Update message class texts in a specific language. Requires lock handle from LockObject. Expert mode only."),
mcp.WithString("name",
mcp.Required(),
mcp.Description("Message class name"),
),
mcp.WithString("language",
mcp.Required(),
mcp.Description("SAP language code (e.g., EN, FR, DE)"),
),
mcp.WithString("lock_handle",
mcp.Required(),
mcp.Description("Lock handle from LockObject"),
),
mcp.WithString("transport",
mcp.Description("Transport request number (optional, for transportable objects)"),
),
), s.handleWriteMessageClassTexts)
}

if shouldRegister("WriteDataElementLabels") {
s.mcpServer.AddTool(mcp.NewTool("WriteDataElementLabels",
mcp.WithDescription("Update data element labels in a specific language. Requires lock handle from LockObject. Expert mode only."),
mcp.WithString("name",
mcp.Required(),
mcp.Description("Data element name"),
),
mcp.WithString("language",
mcp.Required(),
mcp.Description("SAP language code (e.g., EN, FR, DE)"),
),
mcp.WithString("short",
mcp.Description("Short description label"),
),
mcp.WithString("medium",
mcp.Description("Medium description label"),
),
mcp.WithString("long",
mcp.Description("Long description label"),
),
mcp.WithString("heading",
mcp.Description("Heading/column header label"),
),
mcp.WithString("lock_handle",
mcp.Required(),
mcp.Description("Lock handle from LockObject"),
),
mcp.WithString("transport",
mcp.Description("Transport request number (optional, for transportable objects)"),
),
), s.handleWriteDataElementLabels)
}

if shouldRegister("GetTextPoolInLanguage") {
s.mcpServer.AddTool(mcp.NewTool("GetTextPoolInLanguage",
mcp.WithDescription("Get the text pool (text elements/selection texts) of a program in a specific language."),
mcp.WithString("program_name",
mcp.Required(),
mcp.Description("ABAP program name"),
),
mcp.WithString("language",
mcp.Required(),
mcp.Description("SAP language code (e.g., EN, FR, DE)"),
),
), s.handleGetTextPoolInLanguage)
}

if shouldRegister("CompareObjectLanguages") {
s.mcpServer.AddTool(mcp.NewTool("CompareObjectLanguages",
mcp.WithDescription("Compare the text content of an ABAP object between two languages. Shows differences and missing translations."),
mcp.WithString("object_url",
mcp.Required(),
mcp.Description("ADT source URL (e.g., /sap/bc/adt/programs/programs/ZTEST/source/main)"),
),
mcp.WithString("source_language",
mcp.Required(),
mcp.Description("Source language code (e.g., EN)"),
),
mcp.WithString("target_language",
mcp.Required(),
mcp.Description("Target language code (e.g., FR)"),
),
), s.handleCompareObjectLanguages)
}

// --- Git/abapGit Integration (via ZADT_VSP WebSocket) ---

// GitTypes
Expand Down
Loading