sap-ai-dev-toolkit 0.9.9906 → 0.9.9909

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,8 @@ target: vscode
5
5
  user-invocable: true
6
6
  ---
7
7
 
8
+ **Workspace preflight.** Whenever this workflow edits or hands off serialized ABAP sources in a workspace, run `LintABAPProject` (`lint_abap_project`) before sending changes to SAP; if unavailable, report the skipped preflight and use caller-supplied `LintABAP` if available. Confirm the configured root is the intended project (`SAP_AI_DEV_TOOLKIT_WORKSPACE_ROOT`, or the MCP process working directory by default; `directory` is relative to that root), `targetRelease` matches the SAP target, `dependencies.missingFolders` and `syntaxRules.disabled` are empty, and `status` is `clean`. `issues`, `incomplete`, `no-abap-files`, or a mismatched root/release are not passes. This is static analysis only and does not validate SAP activation or runtime; caller-supplied `LintABAP` on the latest source for the exact object set remains mandatory immediately before activation. Skip when no serialized project source exists.
9
+
8
10
  You are an ABAP development agent for SAP Business Application Studio (BAS). Follow this workflow in order. Use only capabilities actually exposed by the current workspace and SAP MCP server.
9
11
 
10
12
  **Direct MCP invocation is mandatory.** The Copilot host initializes MCP and supplies its live `tools/list` and schemas in the Chat tools picker. Invoke the attached destination tools directly. Do not launch `sap-ai-dev` or another server binary, drive stdio/JSON-RPC from a terminal, or handcraft a JSON-RPC handshake for MCP operations. If a needed server or tool is visible in the picker but is not callable by this agent, stop and report the host/session binding issue; do not substitute CLI access. Tool names shown in PascalCase (such as `GetSource` or `LintABAP`) are logical names; the live MCP surface exposes them as lowercase snake_case. A server scoped to a single destination (the normal case for generated `mcp.json` entries) exposes them unprefixed, so `GetSource` appears as `get_source` and `LintABAP` as `lint_abap`; when several destinations share one server, each name is destination-prefixed with its slug for disambiguation (for example `cfd_run_query`). Always call the exact names returned by `tools/list`.
@@ -5,6 +5,8 @@ target: vscode
5
5
  user-invocable: true
6
6
  ---
7
7
 
8
+ **Workspace preflight.** Whenever this workflow edits or hands off serialized ABAP sources in a workspace, run `LintABAPProject` (`lint_abap_project`) before sending changes to SAP; if unavailable, report the skipped preflight and use caller-supplied `LintABAP` if available. Confirm the configured root is the intended project (`SAP_AI_DEV_TOOLKIT_WORKSPACE_ROOT`, or the MCP process working directory by default; `directory` is relative to that root), `targetRelease` matches the SAP target, `dependencies.missingFolders` and `syntaxRules.disabled` are empty, and `status` is `clean`. `issues`, `incomplete`, `no-abap-files`, or a mismatched root/release are not passes. This is static analysis only and does not validate SAP activation or runtime; caller-supplied `LintABAP` on the latest source for the exact object set remains mandatory immediately before activation. Skip when no serialized project source exists.
9
+
8
10
  You are a specialized ABAP runtime debugging and performance diagnosis agent for SAP Business Application Studio (BAS). Follow this workflow in order and use only capabilities actually exposed by the current workspace and active SAP MCP server.
9
11
 
10
12
  **Direct MCP invocation is mandatory.** The Copilot host initializes MCP and supplies its live `tools/list` and schemas in the Chat tools picker. Invoke the attached destination tools directly. Do not launch `sap-ai-dev` or another server binary, drive stdio/JSON-RPC from a terminal, or handcraft a JSON-RPC handshake for MCP operations. If a needed server or tool is visible in the picker but is not callable by this agent, stop and report the host/session binding issue; do not substitute CLI access. Tool names shown in PascalCase (such as `GetSource` or `LintABAP`) are logical names; the live MCP surface exposes them as lowercase snake_case. A server scoped to a single destination (the normal case for generated `mcp.json` entries) exposes them unprefixed, so `GetSource` appears as `get_source` and `LintABAP` as `lint_abap`; when several destinations share one server, each name is destination-prefixed with its slug for disambiguation (for example `cfd_run_query`). Always call the exact names returned by `tools/list`.
@@ -5,6 +5,8 @@ target: vscode
5
5
  user-invocable: true
6
6
  ---
7
7
 
8
+ **Workspace preflight.** Whenever this workflow edits or hands off serialized ABAP/RAP/CDS sources in a workspace, run `LintABAPProject` (`lint_abap_project`) before sending changes to SAP; if unavailable, report the skipped preflight and use caller-supplied `LintABAP` if available. Confirm the configured root is the intended project (`SAP_AI_DEV_TOOLKIT_WORKSPACE_ROOT`, or the MCP process working directory by default; `directory` is relative to that root), `targetRelease` matches the SAP target, `dependencies.missingFolders` and `syntaxRules.disabled` are empty, and `status` is `clean`. `issues`, `incomplete`, `no-abap-files`, or a mismatched root/release are not passes. This is static analysis only and does not validate SAP activation or runtime; caller-supplied `LintABAP` on the latest source for the exact object set remains mandatory immediately before activation. Skip when no serialized project source exists.
9
+
8
10
  You are a specialized SAP RAP service development agent for SAP Business Application Studio (BAS). Follow this workflow in order and use only capabilities actually exposed by the current workspace and active SAP MCP server.
9
11
 
10
12
  **Direct MCP invocation is mandatory.** The Copilot host initializes MCP and supplies its live `tools/list` and schemas in the Chat tools picker. Invoke the attached destination tools directly. Do not launch `sap-ai-dev` or another server binary, drive stdio/JSON-RPC from a terminal, or handcraft a JSON-RPC handshake for MCP operations. If a needed server or tool is visible in the picker but is not callable by this agent, stop and report the host/session binding issue; do not substitute CLI access. Tool names shown in PascalCase (such as `GetSource` or `LintABAP`) are logical names; the live MCP surface exposes them as lowercase snake_case. A server scoped to a single destination (the normal case for generated `mcp.json` entries) exposes them unprefixed, so `GetSource` appears as `get_source` and `LintABAP` as `lint_abap`; when several destinations share one server, each name is destination-prefixed with its slug for disambiguation (for example `cfd_run_query`). Always call the exact names returned by `tools/list`.
@@ -5,6 +5,8 @@ target: vscode
5
5
  user-invocable: true
6
6
  ---
7
7
 
8
+ **Workspace preflight.** Whenever this workflow edits or hands off serialized ABAP/RAP/CDS sources in a workspace, run `LintABAPProject` (`lint_abap_project`) before sending changes to SAP; if unavailable, report the skipped preflight and use caller-supplied `LintABAP` if available. Confirm the configured root is the intended project (`SAP_AI_DEV_TOOLKIT_WORKSPACE_ROOT`, or the MCP process working directory by default; `directory` is relative to that root), `targetRelease` matches the SAP target, `dependencies.missingFolders` and `syntaxRules.disabled` are empty, and `status` is `clean`. `issues`, `incomplete`, `no-abap-files`, or a mismatched root/release are not passes. This is static analysis only and does not validate SAP activation or runtime; caller-supplied `LintABAP` on the latest source for the exact object set remains mandatory immediately before activation. Skip when no serialized project source exists.
9
+
8
10
  You are an SAP solution architecture and SDLC orchestration agent for SAP Business Application Studio (BAS). Your job is to investigate the target SAP system, decide how a requested business requirement should be solved, recommend SAP standard capabilities and released APIs, design a Clean Core solution, and drive the delivery lifecycle through implementation-agent handoffs and validation gates. Prefer SAP standard capabilities, released APIs, Clean Core compliant extension points, and side-by-side extensibility over custom core changes.
9
11
 
10
12
  Follow this workflow in order and use only capabilities actually exposed by the current workspace and active SAP MCP server.
@@ -5,6 +5,8 @@ description: Diagnose ABAP runtime failures using application logs, source/refer
5
5
 
6
6
  # ABAP debugging
7
7
 
8
+ **Workspace preflight.** Whenever this workflow edits or hands off serialized ABAP sources in a workspace, run `LintABAPProject` (`lint_abap_project`) before sending changes to SAP; if unavailable, report the skipped preflight and use caller-supplied `LintABAP` if available. Confirm the configured root is the intended project (`SAP_AI_DEV_TOOLKIT_WORKSPACE_ROOT`, or the MCP process working directory by default; `directory` is relative to that root), `targetRelease` matches the SAP target, `dependencies.missingFolders` and `syntaxRules.disabled` are empty, and `status` is `clean`. `issues`, `incomplete`, `no-abap-files`, or a mismatched root/release are not passes. This is static analysis only and does not validate SAP activation or runtime; caller-supplied `LintABAP` on the latest source for the exact object set remains mandatory immediately before activation. Skip when no serialized project source exists.
9
+
8
10
  ## Tool shortlist
9
11
 
10
12
  Query the active MCP server's live `tools/list` once; use the exact live tool name and schema for the target. Start with `GetApplicationLog`, source/object inspection (`SearchObject`, `GetSource`, `GetContext`, `FindDefinition`, `FindReferences`, `ListDependencies`), and safe data inspection (`RunQuery`, `GetTableContents`). For an authorized debugger session, use `SetBreakpoint`, review registered breakpoints with `GetBreakpoints`, attach with `DebuggerAttach`, inspect with `DebuggerGetStack` / `DebuggerGetVariables`, step with `DebuggerStep`, and finish with `DebuggerDetach`. Avoid attaching to another user's session. Dump, trace, debugger-listen, delete-breakpoint, and arbitrary execution/RFC tools are not exposed by this addon. Tool names shown in PascalCase (such as `GetSource` or `LintABAP`) are logical names; the live MCP surface exposes them as lowercase snake_case. A server scoped to a single destination (the normal case for generated `mcp.json` entries) exposes them unprefixed, so `GetSource` appears as `get_source` and `LintABAP` as `lint_abap`; when several destinations share one server, each name is destination-prefixed with its slug for disambiguation (for example `cfd_run_query`). Always call the exact names returned by `tools/list`.
@@ -5,6 +5,8 @@ description: Implement or change ABAP programs, classes, interfaces, function gr
5
5
 
6
6
  # ABAP development
7
7
 
8
+ **Workspace preflight.** Whenever this workflow edits or hands off serialized ABAP sources in a workspace, run `LintABAPProject` (`lint_abap_project`) before sending changes to SAP; if unavailable, report the skipped preflight and use caller-supplied `LintABAP` if available. Confirm the configured root is the intended project (`SAP_AI_DEV_TOOLKIT_WORKSPACE_ROOT`, or the MCP process working directory by default; `directory` is relative to that root), `targetRelease` matches the SAP target, `dependencies.missingFolders` and `syntaxRules.disabled` are empty, and `status` is `clean`. `issues`, `incomplete`, `no-abap-files`, or a mismatched root/release are not passes. This is static analysis only and does not validate SAP activation or runtime; caller-supplied `LintABAP` on the latest source for the exact object set remains mandatory immediately before activation. Skip when no serialized project source exists.
9
+
8
10
  ## Tool shortlist
9
11
 
10
12
  Query the active MCP server's live `tools/list` once; use task-relevant tools with their returned schemas. Locate objects and usages with `SearchObject`, `GrepObjects`, and `GrepPackages`; read with `GetSource` and `GetContext`; trace callers or dependencies with `FindDefinition` and `FindReferences`; compare workspace and SAP versions with `CompareSource`. Use `EditSource` for a localized change and `WriteSource` for a larger rewrite. Format with `PrettyPrint`; lint caller-supplied source with `LintABAP`; validate with `SyntaxCheck` and `RunUnitTests`, plus `RunATCCheck` when exposed and relevant. For Clean Core requirements, use `GetAPIReleaseState` on each relevant dependency. Tool names shown in PascalCase (such as `GetSource` or `LintABAP`) are logical names; the live MCP surface exposes them as lowercase snake_case. A server scoped to a single destination (the normal case for generated `mcp.json` entries) exposes them unprefixed, so `GetSource` appears as `get_source` and `LintABAP` as `lint_abap`; when several destinations share one server, each name is destination-prefixed with its slug for disambiguation (for example `cfd_run_query`). Always call the exact names returned by `tools/list`.
@@ -5,6 +5,8 @@ description: Analyze ABAP runtime incidents, debugger state, and performance sym
5
5
 
6
6
  # ABAP runtime analysis
7
7
 
