Skip to content
Draft
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
8 changes: 5 additions & 3 deletions cmd/vsp/devops.go
Original file line number Diff line number Diff line change
Expand Up @@ -1446,7 +1446,7 @@ func runSourceEdit(cmd *cobra.Command, args []string) error {
// Build object URL from type + name
objectURL := buildObjectURL(objType, name)
if objectURL == "" {
return fmt.Errorf("unsupported object type: %s (supported: CLAS, PROG, INTF)", objType)
return fmt.Errorf("unsupported object type: %s (supported: CLAS, PROG, INCL, INTF)", objType)
}

ctx := context.Background()
Expand Down Expand Up @@ -1563,7 +1563,7 @@ func runTest(cmd *cobra.Command, args []string) error {
name := strings.ToUpper(args[1])
objectURL = buildObjectURL(objType, name)
if objectURL == "" {
return fmt.Errorf("unsupported object type: %s (supported: CLAS, PROG, INTF)", objType)
return fmt.Errorf("unsupported object type: %s (supported: CLAS, PROG, INCL, INTF)", objType)
}
}

Expand Down Expand Up @@ -1643,7 +1643,7 @@ func runATC(cmd *cobra.Command, args []string) error {

objectURL := buildObjectURL(objType, name)
if objectURL == "" {
return fmt.Errorf("unsupported object type: %s (supported: CLAS, PROG, INTF)", objType)
return fmt.Errorf("unsupported object type: %s (supported: CLAS, PROG, INCL, INTF)", objType)
}

ctx := context.Background()
Expand Down Expand Up @@ -3406,6 +3406,8 @@ func buildObjectURL(objType, name string) string {
return fmt.Sprintf("/sap/bc/adt/oo/classes/%s", name)
case "PROG":
return fmt.Sprintf("/sap/bc/adt/programs/programs/%s", name)
case "INCL":
return fmt.Sprintf("/sap/bc/adt/programs/includes/%s", name)
case "INTF":
return fmt.Sprintf("/sap/bc/adt/oo/interfaces/%s", name)
case "FUGR":
Expand Down
14 changes: 7 additions & 7 deletions internal/mcp/handlers_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func (s *Server) routeSourceAction(ctx context.Context, action, objectType, obje
if action == "edit" {
// High-level WriteSource
switch objectType {
case "CLAS", "PROG", "INTF", "DDLS", "BDEF", "SRVD":
case "CLAS", "PROG", "INTF", "INCL", "DDLS", "BDEF", "SRVD":
if src := getStringParam(params, "source"); src != "" {
args := map[string]any{
"object_type": objectType,
Expand Down Expand Up @@ -118,10 +118,10 @@ func (s *Server) registerGetSource() {
// registerWriteSource registers the unified WriteSource tool
func (s *Server) registerWriteSource() {
s.mcpServer.AddTool(mcp.NewTool("WriteSource",
mcp.WithDescription("Unified tool for writing ABAP source code with automatic create/update detection. Supports PROG, CLAS, INTF, and RAP types (DDLS, BDEF, SRVD)."),
mcp.WithDescription("Unified tool for writing ABAP source code with automatic create/update detection. Supports PROG, CLAS, INTF, INCL, and RAP types (DDLS, BDEF, SRVD)."),
mcp.WithString("object_type",
mcp.Required(),
mcp.Description("Object type: PROG (program), CLAS (class), INTF (interface), DDLS (CDS view), BDEF (behavior definition), SRVD (service definition)"),
mcp.Description("Object type: PROG (program), CLAS (class), INTF (interface), INCL (include), DDLS (CDS view), BDEF (behavior definition), SRVD (service definition)"),
),
mcp.WithString("name",
mcp.Required(),
Expand Down Expand Up @@ -302,10 +302,10 @@ func (s *Server) registerGrepPackages() {
// registerImportFromFile registers the ImportFromFile tool (alias for DeployFromFile)
func (s *Server) registerImportFromFile() {
s.mcpServer.AddTool(mcp.NewTool("ImportFromFile",
mcp.WithDescription("Import ABAP object from local file into SAP system. Auto-detects object type from file extension, creates or updates, activates. Supports: programs, classes (with includes), interfaces, function groups/modules, CDS views (DDLS), behavior definitions (BDEF), service definitions (SRVD). For class includes (.clas.testclasses.abap, .clas.locals_def.abap, etc.), the parent class must exist."),
mcp.WithDescription("Import ABAP object from local file into SAP system. Auto-detects object type from file extension, creates or updates, activates. Supports: programs, includes, classes (with includes), interfaces, function groups/modules, CDS views (DDLS), behavior definitions (BDEF), service definitions (SRVD). For class includes (.clas.testclasses.abap, .clas.locals_def.abap, etc.), the parent class must exist."),
mcp.WithString("file_path",
mcp.Required(),
mcp.Description("Absolute path to ABAP source file. Supported extensions: .prog.abap, .clas.abap, .clas.testclasses.abap, .clas.locals_def.abap, .clas.locals_imp.abap, .intf.abap, .fugr.abap, .func.abap, .ddls.asddls, .bdef.asbdef, .srvd.srvdsrv"),
mcp.Description("Absolute path to ABAP source file. Supported extensions: .prog.abap, .incl.abap, .clas.abap, .clas.testclasses.abap, .clas.locals_def.abap, .clas.locals_imp.abap, .intf.abap, .fugr.abap, .func.abap, .ddls.asddls, .bdef.asbdef, .srvd.srvdsrv"),
),
mcp.WithString("package_name",
mcp.Description("Target package name (required for new objects, not needed for class includes)"),
Expand All @@ -319,10 +319,10 @@ func (s *Server) registerImportFromFile() {
// registerExportToFile registers the ExportToFile tool (alias for SaveToFile)
func (s *Server) registerExportToFile() {
s.mcpServer.AddTool(mcp.NewTool("ExportToFile",
mcp.WithDescription("Export ABAP object from SAP system to local file. Saves source code with appropriate file extension. Supports: programs, classes (with includes), interfaces, function groups/modules, CDS views (DDLS), behavior definitions (BDEF), service definitions (SRVD). For classes, use 'include' parameter to export specific includes (testclasses, definitions, implementations, macros)."),
mcp.WithDescription("Export ABAP object from SAP system to local file. Saves source code with appropriate file extension. Supports: programs, includes, classes (with includes), interfaces, function groups/modules, CDS views (DDLS), behavior definitions (BDEF), service definitions (SRVD). For classes, use 'include' parameter to export specific includes (testclasses, definitions, implementations, macros)."),
mcp.WithString("object_type",
mcp.Required(),
mcp.Description("Object type: PROG, CLAS, INTF, FUGR, FUNC, DDLS, BDEF, SRVD"),
mcp.Description("Object type: PROG, INCL, CLAS, INTF, FUGR, FUNC, DDLS, BDEF, SRVD"),
),
mcp.WithString("object_name",
mcp.Required(),
Expand Down
14 changes: 10 additions & 4 deletions pkg/adt/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,17 @@ func (c *Client) getObjectPackage(ctx context.Context, objectURL string) (string
func normalizeObjectURLForPackageCheck(objectURL string) string {
normalized := strings.TrimSuffix(objectURL, "/")

if idx := strings.Index(normalized, "/includes/"); idx >= 0 {
return normalized[:idx]
}
if strings.HasSuffix(normalized, "/source/main") {
return strings.TrimSuffix(normalized, "/source/main")
normalized = strings.TrimSuffix(normalized, "/source/main")
}

// Strip /includes/... only for class sub-resources (e.g. /oo/classes/ZCL_FOO/includes/locals_def).
// Program includes use /programs/includes/NAME where /includes/ is the collection path — don't strip.
if idx := strings.Index(normalized, "/includes/"); idx >= 0 {
prefix := normalized[:idx]
if !strings.HasSuffix(prefix, "/programs") {
return prefix
}
}

return normalized
Expand Down
5 changes: 4 additions & 1 deletion pkg/adt/devtools.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ func (c *Client) SyntaxCheck(ctx context.Context, objectURL string, content stri
// SAP's URI length limit for long namespaced classes.
checkObjectURI := objectURL
artifactURI := objectURL
if !strings.Contains(objectURL, "/includes/") {
// Class includes (/oo/classes/ZCL_FOO/includes/testclasses) have no /source/main suffix.
// Program includes (/programs/includes/ZZ_NAME) DO need /source/main — /includes/ here is the collection path.
isClassInclude := strings.Contains(objectURL, "/oo/classes/") && strings.Contains(objectURL, "/includes/")
if !isClassInclude {
artifactURI = objectURL + "/source/main"
}
encodedContent := base64.StdEncoding.EncodeToString([]byte(content))
Expand Down
5 changes: 4 additions & 1 deletion pkg/adt/fileparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ func ParseABAPFile(filePath string) (*ABAPFileInfo, error) {
info.ClassIncludeType = ClassIncludeMain
case strings.HasSuffix(baseName, ".prog.abap"):
info.ObjectType = ObjectTypeProgram
case strings.HasSuffix(baseName, ".incl.abap"):
info.ObjectType = ObjectTypeInclude
info.ObjectName = strings.ToUpper(strings.ReplaceAll(strings.TrimSuffix(baseName, ".incl.abap"), "#", "/"))
case strings.HasSuffix(baseName, ".intf.abap"):
info.ObjectType = ObjectTypeInterface
case strings.HasSuffix(baseName, ".fugr.abap"):
Expand All @@ -130,7 +133,7 @@ func ParseABAPFile(filePath string) (*ABAPFileInfo, error) {
// Generic .abap: detect from content
return parseFromContent(filePath)
default:
return nil, fmt.Errorf("unsupported file extension: %s (expected .clas.abap, .clas.testclasses.abap, .clas.locals_def.abap, .clas.locals_imp.abap, .prog.abap, .intf.abap, .fugr.abap, .func.abap, .ddls.asddls, .bdef.asbdef, or .srvd.srvdsrv)", ext)
return nil, fmt.Errorf("unsupported file extension: %s (expected .clas.abap, .clas.testclasses.abap, .clas.locals_def.abap, .clas.locals_imp.abap, .prog.abap, .incl.abap, .intf.abap, .fugr.abap, .func.abap, .ddls.asddls, .bdef.asbdef, or .srvd.srvdsrv)", ext)
}

// 2. Parse file content to extract name and metadata
Expand Down
82 changes: 82 additions & 0 deletions pkg/adt/workflows.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,88 @@ func (c *Client) WriteProgram(ctx context.Context, programName string, source st
return result, nil
}

// WriteIncludeResult represents the result of writing an ABAP include.
type WriteIncludeResult struct {
Success bool `json:"success"`
IncludeName string `json:"includeName"`
ObjectURL string `json:"objectUrl"`
SyntaxErrors []SyntaxCheckResult `json:"syntaxErrors,omitempty"`
Activation *ActivationResult `json:"activation,omitempty"`
Message string `json:"message,omitempty"`
}

// WriteInclude performs Lock -> SyntaxCheck -> UpdateSource -> Unlock -> Activate for an ABAP include.
func (c *Client) WriteInclude(ctx context.Context, includeName string, source string, transport string) (*WriteIncludeResult, error) {
includeName = strings.ToUpper(includeName)
objectURL := fmt.Sprintf("/sap/bc/adt/programs/includes/%s", url.PathEscape(includeName))
sourceURL := objectURL + "/source/main"

if err := c.checkMutation(ctx, MutationContext{
Op: OpWorkflow,
OpName: "WriteInclude",
ObjectURL: objectURL,
Transport: transport,
}); err != nil {
return nil, err
}

result := &WriteIncludeResult{
IncludeName: includeName,
ObjectURL: objectURL,
}

syntaxErrors, err := c.SyntaxCheck(ctx, objectURL, source)
if err != nil {
result.Message = fmt.Sprintf("Syntax check failed: %v", err)
return result, nil
}
for _, se := range syntaxErrors {
if se.Severity == "E" || se.Severity == "A" || se.Severity == "X" {
result.SyntaxErrors = syntaxErrors
result.Message = "Source has syntax errors - not saved"
return result, nil
}
}
result.SyntaxErrors = syntaxErrors

lock, err := c.LockObject(ctx, objectURL, "MODIFY")
if err != nil {
result.Message = fmt.Sprintf("Failed to lock object: %v", err)
return result, nil
}
defer func() {
if !result.Success {
c.UnlockObject(ctx, objectURL, lock.LockHandle)
}
}()

if err = c.UpdateSource(ctx, sourceURL, source, lock.LockHandle, transport); err != nil {
result.Message = fmt.Sprintf("Failed to update source: %v", err)
return result, nil
}

if err = c.UnlockObject(ctx, objectURL, lock.LockHandle); err != nil {
result.Message = fmt.Sprintf("Failed to unlock object: %v", err)
return result, nil
}

activation, err := c.Activate(ctx, objectURL, includeName)
if err != nil {
result.Message = fmt.Sprintf("Failed to activate: %v", err)
result.Activation = activation
return result, nil
}

result.Activation = activation
if activation.Success {
result.Success = true
result.Message = "Include updated and activated successfully"
} else {
result.Message = "Activation failed - check activation messages"
}
return result, nil
}

// WriteClassResult represents the result of writing a class.
type WriteClassResult struct {
Success bool `json:"success"`
Expand Down
47 changes: 24 additions & 23 deletions pkg/adt/workflows_deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,29 +224,8 @@ func (c *Client) UpdateFromFile(ctx context.Context, filePath, transport string)
return nil, err
}

// 4. Lock object
lockResult, err := c.LockObject(ctx, objectURL, "MODIFY")
if err != nil {
return &DeployResult{
FilePath: filePath,
ObjectURL: objectURL,
ObjectName: info.ObjectName,
ObjectType: string(info.ObjectType),
Success: false,
Errors: []string{fmt.Sprintf("lock failed: %v", err)},
Message: fmt.Sprintf("Failed to lock object: %v", err),
}, nil
}

// Ensure unlock on any error
unlocked := false
defer func() {
if !unlocked {
_ = c.UnlockObject(ctx, objectURL, lockResult.LockHandle)
}
}()

// 5. Syntax check (skip for class includes - will check after update)
// 4. Syntax check before lock — must run before acquiring lock to avoid
// breaking the stateful SAP session (SyntaxCheck uses a stateless request).
if !isClassInclude {
syntaxErrors, err := c.SyntaxCheck(ctx, objectURL, source)
if err != nil {
Expand Down Expand Up @@ -279,6 +258,28 @@ func (c *Client) UpdateFromFile(ctx context.Context, filePath, transport string)
}
}

// 5. Lock object
lockResult, err := c.LockObject(ctx, objectURL, "MODIFY")
if err != nil {
return &DeployResult{
FilePath: filePath,
ObjectURL: objectURL,
ObjectName: info.ObjectName,
ObjectType: string(info.ObjectType),
Success: false,
Errors: []string{fmt.Sprintf("lock failed: %v", err)},
Message: fmt.Sprintf("Failed to lock object: %v", err),
}, nil
}

// Ensure unlock on any error
unlocked := false
defer func() {
if !unlocked {
_ = c.UnlockObject(ctx, objectURL, lockResult.LockHandle)
}
}()

// 6. Write source
if isClassInclude {
// For class includes, use UpdateClassInclude
Expand Down
5 changes: 3 additions & 2 deletions pkg/adt/workflows_edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,9 @@ func (c *Client) EditSourceWithOptions(ctx context.Context, objectURL, oldString
result.Method = opts.Method
}

// Detect if this is a class include (e.g., /sap/bc/adt/oo/classes/ZCL_FOO/includes/testclasses)
isClassInclude := strings.Contains(objectURL, "/includes/")
// Detect if this is a class include (e.g., /sap/bc/adt/oo/classes/ZCL_FOO/includes/testclasses).
// Program includes (/programs/includes/ZZ_NAME) are NOT class includes — /includes/ is their collection path.
isClassInclude := strings.Contains(objectURL, "/oo/classes/") && strings.Contains(objectURL, "/includes/")
var className string
var includeType ClassIncludeType
var parentClassURL string
Expand Down
43 changes: 41 additions & 2 deletions pkg/adt/workflows_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,10 +218,10 @@ func (c *Client) WriteSource(ctx context.Context, objectType, name, source strin

// Validate object type
switch objectType {
case "PROG", "CLAS", "INTF", "DDLS", "BDEF", "SRVD", "SRVB":
case "PROG", "CLAS", "INTF", "INCL", "DDLS", "BDEF", "SRVD", "SRVB":
// Supported types
default:
result.Message = fmt.Sprintf("Unsupported object type: %s (supported: PROG, CLAS, INTF, DDLS, BDEF, SRVD, SRVB)", objectType)
result.Message = fmt.Sprintf("Unsupported object type: %s (supported: PROG, CLAS, INTF, INCL, DDLS, BDEF, SRVD, SRVB)", objectType)
return result, nil
}

Expand All @@ -239,6 +239,9 @@ func (c *Client) WriteSource(ctx context.Context, objectType, name, source strin
case "INTF":
_, err := c.GetInterface(ctx, name)
objectExists = (err == nil)
case "INCL":
_, err := c.GetInclude(ctx, name)
objectExists = (err == nil)
case "DDLS":
_, err := c.GetDDLS(ctx, name)
objectExists = (err == nil)
Expand Down Expand Up @@ -317,6 +320,29 @@ func (c *Client) writeSourceCreate(ctx context.Context, objectType, name, source
result.Message = progResult.Message
return result, nil

case "INCL":
if err := c.CreateObject(ctx, CreateObjectOptions{
ObjectType: ObjectTypeInclude,
Name: name,
Description: opts.Description,
PackageName: opts.Package,
Transport: opts.Transport,
}); err != nil {
result.Message = fmt.Sprintf("Failed to create include: %v", err)
return result, nil
}
inclResult, err := c.WriteInclude(ctx, name, source, opts.Transport)
if err != nil {
result.Message = fmt.Sprintf("Failed to write include source: %v", err)
return result, nil
}
result.Success = inclResult.Success
result.ObjectURL = inclResult.ObjectURL
result.SyntaxErrors = inclResult.SyntaxErrors
result.Activation = inclResult.Activation
result.Message = inclResult.Message
return result, nil

case "CLAS":
if opts.TestSource != "" {
classResult, err := c.CreateClassWithTests(ctx, name, opts.Description, opts.Package, source, opts.TestSource, opts.Transport)
Expand Down Expand Up @@ -683,6 +709,19 @@ func (c *Client) writeSourceUpdate(ctx context.Context, objectType, name, source
result.Message = progResult.Message
return result, nil

case "INCL":
inclResult, err := c.WriteInclude(ctx, name, source, opts.Transport)
if err != nil {
result.Message = fmt.Sprintf("Failed to update include: %v", err)
return result, nil
}
result.Success = inclResult.Success
result.ObjectURL = inclResult.ObjectURL
result.SyntaxErrors = inclResult.SyntaxErrors
result.Activation = inclResult.Activation
result.Message = inclResult.Message
return result, nil

case "CLAS":
// Method-level update: replace only the specified method
if opts.Method != "" {
Expand Down