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
4 changes: 2 additions & 2 deletions internal/mcp/handlers_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ import (
// routeSourceAction routes "read" for GetSource and "edit" for WriteSource/EditSource.
func (s *Server) routeSourceAction(ctx context.Context, action, objectType, objectName string, params map[string]any) (*mcp.CallToolResult, bool, error) {
if action == "read" {
// GetSource covers: CLAS, PROG, INTF, FUNC, FUGR, INCL, DDLS, BDEF, SRVD, MSAG, VIEW
// GetSource covers: CLAS, PROG, INTF, FUNC, FUGR, INCL, DDLS, BDEF, SRVD, SRVB, MSAG, VIEW
switch objectType {
case "CLAS", "PROG", "INTF", "FUNC", "FUGR", "INCL", "DDLS", "BDEF", "SRVD", "MSAG", "VIEW":
case "CLAS", "PROG", "INTF", "FUNC", "FUGR", "INCL", "DDLS", "BDEF", "SRVD", "SRVB", "MSAG", "VIEW":
args := map[string]any{
"object_type": objectType,
"name": objectName,
Expand Down
4 changes: 2 additions & 2 deletions internal/mcp/tools_register.go
Original file line number Diff line number Diff line change
Expand Up @@ -964,10 +964,10 @@ func (s *Server) registerCRUDTools(shouldRegister func(string) bool) {
mcp.Description("For SRVB: the service definition name to bind"),
),
mcp.WithString("binding_version",
mcp.Description("For SRVB: OData version 'V2' or 'V4' (default: V2)"),
mcp.Description("For SRVB: OData version 'V2' or 'V4'. Defaults to 'V2' - pass 'V4' explicitly for Fiori Elements V4 apps, otherwise a V2 binding is created silently."),
),
mcp.WithString("binding_category",
mcp.Description("For SRVB: '0' for Web API, '1' for UI (default: 0)"),
mcp.Description("For SRVB: '0' = UI (User Interface), '1' = A2X (Web API). Default: '0' (UI). Values follow SAP domain SRVB_BND_CATEGORY."),
),
), s.handleCreateObject)
}
Expand Down
5 changes: 3 additions & 2 deletions pkg/adt/crud.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,8 @@ type CreateObjectOptions struct {
BindingType string `json:"bindingType,omitempty"`
// For SRVB: binding version ("V2" or "V4")
BindingVersion string `json:"bindingVersion,omitempty"`
// For SRVB: category ("0" for Web API, "1" for UI)
// For SRVB: category per SAP domain SRVB_BND_CATEGORY:
// "0" = UI (User Interface), "1" = A2X (Application to X users, i.e. Web API)
BindingCategory string `json:"bindingCategory,omitempty"`

// For BDEF: source code (required for creation - ADT API embeds source in creation request)
Expand Down Expand Up @@ -765,7 +766,7 @@ func buildCreateObjectBody(opts CreateObjectOptions, typeInfo objectTypeInfo, de
}
bindingCategory := opts.BindingCategory
if bindingCategory == "" {
bindingCategory = "0" // Web API
bindingCategory = "0" // UI (SRVB_BND_CATEGORY: 0=UI, 1=A2X/Web API)
}
return fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
<%s %s xmlns:adtcore="http://www.sap.com/adt/core"
Expand Down
93 changes: 93 additions & 0 deletions pkg/adt/crud_srvb_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package adt

import (
"strings"
"testing"
)

// srvbTypeInfo mirrors what CreateObject resolves for ObjectTypeSRVB.
func srvbTestTypeInfo(t *testing.T) objectTypeInfo {
t.Helper()
ti, ok := objectTypes[ObjectTypeSRVB]
if !ok {
t.Fatalf("ObjectTypeSRVB not present in objectTypes registry")
}
return ti
}

func buildSRVB(t *testing.T, opts CreateObjectOptions) string {
t.Helper()
opts.ObjectType = ObjectTypeSRVB
if opts.Name == "" {
opts.Name = "ZUI_TEST_O4"
}
if opts.PackageName == "" {
opts.PackageName = "$TMP"
}
if opts.ServiceDefinition == "" {
opts.ServiceDefinition = "ZUI_TEST"
}
return buildCreateObjectBody(opts, srvbTestTypeInfo(t), "DEVELOPER")
}

// The binding category values come from SAP domain SRVB_BND_CATEGORY:
//
// 0 = UI (User Interface)
// 1 = A2X (Application to X users) i.e. Web API
//
// The default must be UI ("0"), and an explicitly requested category must survive.
func TestBuildCreateObjectBody_SRVBCategory(t *testing.T) {
tests := []struct {
name string
category string
wantCategory string
}{
{name: "default is UI", category: "", wantCategory: `srvb:category="0"`},
{name: "explicit UI", category: "0", wantCategory: `srvb:category="0"`},
{name: "explicit A2X/Web API", category: "1", wantCategory: `srvb:category="1"`},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
body := buildSRVB(t, CreateObjectOptions{BindingCategory: tc.category})
if !strings.Contains(body, tc.wantCategory) {
t.Errorf("want %s in body, got:\n%s", tc.wantCategory, body)
}
})
}
}

// Regression: passing binding_version must actually reach the ADT payload.
// A silently-defaulted V2 binding for a caller that asked for V4 produces a
// service that cannot drive a Fiori Elements V4 app.
func TestBuildCreateObjectBody_SRVBVersion(t *testing.T) {
tests := []struct {
name string
version string
wantVersion string
}{
{name: "default is V2", version: "", wantVersion: `srvb:version="V2"`},
{name: "explicit V2", version: "V2", wantVersion: `srvb:version="V2"`},
{name: "explicit V4", version: "V4", wantVersion: `srvb:version="V4"`},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
body := buildSRVB(t, CreateObjectOptions{BindingVersion: tc.version})
if !strings.Contains(body, tc.wantVersion) {
t.Errorf("want %s in body, got:\n%s", tc.wantVersion, body)
}
})
}
}

// The bound service definition must be upper-cased and present.
func TestBuildCreateObjectBody_SRVBServiceDefinition(t *testing.T) {
body := buildSRVB(t, CreateObjectOptions{ServiceDefinition: "zui_flight_o4"})
if !strings.Contains(body, `<srvb:serviceDefinition adtcore:name="ZUI_FLIGHT_O4"/>`) {
t.Errorf("service definition not bound/upper-cased, got:\n%s", body)
}
if !strings.Contains(body, `srvb:type="ODATA"`) {
t.Errorf(`want srvb:type="ODATA", got:\n%s`, body)
}
}