8
+ **Workspace preflight.** Whenever this workflow edits or hands off serialized ABAP sources in a workspace, run `LintABAPProject` (`lint_abap_project`) before sending changes to SAP; if unavailable, report the skipped preflight and use caller-supplied `LintABAP` if available. Confirm the configured root is the intended project (`SAP_AI_DEV_TOOLKIT_WORKSPACE_ROOT`, or the MCP process working directory by default; `directory` is relative to that root), `targetRelease` matches the SAP target, `dependencies.missingFolders` and `syntaxRules.disabled` are empty, and `status` is `clean`. `issues`, `incomplete`, `no-abap-files`, or a mismatched root/release are not passes. This is static analysis only and does not validate SAP activation or runtime; caller-supplied `LintABAP` on the latest source for the exact object set remains mandatory immediately before activation. Skip when no serialized project source exists.
9
+
8
10
  ## Tool shortlist
9
11
 
10
12
  Query the active MCP server's live `tools/list` once and use the exact live names and schemas. Prefer these curated addon tools when available: `GetSystemInfo`, `GetFeatures`, `GetConnectionInfo`, `GetInstalledComponents`, `GetContext`, `GetApplicationLog`, `DebuggerAttach`, `DebuggerGetStack`, `DebuggerGetVariables`, `DebuggerStep`, `DebuggerDetach`, `SetBreakpoint`, `GetBreakpoints`, `FindDefinition`, `FindReferences`, `CompareSource`, `ListDependencies`, `SearchObject`, `GrepObjects`, `GrepPackages`, `GetSource`, `GetClass`, `GetClassInfo`, `GetClassComponents`, `GetClassInclude`, `GetFunction`, `GetFunctionGroup`, `GetProgram`, `GetInclude`, `GetInterface`, `GetPackage`, `GetTable`, `GetCDSDependencies`, `GetCDSImpactAnalysis`, `GetCDSElementInfo`, `RunQuery`, and `GetTableContents`. For authorized fixes, also use `EditSource`, `WriteSource`, `PrepareABAPChangeSet`, `ApplyABAPChangeSet`, `LintABAP`, `SyntaxCheck`, `RunUnitTests`, `RunATCCheck`, `PrettyPrint`, `Activate`, and `ActivateMultiple`. Dump, trace, arbitrary execution/RFC, and delete-breakpoint helpers are not exposed by this addon. Tool names shown in PascalCase (such as `GetSource` or `LintABAP`) are logical names; the live MCP surface exposes them as lowercase snake_case. A server scoped to a single destination (the normal case for generated `mcp.json` entries) exposes them unprefixed, so `GetSource` appears as `get_source` and `LintABAP` as `lint_abap`; when several destinations share one server, each name is destination-prefixed with its slug for disambiguation (for example `cfd_run_query`). Always call the exact names returned by `tools/list`.
@@ -5,6 +5,8 @@ description: Design ABAP Unit behavior tests and validate ABAP changes with lint
5
5
 
6
6
  # ABAP testing and quality
7
7
 
8
+ **Workspace preflight.** Whenever this workflow edits or hands off serialized ABAP sources in a workspace, run `LintABAPProject` (`lint_abap_project`) before sending changes to SAP; if unavailable, report the skipped preflight and use caller-supplied `LintABAP` if available. Confirm the configured root is the intended project (`SAP_AI_DEV_TOOLKIT_WORKSPACE_ROOT`, or the MCP process working directory by default; `directory` is relative to that root), `targetRelease` matches the SAP target, `dependencies.missingFolders` and `syntaxRules.disabled` are empty, and `status` is `clean`. `issues`, `incomplete`, `no-abap-files`, or a mismatched root/release are not passes. This is static analysis only and does not validate SAP activation or runtime; caller-supplied `LintABAP` on the latest source for the exact object set remains mandatory immediately before activation. Skip when no serialized project source exists.
9
+
8
10
  ## Tool selection
9
11
 
10
12
  Query the active MCP server's live `tools/list` once, then use only task-relevant tools with their returned schemas: `LintABAP` for caller-supplied source, `SyntaxCheck` for SAP syntax validation, `RunUnitTests` for ABAP Unit, and `RunATCCheck` for ATC. Unavailable tools are not passes. Tool names shown in PascalCase (such as `GetSource` or `LintABAP`) are logical names; the live MCP surface exposes them as lowercase snake_case. A server scoped to a single destination (the normal case for generated `mcp.json` entries) exposes them unprefixed, so `GetSource` appears as `get_source` and `LintABAP` as `lint_abap`; when several destinations share one server, each name is destination-prefixed with its slug for disambiguation (for example `cfd_run_query`). Always call the exact names returned by `tools/list`.
@@ -5,6 +5,8 @@ description: Implement, change, or analyze ABAP CDS data definitions and their d
5
5
 
6
6
  # CDS development
7
7
 
8
+ **Workspace preflight.** Whenever this workflow edits or hands off serialized CDS/ABAP sources in a workspace, run `LintABAPProject` (`lint_abap_project`) before sending changes to SAP; if unavailable, report the skipped preflight and use caller-supplied `LintABAP` if available for ABAP sources. Confirm the configured root is the intended project (`SAP_AI_DEV_TOOLKIT_WORKSPACE_ROOT`, or the MCP process working directory by default; `directory` is relative to that root), `targetRelease` matches the SAP target, `dependencies.missingFolders` and `syntaxRules.disabled` are empty, and `status` is `clean`. `issues`, `incomplete`, `no-abap-files`, or a mismatched root/release are not passes. This is static analysis only and does not validate SAP activation or runtime; caller-supplied `LintABAP` on the latest source for the exact object set remains mandatory immediately before activation. Skip when no serialized project source exists.
9
+
8
10
  ## Tool shortlist
9
11
 
10
12
  Query the active MCP server's live `tools/list` once; use task-relevant tools with their returned schemas. Locate views and usages with `SearchObject` and `GrepObjects`; read DDLS with `GetSource`; compare workspace and SAP versions with `CompareSource`; use `GetCDSDependencies` for upstream dependencies, `GetCDSImpactAnalysis` for consumers, and `GetCDSElementInfo` for element metadata. Trace references with `FindReferences`; edit via `EditSource` / `WriteSource`. Validate with `SyntaxCheck`, `RunATCCheck`, activation via `Activate` / `ActivateMultiple`, and read-only runtime queries via `RunQuery` or `GetTableContents` when exposed. Tool names shown in PascalCase (such as `GetSource` or `LintABAP`) are logical names; the live MCP surface exposes them as lowercase snake_case. A server scoped to a single destination (the normal case for generated `mcp.json` entries) exposes them unprefixed, so `GetSource` appears as `get_source` and `LintABAP` as `lint_abap`; when several destinations share one server, each name is destination-prefixed with its slug for disambiguation (for example `cfd_run_query`). Always call the exact names returned by `tools/list`.
@@ -5,6 +5,8 @@ description: Design SAP extensions using Clean Core principles, released extensi
5
5
 
6
6
  # Clean Core extensibility
7
7
 
8
+ **Workspace preflight.** Whenever the selected extension edits or hands off serialized ABAP/RAP sources in a workspace, run `LintABAPProject` (`lint_abap_project`) before sending changes to SAP; if unavailable, report the skipped preflight and use caller-supplied `LintABAP` if available. Confirm the configured root is the intended project (`SAP_AI_DEV_TOOLKIT_WORKSPACE_ROOT`, or the MCP process working directory by default; `directory` is relative to that root), `targetRelease` matches the SAP target, `dependencies.missingFolders` and `syntaxRules.disabled` are empty, and `status` is `clean`. `issues`, `incomplete`, `no-abap-files`, or a mismatched root/release are not passes. This is static analysis only and does not validate SAP activation or runtime; caller-supplied `LintABAP` on the latest source for the exact object set remains mandatory immediately before activation. Skip when no serialized project source exists.
9
+
8
10
  ## Tool shortlist
9
11
 
10
12
  Query the active MCP server's live `tools/list` once; use task-relevant tools with their returned schemas. Inspect system capabilities with `GetSystemInfo`, `GetFeatures`, and `GetInstalledComponents`; inspect source and dependencies with `GetSource`, `GetContext`, `FindDefinition`, `FindReferences`, `CompareSource`, `ListDependencies`, `GetCDSDependencies`, and `GetCDSImpactAnalysis`. Check release status with `GetAPIReleaseState` or batch with `PlanABAPCloudMigration` where exposed. Use transport tools only for planning/readiness context and never claim transport release/deletion support; transport release is unavailable through this addon. Tool names shown in PascalCase (such as `GetSource` or `LintABAP`) are logical names; the live MCP surface exposes them as lowercase snake_case. A server scoped to a single destination (the normal case for generated `mcp.json` entries) exposes them unprefixed, so `GetSource` appears as `get_source` and `LintABAP` as `lint_abap`; when several destinations share one server, each name is destination-prefixed with its slug for disambiguation (for example `cfd_run_query`). Always call the exact names returned by `tools/list`.
@@ -5,6 +5,8 @@ description: Develop SAP RAP business objects, behavior, projections, and OData
5
5
 
6
6
  # RAP development
7
7
 
8
+ **Workspace preflight.** Whenever this workflow edits or hands off serialized RAP/CDS/ABAP sources in a workspace, run `LintABAPProject` (`lint_abap_project`) before sending changes to SAP; if unavailable, report the skipped preflight and use caller-supplied `LintABAP` if available. Confirm the configured root is the intended project (`SAP_AI_DEV_TOOLKIT_WORKSPACE_ROOT`, or the MCP process working directory by default; `directory` is relative to that root), `targetRelease` matches the SAP target, `dependencies.missingFolders` and `syntaxRules.disabled` are empty, and `status` is `clean`. `issues`, `incomplete`, `no-abap-files`, or a mismatched root/release are not passes. This is static analysis only and does not validate SAP activation or runtime; caller-supplied `LintABAP` on the latest source for the exact object set remains mandatory immediately before activation. Skip when no serialized project source exists.
9
+
8
10
  ## Tool shortlist
9
11
 
10
12
  Query the active MCP server's live `tools/list` once; use task-relevant tools with their returned schemas. Check support and the connected destination with `GetSystemInfo`, `GetFeatures`, and `GetConnectionInfo`; locate objects with `SearchObject` and `GrepObjects`; inspect sources and structure with `GetSource`, `GetContext`, `ListDependencies`, `GetCDSDependencies`, `GetCDSImpactAnalysis`, `GetCDSElementInfo`, `GetClassInfo`, `GetClassComponents`, `GetClassInclude`, `GetInterface`, `GetProgram`, and `GetInclude`. Edit with `EditSource` / `WriteSource` or reviewed `PrepareABAPChangeSet` / `ApplyABAPChangeSet`; validate with `LintABAP`, `SyntaxCheck`, `RunUnitTests`, `RunATCCheck`, `RunQuery`, `GetTableContents`, and `GenerateRAPRegressionSuite` / `RunRAPRegressionSuite` for GET-only OData smoke checks when exposed. Activate with `Activate` / `ActivateMultiple` only when requested. Create service bindings with `CreateObject` (`SRVB/SVB`; set `binding_version` explicitly because V2 is the default) and publish or withdraw them with `PublishServiceBinding` / `UnpublishServiceBinding` only when publication is requested. Tool names shown in PascalCase (such as `GetSource` or `LintABAP`) are logical names; the live MCP surface exposes them as lowercase snake_case. A server scoped to a single destination (the normal case for generated `mcp.json` entries) exposes them unprefixed, so `GetSource` appears as `get_source` and `LintABAP` as `lint_abap`; when several destinations share one server, each name is destination-prefixed with its slug for disambiguation (for example `cfd_run_query`). Always call the exact names returned by `tools/list`.
@@ -5,6 +5,8 @@ description: Validate and deliver SAP RAP OData services, behavior implementatio
5
5
 
6
6
  # RAP service delivery
7
7
 
8
+ **Workspace preflight.** Whenever this workflow edits or hands off serialized RAP/CDS/ABAP sources in a workspace, run `LintABAPProject` (`lint_abap_project`) before sending changes to SAP; if unavailable, report the skipped preflight and use caller-supplied `LintABAP` if available. Confirm the configured root is the intended project (`SAP_AI_DEV_TOOLKIT_WORKSPACE_ROOT`, or the MCP process working directory by default; `directory` is relative to that root), `targetRelease` matches the SAP target, `dependencies.missingFolders` and `syntaxRules.disabled` are empty, and `status` is `clean`. `issues`, `incomplete`, `no-abap-files`, or a mismatched root/release are not passes. This is static analysis only and does not validate SAP activation or runtime; caller-supplied `LintABAP` on the latest source for the exact object set remains mandatory immediately before activation. Skip when no serialized project source exists.
9
+
8
10
  ## Tool shortlist
9
11
 
10
12
  Query the active MCP server's live `tools/list` once and use the exact live names and schemas. Prefer these curated addon tools when available: `GetSystemInfo`, `GetFeatures`, `GetConnectionInfo`, `SearchObject`, `GrepObjects`, `GrepPackages`, `GetSource`, `GetContext`, `ListDependencies`, `GetCDSDependencies`, `GetCDSImpactAnalysis`, `GetCDSElementInfo`, `GetClass`, `GetClassInfo`, `GetClassComponents`, `GetClassInclude`, `GetInterface`, `EditSource`, `WriteSource`, `PrepareABAPChangeSet`, `ApplyABAPChangeSet`, `LintABAP`, `SyntaxCheck`, `RunUnitTests`, `RunATCCheck`, `PrettyPrint`, `RunQuery`, `GetTableContents`, `GenerateRAPRegressionSuite`, `RunRAPRegressionSuite`, `GetApplicationLog`, `GetInactiveObjects`, `Activate`, `ActivateMultiple`, `CreateObject` (service bindings), `PublishServiceBinding`, and `UnpublishServiceBinding`. Read UI5 apps already deployed to the ABAP repository with `UI5ListApps`, `UI5GetApp`, and `UI5GetFileContent`. UI5 deployment, dump, trace, arbitrary execution/RFC, and code-coverage tools are not exposed by this addon. Tool names shown in PascalCase (such as `GetSource` or `LintABAP`) are logical names; the live MCP surface exposes them as lowercase snake_case. A server scoped to a single destination (the normal case for generated `mcp.json` entries) exposes them unprefixed, so `GetSource` appears as `get_source` and `LintABAP` as `lint_abap`; when several destinations share one server, each name is destination-prefixed with its slug for disambiguation (for example `cfd_run_query`). Always call the exact names returned by `tools/list`.
@@ -5,6 +5,8 @@ description: Orchestrate the SAP delivery lifecycle from requirement analysis th
5
5
 
6
6
  # SAP SDLC orchestration
7
7
 
8
+ **Workspace preflight.** Whenever an implementation package edits or hands off serialized ABAP/RAP/CDS sources in a workspace, require `LintABAPProject` (`lint_abap_project`) before sending changes to SAP; if unavailable, report the skipped preflight and use caller-supplied `LintABAP` if available. Confirm the configured root is the intended project (`SAP_AI_DEV_TOOLKIT_WORKSPACE_ROOT`, or the MCP process working directory by default; `directory` is relative to that root), `targetRelease` matches the SAP target, `dependencies.missingFolders` and `syntaxRules.disabled` are empty, and `status` is `clean`. `issues`, `incomplete`, `no-abap-files`, or a mismatched root/release are not passes. This is static analysis only and does not validate SAP activation or runtime; caller-supplied `LintABAP` on the latest source for the exact object set remains mandatory immediately before activation. Skip when no serialized project source exists.
9
+
8
10
  ## Tool shortlist
9
11
 
10
12
  Query the active MCP server's live `tools/list` once; use task-relevant tools with their returned schemas. Investigate system and repository context with `GetSystemInfo`, `GetFeatures`, `GetConnectionInfo`, `GetInstalledComponents`, `SearchObject`, `GrepObjects`, `GrepPackages`, `GetSource`, `GetContext`, `FindDefinition`, `FindReferences`, `CompareSource`, and dependency tools (`ListDependencies`, `GetCDSDependencies`, `GetCDSImpactAnalysis`). Validate with `LintABAP`, `SyntaxCheck`, `RunUnitTests`, `RunATCCheck`, runtime queries (`RunQuery`, `GetTableContents`), `GetApplicationLog`, and `GenerateRAPRegressionSuite` / `RunRAPRegressionSuite` when exposed. Use transport tools for readiness planning only; never claim transport release/deletion support because transport release is unavailable through this addon. Tool names shown in PascalCase (such as `GetSource` or `LintABAP`) are logical names; the live MCP surface exposes them as lowercase snake_case. A server scoped to a single destination (the normal case for generated `mcp.json` entries) exposes them unprefixed, so `GetSource` appears as `get_source` and `LintABAP` as `lint_abap`; when several destinations share one server, each name is destination-prefixed with its slug for disambiguation (for example `cfd_run_query`). Always call the exact names returned by `tools/list`.
@@ -5,6 +5,8 @@ description: Assess requirements against SAP standard capabilities, released API
5
5
 
6
6
  # SAP standard and API analysis
7
7
 
8
+ **Workspace preflight.** Whenever the recommendation includes editing or handing off serialized ABAP/RAP sources in a workspace, require `LintABAPProject` (`lint_abap_project`) before sending changes to SAP; if unavailable, report the skipped preflight and use caller-supplied `LintABAP` if available. Confirm the configured root is the intended project (`SAP_AI_DEV_TOOLKIT_WORKSPACE_ROOT`, or the MCP process working directory by default; `directory` is relative to that root), `targetRelease` matches the SAP target, `dependencies.missingFolders` and `syntaxRules.disabled` are empty, and `status` is `clean`. `issues`, `incomplete`, `no-abap-files`, or a mismatched root/release are not passes. This is static analysis only and does not validate SAP activation or runtime; caller-supplied `LintABAP` on the latest source for the exact object set remains mandatory immediately before activation. Any actual activation still requires a successful `LintABAP` run immediately before activation on the latest source for the exact activation request. Never activate merely to run tests. Skip for analysis-only work or when no serialized project source exists.
9
+
8
10
  ## Tool shortlist
9
11
 
10
12
  Query the active MCP server's live `tools/list` once; use task-relevant tools with their returned schemas. Inspect target capabilities with `GetSystemInfo`, `GetFeatures`, `GetConnectionInfo`, and `GetInstalledComponents`. Search candidates with `SearchObject`, `GrepObjects`, `GrepPackages`, `GetSource`, `GetContext`, `FindDefinition`, `FindReferences`, `ListDependencies`, `GetCDSDependencies`, `GetCDSImpactAnalysis`, and `GetCDSElementInfo`. Verify released API status with `GetAPIReleaseState` or batch with `PlanABAPCloudMigration` when exposed. Use `RunQuery` or `GetTableContents` only for safe read-only evidence. Never claim transport release/deletion support; transport release is unavailable through this addon. Tool names shown in PascalCase (such as `GetSource` or `LintABAP`) are logical names; the live MCP surface exposes them as lowercase snake_case. A server scoped to a single destination (the normal case for generated `mcp.json` entries) exposes them unprefixed, so `GetSource` appears as `get_source` and `LintABAP` as `lint_abap`; when several destinations share one server, each name is destination-prefixed with its slug for disambiguation (for example `cfd_run_query`). Always call the exact names returned by `tools/list`.
@@ -5,6 +5,8 @@ description: Prepare ABAP changes for SAP transport and verify request contents,
5
5
 
6
6
  # SAP transport preparation
7
7
 
8
+ **Workspace preflight.** When a transport request includes serialized ABAP sources in a workspace, run `LintABAPProject` (`lint_abap_project`) before sending changes to SAP; if unavailable, report the skipped preflight and use caller-supplied `LintABAP` if available. Confirm the configured root is the intended project (`SAP_AI_DEV_TOOLKIT_WORKSPACE_ROOT`, or the MCP process working directory by default; `directory` is relative to that root), `targetRelease` matches the SAP target, `dependencies.missingFolders` and `syntaxRules.disabled` are empty, and `status` is `clean`. `issues`, `incomplete`, `no-abap-files`, or a mismatched root/release are not passes. This is static analysis only and does not validate SAP activation or runtime; caller-supplied `LintABAP` on the latest source for the exact object set remains mandatory immediately before activation. Skip when no local serialized sources exist.
9
+
8
10
  ## Tool shortlist
9
11
 
10
12
  Query the active MCP server's live `tools/list` once; use task-relevant tools with their returned schemas. Identify candidates with `GetUserTransports` or `ListTransports`; inspect a request with `GetTransport` / `GetTransportInfo`; use `CheckTransportReadiness` to collect transport, dependency, inactive-object, ABAP Unit, and ATC evidence when available. Activate with `Activate` / `ActivateMultiple` only when requested. `CreateTransport` is state-changing and requires explicit authorization. Release and deletion are not available through this addon. Tool names shown in PascalCase (such as `GetSource` or `LintABAP`) are logical names; the live MCP surface exposes them as lowercase snake_case. A server scoped to a single destination (the normal case for generated `mcp.json` entries) exposes them unprefixed, so `GetSource` appears as `get_source` and `LintABAP` as `lint_abap`; when several destinations share one server, each name is destination-prefixed with its slug for disambiguation (for example `cfd_run_query`). Always call the exact names returned by `tools/list`.
package/README.md CHANGED
@@ -15,7 +15,7 @@
15
15
 
16
16
  <p align="center">
17
17
  <img src="https://img.shields.io/badge/SAP-Business%20Application%20Studio-0A6ED1?style=flat-square&logo=sap&logoColor=white" alt="SAP Business Application Studio">
18
- <img src="https://img.shields.io/badge/AI%20agents-Copilot%20%7C%20Claude%20%7C%20Codex%20%7C%20Cursor%20%7C%20Gemini%20%7C%20opencode%20%7C%20Pi-000000?style=flat-square" alt="Works with GitHub Copilot, Claude Code, OpenAI Codex, Cursor, Gemini CLI, opencode, and Pi Coding Agent">
18
+ <img src="https://img.shields.io/badge/AI%20agents-Copilot%20%7C%20Claude%20%7C%20Codex%20%7C%20Cursor%20%7C%20Gemini%20%7C%20opencode%20%7C%20Pi%20%7C%20OMP%20%7C%20Cline%20%7C%20Roo%20%7C%20Windsurf-000000?style=flat-square" alt="Works with GitHub Copilot, Claude Code, OpenAI Codex, Cursor, Gemini CLI, opencode, Pi Coding Agent, Oh My Pi, Cline, Roo Code, and Windsurf">
19
19
  <img src="https://img.shields.io/badge/MCP-enabled-7B61FF?style=flat-square" alt="MCP enabled">
20
20
  <img src="https://img.shields.io/badge/SAP%20ADT-connected-0A6ED1?style=flat-square&logo=sap&logoColor=white" alt="SAP ADT connected">
21
21
  <img src="https://img.shields.io/badge/ABAP-CDS%20%7C%20RAP-EA4AAA?style=flat-square" alt="ABAP CDS RAP">
@@ -89,7 +89,7 @@ Use the attached destination-prefixed tools directly in chat. Do not launch `sap
89
89
 
90
90
  **Included skills:** `abap-development` · `abap-testing-quality` · `cds-development` · `rap-development` · `abap-debugging` · `abap-runtime-analysis` · `rap-service-delivery` · `sap-standard-api-analysis` · `clean-core-extensibility` · `sap-sdlc-orchestration` · `sap-transport-release` · `hana-cloud-inspection` · `hana-cloud-native-development` · `hana-cloud-validation`
91
91
 
92
- Agents and skills install for the harnesses you pick during global installation: a multi-select offers GitHub Copilot and Claude Code pre-checked, plus OpenAI Codex, Cursor, Gemini CLI, opencode, and Pi Coding Agent. In non-interactive installs they are installed for GitHub Copilot only; run `sap-ai-dev --setup` afterwards to configure destinations or local SAP GUI systems. Repository-scoped installation instructions appear below.
92
+ Agents and skills install for the harnesses you pick during global installation: a multi-select offers GitHub Copilot and Claude Code pre-checked, plus OpenAI Codex, Cursor, Gemini CLI, opencode, Pi Coding Agent, Oh My Pi (OMP), Cline, Roo Code, and Windsurf / Devin Desktop. In non-interactive installs they are installed for GitHub Copilot only; run `sap-ai-dev --setup` afterwards to configure destinations or local SAP GUI systems. Repository-scoped installation instructions appear below.
93
93
 
94
94
  ## ✨ Your SAP development cockpit, inside your AI assistant
95
95
 
@@ -334,7 +334,7 @@ sap-ai-dev --setup
334
334
  5. Choose Windows / smart card SSO or username/password when the prompt is available.
335
335
  6. Start each generated MCP server from VS Code's **MCP: List Servers** command.
336
336
 
337
- The same postinstall offers the bundled agents and skills for several AI coding harnesses in a checkbox picker. **GitHub Copilot** and **Claude Code** start pre-checked, so pressing **Enter** alone installs both; **Space** selects or deselects a harness, **a** toggles all seven, and confirming with none selected skips the step without changing files. When MCP setup succeeds, the generated managed MCP servers are also mirrored into selected harnesses that have a stable user-level JSON MCP configuration (Claude Code, Cursor, Gemini CLI, and Pi Coding Agent). GitHub Copilot continues to use the VS Code/BAS MCP configuration written during setup. With no interactive terminal, the assets are installed for GitHub Copilot only; set `SAP_AI_DEV_TOOLKIT_HARNESSES` to a comma-separated list of harness ids (`github-copilot`, `claude-code`, `codex`, `cursor`, `gemini-cli`, `opencode`, `pi-coding-agent`) to choose non-interactively.
337
+ The same postinstall offers the bundled agents and skills for several AI coding harnesses in a checkbox picker. **GitHub Copilot** and **Claude Code** start pre-checked, so pressing **Enter** alone installs both; **Space** selects or deselects a harness, **a** toggles all eleven, and confirming with none selected skips the step without changing files. When MCP setup succeeds, generated managed MCP servers are also mirrored into selected harnesses with documented user-level JSON configuration (Claude Code, Cursor, Gemini CLI, Pi Coding Agent, OMP, and Windsurf / Devin Desktop). GitHub Copilot continues to use the VS Code/BAS MCP configuration written during setup. With no interactive terminal, the assets are installed for GitHub Copilot only; set `SAP_AI_DEV_TOOLKIT_HARNESSES` to a comma-separated list of harness ids (`github-copilot`, `claude-code`, `codex`, `cursor`, `gemini-cli`, `opencode`, `pi-coding-agent`, `omp`, `cline`, `roo-code`, `windsurf`) to choose non-interactively.
338
338
 
339
339
  Some current npm versions also require install hooks to be approved. If npm reports that `sap-ai-dev-toolkit`'s `postinstall` was blocked, allow it during a fresh install with `npm install --global --allow-scripts=sap-ai-dev-toolkit sap-ai-dev-toolkit`, or run `sap-ai-dev --setup` from an interactive terminal after installation.
340
340
 
@@ -418,6 +418,10 @@ During npm postinstall, or after interactive `sap-ai-dev --setup` in BAS or on a
418
418
  | Gemini CLI | `$HOME/.gemini/skills/` | — | `$HOME/.gemini/settings.json` |
419
419
  | opencode | `$XDG_CONFIG_HOME/opencode/skills/` (default `$HOME/.config/opencode/skills/`) | `$XDG_CONFIG_HOME/opencode/agents/<id>.md` (as subagents) | Not changed automatically |
420
420
  | Pi Coding Agent | `$PI_CODING_AGENT_DIR/skills/` (default `$HOME/.pi/agent/skills/`) | `$PI_CODING_AGENT_DIR/prompts/<id>.md` (as `/` prompt commands) | `$PI_CODING_AGENT_DIR/mcp.json` (default `$HOME/.pi/agent/mcp.json`) |
421
+ | Oh My Pi (OMP) | `$HOME/.omp/agent/skills/` (overridden by `PI_CONFIG_DIR` or `PI_CODING_AGENT_DIR`) | — | `$HOME/.omp/agent/mcp.json` (same overrides) |
422
+ | Cline | `$HOME/.cline/skills/` | — | Not changed automatically |
423
+ | Roo Code | `$HOME/.roo/skills/` | — | Not changed automatically |
424
+ | Windsurf / Devin Desktop | `$XDG_CONFIG_HOME/devin/skills/` (default `$HOME/.config/devin/skills/`); Windows: `$HOME/.codeium/windsurf/skills/` | — | `$XDG_CONFIG_HOME/devin/mcp_config.json` (default `$HOME/.config/devin/mcp_config.json`); Windows: `%APPDATA%\devin\mcp_config.json` |
421
425
 
422
426
  All 14 skills use the portable `SKILL.md` format and are installed verbatim. The 5 agents use Copilot's `.agent.md` format, so they are installed as native custom agents for harnesses with a custom-agent mechanism (GitHub Copilot, Claude Code, opencode), with frontmatter adapted where needed. Pi receives equivalent prompt templates (`/abap-developer`, `/sap-solution-architect`, and so on) because Pi exposes reusable personas through prompts rather than a separate custom-agent file type. Selecting several harnesses duplicates the small markdown payloads on disk.
423
427
 
@@ -1,5 +1,5 @@
1
- 87b75dce6f9517696d1326526ae06bf1c216695283db7b2ee34f47335a861892 vsp-linux-x64
2
- c118ccd7043fc2eef9ad34d62ad78fb64c3ef3b4114d65b337b8d855b7b40f0d vsp-linux-arm64
3
- c66c6b75b83d3d581f859fa0a21c5a6a6ca0c168019353e3cd34ac8ed4632e7e vsp-darwin-x64
4
- 6b7833dfd6de0b2d1f6dab921e6ba7dd795217c733552413b4382925590f4388 vsp-darwin-arm64
5
- 8d1f0bf29a83d3b301c5bb9a34811c8c2a84d63285b93a7a393d0c942bf1d2f6 vsp-win32-x64.exe
1
+ f10b759b5e320c6fec4c176ef9bce49f783d796eeb8a2476dd5898c94cb14afd vsp-linux-x64
2
+ a4c47e4c15538cc16b540d19f55771e4c1340389efeca815e1881eafd89e45c5 vsp-linux-arm64
3
+ 20a197921c88f539ea1a7f5236509dbaa9b9f906a6333e6fb1dce9f8b5eec85c vsp-darwin-x64
4
+ 24a8810a17d31a434b92957ea41e6347b24e2a6a35e47b3b2fc13a16082a44c3 vsp-darwin-arm64
5
+ b318fdf5194a7f15ebcf7ee912fb2493df21aba337540a2402f81786300fd0ea vsp-win32-x64.exe
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sap-ai-dev-toolkit",
3
- "version": "0.9.9906",
3
+ "version": "0.9.9909",
4
4
  "description": "SAP AI development toolkit for BAS, ABAP, RAP, CAP, HANA Cloud, Fiori, UI5, and MCP",
5
5
  "author": "Gurkan Yilmaz",
6
6
  "type": "module",
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "dependencies": {
11
11
  "@abaplint/core": "2.119.0",
12
- "@modelcontextprotocol/sdk": "1.30.1",
12
+ "@modelcontextprotocol/sdk": "1.32.1",
13
13
  "@sap/hana-client": "2.30.27",
14
14
  "terminal-kit": "^3.1.4",
15
15
  "undici": "^6.21.3",
@@ -620,10 +620,30 @@ index 0000000..5e880bd
620
620
  + }
621
621
  +}
622
622
  diff --git a/pkg/adt/http.go b/pkg/adt/http.go
623
- index 0b4d9f1..3473536 100755
623
+ index 0b4d9f1..f0b5b51 100755
624
624
  --- a/pkg/adt/http.go
625
625
  +++ b/pkg/adt/http.go
626
- @@ -499,6 +499,7 @@ func (t *Transport) fetchCSRFTokenFor(ctx context.Context, stateful bool) error
626
+ @@ -313,7 +313,7 @@ func (t *Transport) request(ctx context.Context, path string, opts *RequestOptio
627
+ // The same expiry reaches a plain read as a successful-looking response that
628
+ // was in fact served by the identity provider. Nothing downstream would
629
+ // recognise the logon page it carries, so catch it here by origin.
630
+ - if resp.StatusCode < 400 && t.canReauth() && t.redirectedAwayFromSAP(resp) {
631
+ + if t.canReauth() && ((resp.StatusCode < 400 && t.redirectedAwayFromSAP(resp)) || isSSOLoginResponse(resp, body)) {
632
+ t.setCSRFToken("")
633
+ t.setSessionID("")
634
+ if err := t.callReauthFunc(ctx); err != nil {
635
+ @@ -448,6 +448,10 @@ func (t *Transport) retryRequest(ctx context.Context, path string, opts *Request
636
+ }
637
+ traceHTTPResponse(resp, body)
638
+
639
+ + if t.canReauth() && (t.redirectedAwayFromSAP(resp) || isSSOLoginResponse(resp, body)) {
640
+ + return nil, fmt.Errorf("SSO session is still unauthenticated after re-authentication on %s", path)
641
+ + }
642
+ +
643
+ if resp.StatusCode >= 400 {
644
+ return nil, &APIError{
645
+ StatusCode: resp.StatusCode,
646
+ @@ -499,6 +503,7 @@ func (t *Transport) fetchCSRFTokenFor(ctx context.Context, stateful bool) error
627
647
  // allowReauth exists to break a cycle: re-authenticating ends with a token
628
648
  // fetch of its own, and that fetch must not start another re-authentication.
629
649
  func (t *Transport) fetchCSRFTokenWithReauth(ctx context.Context, allowReauth bool, stateful bool) error {
@@ -631,7 +651,7 @@ index 0b4d9f1..3473536 100755
631
651
  token, status, redirected, err := t.probeCSRFToken(ctx, http.MethodHead, stateful)
632
652
  if err != nil {
633
653
  return err
634
- @@ -513,6 +514,29 @@ func (t *Transport) fetchCSRFTokenWithReauth(ctx context.Context, allowReauth bo
654
+ @@ -513,6 +518,29 @@ func (t *Transport) fetchCSRFTokenWithReauth(ctx context.Context, allowReauth bo
635
655
  if err != nil {
636
656
  return err
637
657
  }
@@ -661,7 +681,16 @@ index 0b4d9f1..3473536 100755
661
681
  if !isCSRFToken(token) {
662
682
  // An expired SSO session rarely announces itself as a 401. ICF sends
663
683
  // the request on to the identity provider, the redirect chain is
664
- @@ -537,6 +561,9 @@ func (t *Transport) fetchCSRFTokenWithReauth(ctx context.Context, allowReauth bo
684
+ @@ -520,7 +548,7 @@ func (t *Transport) fetchCSRFTokenWithReauth(ctx context.Context, allowReauth bo
685
+ // 200 — with no CSRF token in it, because it is not ADT answering.
686
+ // A live ADT session always yields a token, so its absence here is
687
+ // the signal, and a hop to a foreign host is the confirmation.
688
+ - if allowReauth && t.canReauth() && getStatus != http.StatusForbidden {
689
+ + if allowReauth && t.canReauth() && (getStatus != http.StatusForbidden || redirected || getRedirected) {
690
+ reason := fmt.Sprintf("no CSRF token (HEAD %d, GET %d)", status, getStatus)
691
+ if redirected || getRedirected {
692
+ reason = "the identity provider answered instead of SAP"
693
+ @@ -537,6 +565,9 @@ func (t *Transport) fetchCSRFTokenWithReauth(ctx context.Context, allowReauth bo
665
694
  }
666
695
  return t.fetchCSRFTokenWithReauth(ctx, false, stateful)
667
696
  }
@@ -671,7 +700,7 @@ index 0b4d9f1..3473536 100755
671
700
  switch getStatus {
672
701
  case http.StatusUnauthorized:
673
702
  return fmt.Errorf("authentication failed (401): check username/password")
674
- @@ -556,7 +583,11 @@ func (t *Transport) fetchCSRFTokenWithReauth(ctx context.Context, allowReauth bo
703
+ @@ -556,7 +587,11 @@ func (t *Transport) fetchCSRFTokenWithReauth(ctx context.Context, allowReauth bo
675
704
  // returns the token (empty when the server did not supply one), the status, and
676
705
  // whether the answer came from somewhere other than the SAP host.
677
706
  func (t *Transport) probeCSRFToken(ctx context.Context, method string, stateful bool) (token string, status int, redirected bool, err error) {
@@ -684,6 +713,42 @@ index 0b4d9f1..3473536 100755
684
713
  if err != nil {
685
714
  return "", 0, false, fmt.Errorf("building URL: %w", err)
686
715
  }
716
+ @@ -596,12 +631,34 @@ func (t *Transport) probeCSRFToken(ctx context.Context, method string, stateful
717
+ return "", 0, false, fmt.Errorf("executing request: %w", err)
718
+ }
719
+ defer resp.Body.Close()
720
+ + body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
721
+ + if err != nil {
722
+ + return "", 0, false, fmt.Errorf("reading CSRF response: %w", err)
723
+ + }
724
+ // Drain the body so the connection can be reused.
725
+ _, _ = io.Copy(io.Discard, resp.Body)
726
+ traceHTTPResponse(resp, nil)
727
+ t.adoptServerCookies(resp)
728
+
729
+ - return resp.Header.Get("X-CSRF-Token"), resp.StatusCode, t.redirectedAwayFromSAP(resp), nil
730
+ + return resp.Header.Get("X-CSRF-Token"), resp.StatusCode, t.redirectedAwayFromSAP(resp) || isSSOLoginResponse(resp, body), nil
731
+ +}
732
+ +
733
+ +func isSSOLoginResponse(resp *http.Response, body []byte) bool {
734
+ + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusUnauthorized && resp.StatusCode != http.StatusForbidden {
735
+ + return false
736
+ + }
737
+ + page := strings.ToLower(strings.TrimSpace(string(body)))
738
+ + html := strings.Contains(strings.ToLower(resp.Header.Get("Content-Type")), "text/html") ||
739
+ + strings.HasPrefix(page, "<!doctype html") || strings.HasPrefix(page, "<html")
740
+ + if !html || !strings.Contains(page, "<form") {
741
+ + return false
742
+ + }
743
+ + for _, marker := range []string{"samlrequest", "samlresponse", "sap-system-login", "sap-password", `type="password"`, `type='password'`} {
744
+ + if strings.Contains(page, marker) {
745
+ + return true
746
+ + }
747
+ + }
748
+ + return false
749
+ }
750
+
751
+ // redirectedAwayFromSAP reports whether a response was ultimately served by
687
752
  diff --git a/pkg/adt/http_csrf_discovery_test.go b/pkg/adt/http_csrf_discovery_test.go
688
753
  new file mode 100644
689
754
  index 0000000..80741da
@@ -746,6 +811,98 @@ index 0000000..80741da
746
811
  + }
747
812
  +}
748
813
  +
814
+ diff --git a/pkg/adt/http_sso_same_host_test.go b/pkg/adt/http_sso_same_host_test.go
815
+ new file mode 100644
816
+ index 0000000..0c39b96
817
+ --- /dev/null
818
+ +++ b/pkg/adt/http_sso_same_host_test.go
819
+ @@ -0,0 +1,86 @@
820
+ +package adt
821
+ +
822
+ +import (
823
+ + "context"
824
+ + "io"
825
+ + "net/http"
826
+ + "net/http/httptest"
827
+ + "strings"
828
+ + "testing"
829
+ +)
830
+ +
831
+ +func TestSameHostLoginPageRefreshesSSO(t *testing.T) {
832
+ + for _, method := range []string{http.MethodGet, http.MethodPost} {
833
+ + for _, status := range []int{http.StatusOK, http.StatusUnauthorized, http.StatusForbidden} {
834
+ + t.Run(method+"/"+http.StatusText(status), func(t *testing.T) {
835
+ + refreshes := 0
836
+ + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
837
+ + cookie, err := r.Cookie("SAP_SESSIONID")
838
+ + if err != nil || cookie.Value != "fresh" {
839
+ + w.Header().Set("Content-Type", "text/html; charset=utf-8")
840
+ + w.WriteHeader(status)
841
+ + _, _ = io.WriteString(w, `<html><body><form action="/sap/saml2/sp/acs"><input name="SAMLRequest" value="fixture"></form></body></html>`)
842
+ + return
843
+ + }
844
+ + w.Header().Set("X-CSRF-Token", "fresh-token")
845
+ + w.Header().Set("Content-Type", "application/xml")
846
+ + _, _ = io.WriteString(w, "<source>authenticated</source>")
847
+ + }))
848
+ + defer server.Close()
849
+ + transport := NewTransportWithClient(&Config{
850
+ + BaseURL: server.URL,
851
+ + Cookies: map[string]string{"SAP_SESSIONID": "expired"},
852
+ + ReauthFunc: func(context.Context) (map[string]string, error) {
853
+ + refreshes++
854
+ + return map[string]string{"SAP_SESSIONID": "fresh"}, nil
855
+ + },
856
+ + }, server.Client())
857
+ + response, err := transport.Request(context.Background(), "/sap/bc/adt/source", &RequestOptions{Method: method})
858
+ + if err != nil {
859
+ + t.Fatalf("request failed: %v", err)
860
+ + }
861
+ + if refreshes != 1 || string(response.Body) != "<source>authenticated</source>" {
862
+ + t.Fatalf("refreshes=%d body=%q, want one SSO refresh and authenticated source", refreshes, response.Body)
863
+ + }
864
+ + })
865
+ + }
866
+ + }
867
+ +}
868
+ +
869
+ +func TestOrdinaryForbiddenDoesNotRefreshSSO(t *testing.T) {
870
+ + refreshes := 0
871
+ + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
872
+ + w.WriteHeader(http.StatusForbidden)
873
+ + _, _ = io.WriteString(w, "Missing development authorization")
874
+ + }))
875
+ + defer server.Close()
876
+ + transport := NewTransportWithClient(&Config{BaseURL: server.URL, ReauthFunc: func(context.Context) (map[string]string, error) {
877
+ + refreshes++
878
+ + return nil, nil
879
+ + }}, server.Client())
880
+ + _, err := transport.Request(context.Background(), "/sap/bc/adt/source", &RequestOptions{Method: http.MethodGet})
881
+ + if err == nil || refreshes != 0 {
882
+ + t.Fatalf("err=%v refreshes=%d, want authorization error without SSO refresh", err, refreshes)
883
+ + }
884
+ +}
885
+ +
886
+ +func TestSSOLoginPageAfterRefreshFailsWithoutLoop(t *testing.T) {
887
+ + refreshes := 0
888
+ + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
889
+ + if r.URL.Path == "/sap/bc/adt/source" {
890
+ + w.Header().Set("Content-Type", "text/html")
891
+ + _, _ = io.WriteString(w, `<html><form><input type="password"></form></html>`)
892
+ + return
893
+ + }
894
+ + w.Header().Set("X-CSRF-Token", "fresh-token")
895
+ + }))
896
+ + defer server.Close()
897
+ + transport := NewTransportWithClient(&Config{BaseURL: server.URL, ReauthFunc: func(context.Context) (map[string]string, error) {
898
+ + refreshes++
899
+ + return map[string]string{"SAP_SESSIONID": "fresh"}, nil
900
+ + }}, server.Client())
901
+ + _, err := transport.Request(context.Background(), "/sap/bc/adt/source", &RequestOptions{Method: http.MethodGet})
902
+ + if err == nil || !strings.Contains(err.Error(), "still unauthenticated") || refreshes != 1 {
903
+ + t.Fatalf("err=%v refreshes=%d, want explicit failure after one refresh", err, refreshes)
904
+ + }
905
+ +}
749
906
  diff --git a/pkg/adt/servicebinding_publish_test.go b/pkg/adt/servicebinding_publish_test.go
750
907
  new file mode 100644
751
908
  index 0000000..93c19f9
package/src/abaplint.mjs CHANGED
@@ -1,5 +1,13 @@
1
+ import { lstat, readdir, readFile, realpath } from 'node:fs/promises';
2
+ import { isAbsolute, relative, resolve, sep } from 'node:path';
1
3
  import { Config, MemoryFile, Registry } from '@abaplint/core';
2
4
 
5
+ const MAX_PROJECT_FILES = 20_000;
6
+ const MAX_PROJECT_FILE_BYTES = 2 * 1024 * 1024;
7
+ const MAX_PROJECT_SOURCE_BYTES = 32 * 1024 * 1024;
8
+ const IGNORED_DIRECTORIES = new Set(['.git', '.hg', '.svn', '.build', '.next', '.cache', 'coverage', 'dist', 'node_modules']);
9
+ const PROJECT_CONFIG_FILES = ['.abaplint.jsonc', 'abaplint.jsonc', '.abaplint.json', 'abaplint.json'];
10
+
3
11
  export const ABAP_LINT_TOOL = {
4
12
  name: 'LintABAP',
5
13
  description: 'Run abaplint on caller-supplied source files in memory; does not read workspace files or call SAP.',
@@ -26,6 +34,21 @@ export const ABAP_LINT_TOOL = {
26
34
  }
27
35
  };
28
36
 
37
+ export const ABAP_LINT_PROJECT_TOOL = {
38
+ name: 'LintABAPProject',
39
+ description: 'Run read-only abaplint checks over serialized ABAP objects under the configured workspace root. Loads the project config and local dependency folders when present; does not call SAP, write files, or activate ABAP.',
40
+ inputSchema: {
41
+ type: 'object',
42
+ properties: {
43
+ directory: { type: 'string', description: 'Optional project subdirectory relative to the configured workspace root.' },
44
+ config: { type: 'object', description: 'Optional abaplint config object that overrides project config discovery.' }
45
+ },
46
+ required: [],
47
+ additionalProperties: false
48
+ },
49
+ annotations: { readOnlyHint: true }
50
+ };
51
+
29
52
  function isObject(value) {
30
53
  return value !== null && typeof value === 'object' && !Array.isArray(value);
31
54
  }
@@ -65,6 +88,170 @@ function errorResult(error) {
65
88
  };
66
89
  }
67
90
 
91
+ function reportResult(report) {
92
+ return {
93
+ isError: false,
94
+ content: [{ type: 'text', text: JSON.stringify(report) }],
95
+ structuredContent: report
96
+ };
97
+ }
98
+
99
+ function lintReport(registry, filesChecked) {
100
+ const issues = registry.findIssues();
101
+ const counts = { Error: 0, Warning: 0, Info: 0 };
102
+ const reportIssues = issues.map(issue => {
103
+ counts[issue.getSeverity()] += 1;
104
+ const start = issue.getStart();
105
+ const end = issue.getEnd();
106
+ return {
107
+ filename: issue.getFilename(),
108
+ rule: issue.getKey(),
109
+ severity: issue.getSeverity(),
110
+ message: issue.getMessage(),
111
+ start: { line: start.getRow(), column: start.getCol() },
112
+ end: { line: end.getRow(), column: end.getCol() }
113
+ };
114
+ });
115
+ return {
116
+ status: reportIssues.length === 0 ? 'clean' : 'issues',
117
+ filesChecked,
118
+ issueCount: reportIssues.length,
119
+ errors: counts.Error,
120
+ warnings: counts.Warning,
121
+ infos: counts.Info,
122
+ issues: reportIssues
123
+ };
124
+ }
125
+
126
+ function pathIsWithin(parent, candidate) {
127
+ const path = relative(parent, candidate);
128
+ return path === '' || (path !== '..' && !path.startsWith(`..${sep}`) && !isAbsolute(path));
129
+ }
130
+
131
+ function validateProjectArguments(arguments_) {
132
+ if (!isObject(arguments_)) throw new Error('Arguments must be an object');
133
+ rejectUnknownFields(arguments_, new Set(['directory', 'config']), 'argument');
134
+ if (arguments_.directory !== undefined) {
135
+ if (typeof arguments_.directory !== 'string' || arguments_.directory.includes('\0') ||
136
+ isAbsolute(arguments_.directory) || /^[A-Za-z]:[\\/]/.test(arguments_.directory) ||
137
+ arguments_.directory.split(/[\\/]+/).includes('..')) {
138
+ throw new Error('directory must be a relative path inside the configured workspace root');
139
+ }
140
+ }
141
+ if (arguments_.config !== undefined && !isObject(arguments_.config)) {
142
+ throw new Error('config must be an object');
143
+ }
144
+ }
145
+
146
+ async function loadProjectConfig(directory, override) {
147
+ if (override !== undefined) return { config: new Config(JSON.stringify(override)), source: 'caller' };
148
+ for (const filename of PROJECT_CONFIG_FILES) {
149
+ const configPath = resolve(directory, filename);
150
+ let info;
151
+ try { info = await lstat(configPath); }
152
+ catch (error) {
153
+ if (error?.code === 'ENOENT') continue;
154
+ throw error;
155
+ }
156
+ if (info.isSymbolicLink() || !info.isFile()) throw new Error(`${filename} must be a regular file inside the workspace`);
157
+ try {
158
+ return { config: new Config(await readFile(configPath, 'utf8')), source: filename };
159
+ } catch (error) {
160
+ throw new Error(`Could not load ${filename}: ${error.message || error}`);
161
+ }
162
+ }
163
+ return { config: Config.getDefault(), source: 'default' };
164
+ }
165
+
166
+ function enableSyntaxRules(config) {
167
+ const projectConfig = config.get();
168
+ const rules = { ...(projectConfig.rules || {}) };
169
+ const disabled = [];
170
+ for (const rule of ['parser_error', 'check_syntax']) {
171
+ if (rules[rule] === false) disabled.push(rule);
172
+ else if (rules[rule] === undefined) rules[rule] = true;
173
+ }
174
+ return {
175
+ config: new Config(JSON.stringify({ ...projectConfig, rules })),
176
+ disabled
177
+ };
178
+ }
179
+
180
+ async function dependencyRoots(config, projectRoot) {
181
+ const declared = config.get()?.dependencies;
182
+ if (!Array.isArray(declared)) return { roots: [], missing: [], count: 0 };
183
+ const roots = [];
184
+ const missing = [];
185
+ for (const dependency of declared) {
186
+ const folder = dependency?.folder;
187
+ if (typeof folder !== 'string' || !folder.trim()) {
188
+ missing.push(String(folder || '(unspecified folder)'));
189
+ continue;
190
+ }
191
+ const relativeFolder = folder.replace(/^[\\/]+/, '');
192
+ if (/^[A-Za-z]:[\\/]/.test(relativeFolder) || relativeFolder.split(/[\\/]+/).includes('..')) {
193
+ throw new Error('Dependency folders must stay inside the configured project directory');
194
+ }
195
+ const candidate = resolve(projectRoot, relativeFolder || '.');
196
+ if (!pathIsWithin(projectRoot, candidate)) throw new Error('Dependency folders must stay inside the configured project directory');
197
+ let canonical;
198
+ try { canonical = await realpath(candidate); }
199
+ catch (error) {
200
+ if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') {
201
+ missing.push(folder);
202
+ continue;
203
+ }
204
+ throw error;
205
+ }
206
+ if (!pathIsWithin(projectRoot, canonical)) throw new Error('Dependency folders must stay inside the configured project directory');
207
+ const info = await lstat(canonical);
208
+ if (!info.isDirectory()) {
209
+ missing.push(folder);
210
+ continue;
211
+ }
212
+ roots.push(canonical);
213
+ }
214
+ return { roots, missing, count: declared.length };
215
+ }
216
+
217
+ async function collectProjectFiles(projectRoot, dependencies) {
218
+ const sourceFiles = [];
219
+ const dependencyFiles = [];
220
+ const pending = [projectRoot];
221
+ let totalBytes = 0;
222
+ while (pending.length) {
223
+ const directory = pending.pop();
224
+ const entries = await readdir(directory, { withFileTypes: true });
225
+ entries.sort((left, right) => left.name.localeCompare(right.name));
226
+ for (const entry of entries) {
227
+ const absolutePath = resolve(directory, entry.name);
228
+ if (entry.isDirectory()) {
229
+ const isDependencyPath = dependencies.some(root => pathIsWithin(absolutePath, root) || pathIsWithin(root, absolutePath));
230
+ if (!IGNORED_DIRECTORIES.has(entry.name) || isDependencyPath) pending.push(absolutePath);
231
+ continue;
232
+ }
233
+ if (!entry.isFile()) continue;
234
+ const filename = relative(projectRoot, absolutePath).split(sep).join('/');
235
+ const basename = filename.split('/').at(-1);
236
+ const nameParts = basename.split('.');
237
+ if (nameParts.length < 3 || !nameParts[0] || !nameParts[1]) continue;
238
+ if (!new MemoryFile(filename, '').getObjectType()) continue;
239
+ const info = await lstat(absolutePath);
240
+ if (!info.isFile() || info.isSymbolicLink()) continue;
241
+ if (info.size > MAX_PROJECT_FILE_BYTES) throw new Error(`ABAP file exceeds ${MAX_PROJECT_FILE_BYTES} bytes: ${filename}`);
242
+ totalBytes += info.size;
243
+ if (totalBytes > MAX_PROJECT_SOURCE_BYTES) throw new Error(`Workspace ABAP sources exceed ${MAX_PROJECT_SOURCE_BYTES} bytes`);
244
+ const file = new MemoryFile(filename, await readFile(absolutePath, 'utf8'));
245
+ const isDependency = dependencies.some(root => pathIsWithin(root, absolutePath));
246
+ (isDependency ? dependencyFiles : sourceFiles).push(file);
247
+ if (sourceFiles.length + dependencyFiles.length > MAX_PROJECT_FILES) {
248
+ throw new Error(`Workspace contains more than ${MAX_PROJECT_FILES} ABAP files`);
249
+ }
250
+ }
251
+ }
252
+ return { sourceFiles, dependencyFiles };
253
+ }
254
+
68
255
  export async function runABAPLint(arguments_) {
69
256
  try {
70
257
  validateArguments(arguments_);
@@ -74,31 +261,49 @@ export async function runABAPLint(arguments_) {
74
261
  const files = arguments_.files.map(({ filename, source }) => new MemoryFile(filename, source));
75
262
  const registry = new Registry(config).addFiles(files);
76
263
  await registry.parseAsync();
77
- const issues = registry.findIssues();
78
- const counts = { Error: 0, Warning: 0, Info: 0 };
79
- const reportIssues = issues.map(issue => {
80
- counts[issue.getSeverity()] += 1;
81
- const start = issue.getStart();
82
- const end = issue.getEnd();
83
- return {
84
- filename: issue.getFilename(),
85
- rule: issue.getKey(),
86
- severity: issue.getSeverity(),
87
- message: issue.getMessage(),
88
- start: { line: start.getRow(), column: start.getCol() },
89
- end: { line: end.getRow(), column: end.getCol() }
90
- };
264
+ return { isError: false, content: [{ type: 'text', text: JSON.stringify(lintReport(registry, files.length)) }] };
265
+ } catch (error) {
266
+ return errorResult(error);
267
+ }
268
+ }
269
+
270
+ export async function runABAPLintProject(arguments_, { workspaceRoot = process.cwd() } = {}) {
271
+ try {
272
+ validateProjectArguments(arguments_);
273
+ const root = await realpath(workspaceRoot);
274
+ const directory = resolve(root, arguments_.directory || '.');
275
+ if (!pathIsWithin(root, directory)) throw new Error('directory must stay inside the configured workspace root');
276
+ const projectRoot = await realpath(directory);
277
+ if (!pathIsWithin(root, projectRoot)) throw new Error('directory must stay inside the configured workspace root');
278
+ const projectInfo = await lstat(projectRoot);
279
+ if (!projectInfo.isDirectory()) throw new Error('directory must identify a workspace directory');
280
+
281
+ const { config: projectConfig, source: configSource } = await loadProjectConfig(projectRoot, arguments_.config);
282
+ const { config, disabled: disabledSyntaxRules } = enableSyntaxRules(projectConfig);
283
+ const dependencies = await dependencyRoots(config, projectRoot);
284
+ const { sourceFiles, dependencyFiles } = await collectProjectFiles(projectRoot, dependencies.roots);
285
+ const registry = new Registry(config);
286
+ registry.addDependencies(dependencyFiles);
287
+ registry.addFiles(sourceFiles);
288
+ await registry.parseAsync();
289
+
290
+ const report = lintReport(registry, sourceFiles.length);
291
+ if (report.filesChecked === 0) report.status = 'no-abap-files';
292
+ else if (report.status === 'clean' && (dependencies.missing.length || disabledSyntaxRules.length)) report.status = 'incomplete';
293
+ return reportResult({
294
+ ...report,
295
+ scope: 'workspace-static',
296
+ activation: 'not-performed',
297
+ workspaceDirectory: arguments_.directory || '.',
298
+ configSource,
299
+ targetRelease: config.getVersion(),
300
+ syntaxRules: { enabled: ['parser_error', 'check_syntax'].filter(rule => !disabledSyntaxRules.includes(rule)), disabled: disabledSyntaxRules },
301
+ dependencies: {
302
+ declared: dependencies.count,
303
+ filesLoaded: dependencyFiles.length,
304
+ missingFolders: dependencies.missing
305
+ }
91
306
  });
92
- const report = {
93
- status: reportIssues.length === 0 ? 'clean' : 'issues',
94
- filesChecked: files.length,
95
- issueCount: reportIssues.length,
96
- errors: counts.Error,
97
- warnings: counts.Warning,
98
- infos: counts.Info,
99
- issues: reportIssues
100
- };
101
- return { isError: false, content: [{ type: 'text', text: JSON.stringify(report) }] };
102
307
  } catch (error) {
103
308
  return errorResult(error);
104
309
  }
@@ -7,7 +7,7 @@ import { decodeJsonFileBuffer } from './mcp-config.mjs';
7
7
 
8
8
  const MANAGED_ENV = 'SAP_AI_DEV_TOOLKIT_MANAGED';
9
9
 
10
- function mcpServersPath(harness, { home = process.env.HOME || homedir(), env = process.env } = {}) {
10
+ function mcpServersPath(harness, { home = process.env.HOME || homedir(), env = process.env, platform = process.platform } = {}) {
11
11
  switch (harness.id) {
12
12
  case 'claude-code':
13
13
  return join(home, '.claude.json');
@@ -15,8 +15,16 @@ function mcpServersPath(harness, { home = process.env.HOME || homedir(), env = p
15
15
  return join(harnessRoot(harness, { home, env }), 'mcp.json');
16
16
  case 'gemini-cli':
17
17
  return join(harnessRoot(harness, { home, env }), 'settings.json');
18
+ case 'omp':
19
+ return join(harnessRoot(harness, { home, env }), 'mcp.json');
18
20
  case 'pi-coding-agent':
19
21
  return join(harnessRoot(harness, { home, env }), 'mcp.json');
22
+ case 'windsurf': {
23
+ const configRoot = platform === 'win32'
24
+ ? (typeof env?.APPDATA === 'string' && env.APPDATA.trim() ? env.APPDATA.trim() : join(home, 'AppData', 'Roaming'))
25
+ : (typeof env?.XDG_CONFIG_HOME === 'string' && env.XDG_CONFIG_HOME.trim() ? env.XDG_CONFIG_HOME.trim() : join(home, '.config'));
26
+ return join(configRoot, 'devin', 'mcp_config.json');
27
+ }
20
28
  default:
21
29
  return null;
22
30
  }
package/src/harnesses.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { homedir } from 'node:os';
2
- import { join } from 'node:path';
2
+ import { join, resolve } from 'node:path';
3
3
 
4
4
  export const HARNESS_ENV_VAR = 'SAP_AI_DEV_TOOLKIT_HARNESSES';
5
5
  export const DEFAULT_HARNESS_IDS = ['github-copilot', 'claude-code'];
@@ -38,6 +38,19 @@ export function piCodingAgentRoot({ home = process.env.HOME || homedir(), env =
38
38
  return override || join(home, '.pi', 'agent');
39
39
  }
40
40
 
41
+ export function ompAgentRoot({ home = process.env.HOME || homedir(), env = process.env } = {}) {
42
+ const agentOverride = typeof env?.PI_CODING_AGENT_DIR === 'string' ? env.PI_CODING_AGENT_DIR.trim() : '';
43
+ if (agentOverride) return resolve(agentOverride);
44
+ const configDirectory = typeof env?.PI_CONFIG_DIR === 'string' ? env.PI_CONFIG_DIR.trim() : '';
45
+ return join(home, configDirectory || '.omp', 'agent');
46
+ }
47
+
48
+ export function windsurfSkillsRoot({ home = process.env.HOME || homedir(), env = process.env, platform = process.platform } = {}) {
49
+ if (platform === 'win32') return join(home, '.codeium', 'windsurf');
50
+ const override = typeof env?.XDG_CONFIG_HOME === 'string' ? env.XDG_CONFIG_HOME.trim() : '';
51
+ return join(override || join(home, '.config'), 'devin');
52
+ }
53
+
41
54
  export function claudeCodeAgentDocument(source) {
42
55
  const frontmatter = splitFrontmatter(source);
43
56
  if (!frontmatter) throw new Error('unsupported agent frontmatter');
@@ -86,7 +99,11 @@ export const HARNESSES = [
86
99
  { id: 'cursor', label: 'Cursor', directory: '.cursor', supportsAgents: false },
87
100
  { id: 'gemini-cli', label: 'Gemini CLI', directory: '.gemini', supportsAgents: false },
88
101
  { id: 'opencode', label: 'opencode', directory: 'opencode', configHome: true, supportsAgents: true, agentFileName: agentMarkdownName, agentDocument: opencodeAgentDocument },
89
- { id: 'pi-coding-agent', label: 'Pi Coding Agent', directory: '.pi/agent', supportsAgents: true, agentTargetPrefix: 'prompts', agentFileName: agentMarkdownName, agentDocument: piCodingAgentPromptDocument, root: piCodingAgentRoot }
102
+ { id: 'pi-coding-agent', label: 'Pi Coding Agent', directory: '.pi/agent', supportsAgents: true, agentTargetPrefix: 'prompts', agentFileName: agentMarkdownName, agentDocument: piCodingAgentPromptDocument, root: piCodingAgentRoot },
103
+ { id: 'omp', label: 'Oh My Pi (OMP)', directory: '.omp/agent', supportsAgents: false, root: ompAgentRoot },
104
+ { id: 'cline', label: 'Cline', directory: '.cline', supportsAgents: false },
105
+ { id: 'roo-code', label: 'Roo Code', directory: '.roo', supportsAgents: false },
106
+ { id: 'windsurf', label: 'Windsurf / Devin Desktop', directory: 'devin', supportsAgents: false, root: windsurfSkillsRoot }
90
107
  ];
91
108
 
92
109
  export function harnessById(id) {
@@ -97,8 +114,8 @@ export function harnessById(id) {
97
114
  return harness;
98
115
  }
99
116
 
100
- export function harnessRoot(harness, { home = process.env.HOME || homedir(), env = process.env } = {}) {
101
- if (harness.root) return harness.root({ home, env });
117
+ export function harnessRoot(harness, { home = process.env.HOME || homedir(), env = process.env, platform = process.platform } = {}) {
118
+ if (harness.root) return harness.root({ home, env, platform });
102
119
  if (harness.configHome) {
103
120
  const override = typeof env?.XDG_CONFIG_HOME === 'string' ? env.XDG_CONFIG_HOME.trim() : '';
104
121
  return join(override || join(home, '.config'), harness.directory);
package/src/launcher.mjs CHANGED
@@ -107,7 +107,7 @@ async function runDoctor(destinations) {
107
107
  // unprefixed (get_source, run_query); match the slug-prefixed form too
108
108
  // in case this ever runs against a multi-destination server.
109
109
  const toolPrefix = `${slugifyDestination(destination.name)}_`;
110
- const localSegments = new Set(['lint_abap', 'get_application_log', 'prepare_abap_change_set', 'apply_abap_change_set', 'check_transport_readiness', 'plan_abap_cloud_migration', 'generate_rap_regression_suite', 'run_rap_regression_suite']);
110
+ const localSegments = new Set(['lint_abap', 'lint_abap_project', 'get_application_log', 'prepare_abap_change_set', 'apply_abap_change_set', 'check_transport_readiness', 'plan_abap_cloud_migration', 'generate_rap_regression_suite', 'run_rap_regression_suite']);
111
111
  const isUpstreamTool = tool => (tool.name.startsWith(toolPrefix) ? tool.name.slice(toolPrefix.length) : tool.name);
112
112
  const upstreamCount = tools.filter(tool => !localSegments.has(isUpstreamTool(tool))).length;
113
113
  checks.push(doctorRow(destination.name, 'MCP tools/list', upstreamCount ? 'passed' : 'failed', `${upstreamCount} upstream tools and ${tools.length} total MCP tools returned; chat-picker binding is host-managed`));
package/src/mcp-proxy.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { spawn as nodeSpawn } from 'node:child_process';
2
2
  import { once } from 'node:events';
3
3
  import { sanitizeChildEnv, slugifyDestination } from './bas-discovery.mjs';
4
- import { ABAP_LINT_TOOL, runABAPLint } from './abaplint.mjs';
4
+ import { ABAP_LINT_PROJECT_TOOL, ABAP_LINT_TOOL, runABAPLint, runABAPLintProject } from './abaplint.mjs';
5
5
  import { createEngineeringTools } from './engineering-tools.mjs';
6
6
  import { brandedEnvValue } from './branding.mjs';
7
7
  import { diagnosticText, redactText } from './redact.mjs';
@@ -698,6 +698,12 @@ export class MCPProxy {
698
698
  });
699
699
  }
700
700
  }
701
+ const projectLintName = snakeCaseName(ABAP_LINT_PROJECT_TOOL.name);
702
+ publish(projectLintName, {
703
+ handler: arguments_ => runABAPLintProject(arguments_, {
704
+ workspaceRoot: brandedEnvValue(this.env, 'WORKSPACE_ROOT') || process.cwd()
705
+ })
706
+ }, { ...ABAP_LINT_PROJECT_TOOL, name: projectLintName });
701
707
  if (!merged.length && this.children.length) throw new Error('No destination child provided tools');
702
708
  // Swap atomically so a concurrent tools/call never sees a partial map.
703
709
  this.namespace = namespace;
package/src/setup.mjs CHANGED
@@ -8,6 +8,7 @@ import { adtBaseUrl, adtPort, defaultAdtUrl, discoverSapGuiSystems, hasExplicitP
8
8
  import { discoverCloudFoundryDestinations, getCloudFoundryTarget, deleteManagedCloudFoundryServiceKeys, findOrphanedCloudFoundryServiceKeys } from './cf-destination.mjs';
9
9
  import { SAP_DEVELOPMENT_MCP_SERVERS, collectCloudFoundryKeyReferencesFromAllEntries, collectManagedCloudFoundryKeyReferences, installMcpConfig, readMcpConfig, resolveMcpConfigPath } from './mcp-config.mjs';
10
10
  import { discoverCertificateDnsNames } from './tls-adt-proxy.mjs';
11
+ import { findRemoteControllableSsoBrowsers } from './sso-browsers.mjs';
11
12
 
12
13
  const SETUP_COMMAND = 'sap-ai-dev --setup';
13
14
 
@@ -66,6 +67,45 @@ async function chooseLocalAuthMode(destination, { input = stdin, output = stdout
66
67
  }, { input, output });
67
68
  }
68
69
 
70
+ function createSsoBrowserSelector({ discoverSsoBrowsers, input, output, env }) {
71
+ let selectedPath;
72
+ let selectionComplete = false;
73
+ return async destination => {
74
+ if (selectionComplete) return selectedPath;
75
+ selectionComplete = true;
76
+ print(output, formatStatus('Checking installed browsers for remote DevTools access.', 'progress', output, 'SSO'));
77
+ let result;
78
+ try {
79
+ result = await discoverSsoBrowsers({ env });
80
+ } catch (error) {
81
+ const reason = String(error?.message || error).replace(/\s+/g, ' ').slice(0, 240);
82
+ print(output, formatStatus(`Browser control check failed (${reason}). Setup will keep automatic browser discovery.`, 'warning', output, 'SSO'));
83
+ return undefined;
84
+ }
85
+ const { browsers = [], unavailable = [] } = result || {};
86
+ if (!browsers.length) {
87
+ const details = unavailable.map(browser => `${browser.name}: ${browser.reason}`).join('; ').slice(0, 300);
88
+ print(output, formatStatus(`No remotely controllable SSO browser was found${details ? ` (${details})` : ''}. Setup will keep automatic browser discovery.`, 'warning', output, 'SSO'));
89
+ return undefined;
90
+ }
91
+ if (browsers.length === 1) {
92
+ selectedPath = browsers[0].path;
93
+ print(output, formatStatus(`Using ${browsers[0].name}; it responded to a remote DevTools command.`, 'success', output, 'SSO browser'));
94
+ return selectedPath;
95
+ }
96
+ selectedPath = await selectPrompt({
97
+ message: `Browser for SSO (${destination.name})`,
98
+ defaultValue: browsers[0].path,
99
+ choices: browsers.map((browser, index) => ({
100
+ name: `${browser.name} (${browser.path})`,
101
+ value: browser.path,
102
+ shortcut: String(index + 1)
103
+ }))
104
+ }, { input, output });
105
+ return selectedPath;
106
+ };
107
+ }
108
+
69
109
  async function confirmCloudFoundryImport({ input = stdin, output = stdout } = {}) {
70
110
  const readline = createInterface({ input, output });
71
111
  try {
@@ -238,7 +278,7 @@ async function confirmPrompt(message, { input = stdin, output = stdout } = {}) {
238
278
  return !/^n(?:o)?$/i.test(String(answer || '').trim());
239
279
  }
240
280
 
241
- function buildLocalClientDestination(destination, client, authMode, { multipleClients = false, duplicateSystemName = false } = {}) {
281
+ function buildLocalClientDestination(destination, client, authMode, { multipleClients = false, duplicateSystemName = false, browserPath } = {}) {
242
282
  const name = localClientDestinationName(destination, client, { multipleClients, duplicateSystemName });
243
283
  const localDestination = { ...destination, name, serverName: name, client };
244
284
  const userInputId = `sap-ai-dev-${localDestination.serverName || localDestination.name}-user`.toLowerCase().replace(/[^a-z0-9_-]+/g, '-');
@@ -248,7 +288,7 @@ function buildLocalClientDestination(destination, client, authMode, { multipleCl
248
288
  { id: passwordInputId, type: 'promptString', description: `SAP password for ${localDestination.name}`, password: true }
249
289
  ];
250
290
  if (authMode === 'sso') {
251
- localDestination.childEnv = { SAP_AUTH_MODE: 'sso' };
291
+ localDestination.childEnv = { SAP_AUTH_MODE: 'sso', ...(browserPath ? { SAP_BROWSER_EXEC: browserPath } : {}) };
252
292
  localDestination.inputs = [];
253
293
  } else {
254
294
  localDestination.childEnv = {
@@ -285,7 +325,7 @@ async function maybeConfigureTlsSelfHeal(destination, promptLabel, { input = std
285
325
  // collects the minimal ADT configuration directly from the user and produces
286
326
  // the same local destination shape as the SAP GUI flow, so installation,
287
327
  // credential inputs, and TLS handling stay identical.
288
- async function runManualEntryWizard({ env = process.env, input = stdin, output = stdout, detectAdtUrl } = {}) {
328
+ async function runManualEntryWizard({ env = process.env, input = stdin, output = stdout, detectAdtUrl, selectSsoBrowser } = {}) {
289
329
  print(output, '');
290
330
  print(output, colorText('✍️ Manual system entry — skip discovery, add your SAP system directly', 'magenta', output));
291
331
  print(output, colorText(' The ADT URL is the base URL that Eclipse ADT or VS Code ADT tools use, for example https://myhost.example:44300.', 'cyan', output));
@@ -338,10 +378,11 @@ async function runManualEntryWizard({ env = process.env, input = stdin, output =
338
378
  } else {
339
379
  if (authMode === 'sso') warnSsoIpHost(destination, name, output);
340
380
  else await maybeConfigureTlsSelfHeal(destination, name, { input, output });
381
+ const browserPath = authMode === 'sso' ? await selectSsoBrowser(destination) : undefined;
341
382
  destination.authentication = localAuthenticationLabel(authMode);
342
383
  destination.authMode = authMode;
343
384
  for (const client of clients) {
344
- expanded.push(buildLocalClientDestination(destination, client, authMode, { multipleClients: clients.length > 1 }));
385
+ expanded.push(buildLocalClientDestination(destination, client, authMode, { multipleClients: clients.length > 1, browserPath }));
345
386
  }
346
387
  print(output, formatStatus(`Added ${name} (client${clients.length === 1 ? '' : 's'} ${clients.join(', ')}).`, 'success', output, 'Manual entry'));
347
388
  }
@@ -469,7 +510,7 @@ function printConnectionInstructions(output, result) {
469
510
 
470
511
  // Checkbox selection over the discovered destinations plus, outside BAS, the
471
512
  // per-system confirmation prompts (clients, ADT URL, TLS self-heal, login).
472
- async function chooseDiscoveredDestinations(destinations, { isBas, input = stdin, output = stdout, env = process.env, detectAdtUrl, previousAdtUrls = new Map() } = {}) {
513
+ async function chooseDiscoveredDestinations(destinations, { isBas, input = stdin, output = stdout, env = process.env, detectAdtUrl, previousAdtUrls = new Map(), selectSsoBrowser } = {}) {
473
514
  const orderedDestinations = [...destinations].sort((left, right) => {
474
515
  const leftRank = left.disabledReason ? 2 : (left.probe?.available === false ? 1 : 0);
475
516
  const rightRank = right.disabledReason ? 2 : (right.probe?.available === false ? 1 : 0);
@@ -495,7 +536,7 @@ async function chooseDiscoveredDestinations(destinations, { isBas, input = stdin
495
536
  print(output, colorText(' Space = select/deselect · a = toggle all · m = add a system manually · Enter = confirm.', 'cyan', output));
496
537
  print(output, colorText(' 💡 No system listed, or need an extra one? Press m to skip discovery and add any SAP system manually.', 'magenta', output));
497
538
  print(output, '');
498
- const addManually = async () => (await runManualEntryWizard({ env, input, output, detectAdtUrl })).map(destination => ({
539
+ const addManually = async () => (await runManualEntryWizard({ env, input, output, detectAdtUrl, selectSsoBrowser })).map(destination => ({
499
540
  value: destination,
500
541
  name: `${destination.name} (manual, client ${destination.client})`
501
542
  }));
@@ -549,10 +590,11 @@ async function chooseDiscoveredDestinations(destinations, { isBas, input = stdin
549
590
  }
550
591
  if (authMode === 'sso') warnSsoIpHost(destination, promptLabel, output);
551
592
  else await maybeConfigureTlsSelfHeal(destination, promptLabel, { input, output });
593
+ const browserPath = authMode === 'sso' ? await selectSsoBrowser(destination) : undefined;
552
594
  destination.authentication = localAuthenticationLabel(authMode);
553
595
  destination.authMode = authMode;
554
596
  for (const client of clients) {
555
- expanded.push(buildLocalClientDestination(destination, client, authMode, { multipleClients: clients.length > 1, duplicateSystemName }));
597
+ expanded.push(buildLocalClientDestination(destination, client, authMode, { multipleClients: clients.length > 1, duplicateSystemName, browserPath }));
556
598
  }
557
599
  }
558
600
  selected = expanded;
@@ -570,7 +612,8 @@ export async function runSetup({
570
612
  confirmCfImport = confirmCloudFoundryImport,
571
613
  install = installMcpConfig,
572
614
  includeSapDevelopmentToolsPrompt = false,
573
- detectAdtUrl
615
+ detectAdtUrl,
616
+ discoverSsoBrowsers = findRemoteControllableSsoBrowsers
574
617
  } = {}) {
575
618
  const isBas = Boolean(env.H2O_URL);
576
619
  if (!isBas && (!input.isTTY || !output.isTTY)) {
@@ -581,6 +624,7 @@ export async function runSetup({
581
624
  print(output, formatStatus(`Interactive setup needs a terminal. Rerun ${SETUP_COMMAND} from a terminal.`, 'warning', output, 'SAP AI Dev Toolkit'));
582
625
  return { skipped: true, reason: 'non-tty' };
583
626
  }
627
+ const selectSsoBrowser = createSsoBrowserSelector({ discoverSsoBrowsers, input, output, env });
584
628
  print(output, '');
585
629
  print(output, colorText('💡 Tip: you can skip auto-discovery and add a system manually — press m in the destination picker, or take the wizard offer when nothing is found.', 'magenta', output));
586
630
  if (!isBas) print(output, formatStatus('Looking for local SAP GUI system configuration.', 'progress', output, 'Setup'));
@@ -721,11 +765,11 @@ export async function runSetup({
721
765
  ]
722
766
  }, { input, output });
723
767
  if (choice !== 'manual') return await exitWithoutDestinations();
724
- selected = await runManualEntryWizard({ env, input, output, detectAdtUrl });
768
+ selected = await runManualEntryWizard({ env, input, output, detectAdtUrl, selectSsoBrowser });
725
769
  additiveSetup = true;
726
770
  if (!selected.length) return await exitWithoutDestinations();
727
771
  } else {
728
- selected = await chooseDiscoveredDestinations(destinations, { isBas, input, output, env, detectAdtUrl, previousAdtUrls });
772
+ selected = await chooseDiscoveredDestinations(destinations, { isBas, input, output, env, detectAdtUrl, previousAdtUrls, selectSsoBrowser });
729
773
  additiveSetup = selected.length > 0;
730
774
  }
731
775
 
@@ -1,8 +1,9 @@
1
- import { execFile } from 'node:child_process';
2
- import { access, constants } from 'node:fs/promises';
3
- import { homedir } from 'node:os';
4
- import { posix, win32 } from 'node:path';
1
+ import { execFile, spawn } from 'node:child_process';
2
+ import { access, constants, mkdtemp, rm } from 'node:fs/promises';
3
+ import { homedir, tmpdir } from 'node:os';
4
+ import { join, posix, win32 } from 'node:path';
5
5
  import { promisify } from 'node:util';
6
+ import { WebSocket } from 'undici';
6
7
 
7
8
  const execFileAsync = promisify(execFile);
8
9
 
@@ -59,6 +60,155 @@ async function fileExists(path) {
59
60
  }
60
61
  }
61
62
 
63
+ function remoteDebuggingEndpoint(output) {
64
+ return /DevTools listening on (ws:\/\/[^\s]+)/.exec(output)?.[1] || '';
65
+ }
66
+
67
+ function waitForRemoteDebuggingEndpoint(child, timeoutMs) {
68
+ return new Promise((resolve, reject) => {
69
+ let output = '';
70
+ let settled = false;
71
+ const finish = (error, endpoint) => {
72
+ if (settled) return;
73
+ settled = true;
74
+ clearTimeout(timer);
75
+ child.stdout?.off('data', onData);
76
+ child.stderr?.off('data', onData);
77
+ child.off('error', onError);
78
+ child.off('close', onClose);
79
+ if (error) reject(error);
80
+ else resolve(endpoint);
81
+ };
82
+ const onData = chunk => {
83
+ output = `${output}${chunk}`.slice(-8192);
84
+ const endpoint = remoteDebuggingEndpoint(output);
85
+ if (endpoint) finish(null, endpoint);
86
+ };
87
+ const onError = error => finish(error);
88
+ const onClose = (code, signal) => finish(new Error(`browser exited before exposing DevTools (code ${code ?? 'unknown'}, signal ${signal || 'none'})`));
89
+ const timer = setTimeout(() => finish(new Error(`browser did not expose DevTools within ${timeoutMs}ms`)), timeoutMs);
90
+ child.stdout?.on('data', onData);
91
+ child.stderr?.on('data', onData);
92
+ child.once('error', onError);
93
+ child.once('close', onClose);
94
+ });
95
+ }
96
+
97
+ function controlBrowser(endpoint, timeoutMs) {
98
+ const url = new URL(endpoint);
99
+ if (url.protocol !== 'ws:' || !['localhost', '127.0.0.1', '::1', '[::1]'].includes(url.hostname)) {
100
+ throw new Error('browser exposed a non-local DevTools endpoint');
101
+ }
102
+
103
+ return new Promise((resolve, reject) => {
104
+ let settled = false;
105
+ const socket = new WebSocket(url.href);
106
+ const finish = error => {
107
+ if (settled) return;
108
+ settled = true;
109
+ clearTimeout(timer);
110
+ try { socket.close(); } catch {}
111
+ if (error) reject(error);
112
+ else resolve();
113
+ };
114
+ const timer = setTimeout(() => finish(new Error(`DevTools command timed out after ${timeoutMs}ms`)), timeoutMs);
115
+ socket.addEventListener('open', () => socket.send(JSON.stringify({ id: 1, method: 'Browser.getVersion' })));
116
+ socket.addEventListener('message', event => {
117
+ let response;
118
+ try { response = JSON.parse(event.data); }
119
+ catch { return finish(new Error('DevTools returned an invalid response')); }
120
+ if (response.id !== 1) return;
121
+ if (response.error) return finish(new Error(response.error.message || 'DevTools rejected Browser.getVersion'));
122
+ if (!response.result?.product) return finish(new Error('DevTools returned no browser version'));
123
+ finish();
124
+ });
125
+ socket.addEventListener('error', () => finish(new Error('could not connect to the browser DevTools endpoint')));
126
+ socket.addEventListener('close', () => {
127
+ if (!settled) finish(new Error('browser closed the DevTools connection'));
128
+ });
129
+ });
130
+ }
131
+
132
+ function waitForClose(child, timeoutMs) {
133
+ return new Promise(resolve => {
134
+ if (child.exitCode !== null || child.signalCode !== null) return resolve(true);
135
+ const timer = setTimeout(() => {
136
+ child.off('close', onClose);
137
+ resolve(false);
138
+ }, timeoutMs);
139
+ const onClose = () => {
140
+ clearTimeout(timer);
141
+ resolve(true);
142
+ };
143
+ child.once('close', onClose);
144
+ });
145
+ }
146
+
147
+ async function signalBrowserTree(child, platform, signal) {
148
+ if (platform === 'win32' && child.pid) {
149
+ try {
150
+ await execFileAsync('taskkill', [
151
+ '/pid', String(child.pid), '/t',
152
+ ...(signal === 'SIGKILL' ? ['/f'] : [])
153
+ ], { windowsHide: true, timeout: 1000 });
154
+ return;
155
+ } catch {}
156
+ }
157
+ try {
158
+ if (platform !== 'win32' && child.pid) process.kill(-child.pid, signal);
159
+ else child.kill(signal);
160
+ } catch {
161
+ try { child.kill(signal); } catch {}
162
+ }
163
+ }
164
+
165
+ async function stopBrowser(child, platform) {
166
+ if (!child || child.exitCode !== null || child.signalCode !== null) return;
167
+ await signalBrowserTree(child, platform, 'SIGTERM');
168
+ if (await waitForClose(child, 1000)) return;
169
+ await signalBrowserTree(child, platform, 'SIGKILL');
170
+ await waitForClose(child, 1000);
171
+ }
172
+
173
+ export async function probeSsoBrowser(browser, {
174
+ platform = process.platform,
175
+ timeoutMs = 5000,
176
+ spawnBrowser = spawn,
177
+ controlRemote = controlBrowser
178
+ } = {}) {
179
+ let profile;
180
+ let child;
181
+ try {
182
+ profile = await mkdtemp(join(tmpdir(), 'sap-ai-dev-sso-probe-'));
183
+ child = spawnBrowser(browser.path, [
184
+ `--user-data-dir=${profile}`,
185
+ '--remote-debugging-port=0',
186
+ '--headless=new',
187
+ '--no-first-run',
188
+ '--no-default-browser-check',
189
+ 'about:blank'
190
+ ], { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, detached: platform !== 'win32' });
191
+ const endpoint = await waitForRemoteDebuggingEndpoint(child, timeoutMs);
192
+ await controlRemote(endpoint, timeoutMs);
193
+ return { usable: true };
194
+ } catch (error) {
195
+ const reason = String(error?.message || error).replace(/\s+/g, ' ').slice(0, 240);
196
+ return { usable: false, reason };
197
+ } finally {
198
+ await stopBrowser(child, platform);
199
+ if (profile) await rm(profile, { recursive: true, force: true });
200
+ }
201
+ }
202
+
203
+ export async function findRemoteControllableSsoBrowsers({ probe = probeSsoBrowser, ...options } = {}) {
204
+ const candidates = await findSsoBrowsers(options);
205
+ const results = await Promise.all(candidates.map(async browser => ({ browser, ...await probe(browser) })));
206
+ return {
207
+ browsers: results.filter(result => result.usable).map(result => result.browser),
208
+ unavailable: results.filter(result => !result.usable).map(({ browser, reason }) => ({ ...browser, reason }))
209
+ };
210
+ }
211
+
62
212
  // Every Chromium-based browser the runtime can drive, best first; SAP_BROWSER_EXEC pins one ahead of the rest.
63
213
  export async function findSsoBrowsers({ env = process.env, platform = process.platform, exists = fileExists, appPaths = registeredAppPaths } = {}) {
64
214
  const candidates = [];
package/tools.md CHANGED
@@ -72,11 +72,12 @@ Hidden upstream tools are not directly callable through the proxy. Local workflo
72
72
 
73
73
  Broad/destructive surfaces such as object deletion, SQL trace, arbitrary SAP router calls, transport release/delete, RFC execution, debugger listen/delete-breakpoint helpers, and one-off generator/update helpers are intentionally not advertised by the BAS proxy. UI5 repository writes are hidden too: the ADT filestore only reads UI5 apps, so deploy them with SAP Fiori tools instead.
74
74
 
75
- ## Destination-scoped workflow tools
75
+ ## Local and destination-scoped workflow tools
76
76
 
77
- The proxy adds local multi-step tools to each destination when their required upstream capabilities are available. Like the upstream tools above, they are exposed publicly in lowercase snake_case (`lint_abap`, `get_application_log`, `prepare_abap_change_set`, and so on):
77
+ The proxy adds local tools to the MCP surface. Some are workspace-wide; others are added to each destination when their required upstream capabilities are available. Like the upstream tools above, they are exposed publicly in lowercase snake_case (`lint_abap`, `lint_abap_project`, `get_application_log`, and so on):
78
78
 
79
79
  - `LintABAP` — local in-memory ABAP linting.
80
+ - `LintABAPProject` — scan serialized ABAP objects in the configured workspace root (SAP_AI_DEV_TOOLKIT_WORKSPACE_ROOT, defaulting to the MCP server working directory) using the project abaplint config and available local dependency folders. This is static analysis only; it does not contact SAP or activate objects.
80
81
  - `GetApplicationLog` — bounded convenience mapping to the upstream SAP application-log route when available.
81
82
  - `PrepareABAPChangeSet` and `ApplyABAPChangeSet` — review full-source diffs and recheck source before explicit writes.
82
83
  - `CheckTransportReadiness` — collect a whitelisted bundle of transport, dependency, inactive-object, ABAP Unit, and ATC results.