Skip to main content
The SAP connector is a Java 21 Spring Boot application that bridges HTTP requests from the Cloudflare Worker to live SAP systems via SAP Java Connector (JCo). Each SAP system (ECC, S/4HANA) gets its own connector instance.

Architecture

Cloudflare Worker
  → POST /api/sap/read-table (JSON)
    → SecurityFilter (X-Connector-Key)
      → ReadTableController
        → ReadTableService
          → JCo → RFC_READ_TABLE → SAP
Every request follows the same pattern: controller validates input, service calls JCo, response wraps in ConnectorResponse.

Directory structure

connector/
├── pom.xml                          # Maven build (Java 21, Spring Boot 3)
├── Dockerfile                       # Production Docker image
├── lib/                             # SAP JCo libraries (not in repo)
│   ├── sapjco3.jar                  # Java API
│   └── libsapjco3.so               # Native library (Linux)
├── src/main/
│   ├── resources/
│   │   └── application.yml          # Spring Boot config
│   └── java/com/aisi/connector/
│       ├── ConnectorApplication.java
│       ├── config/
│       │   ├── JCoConfig.java       # JCo destination provider registration
│       │   ├── DynamicDestinationProvider.java # Static + per-request SAP destinations
│       │   ├── SapProperties.java   # @ConfigurationProperties
│       │   ├── SecurityFilter.java  # API key auth
│       │   └── SapUserFilter.java   # Optional per-request SAP user/password headers
│       ├── controller/              # REST endpoints
│       ├── service/                 # JCo business logic
│       ├── mock/                    # Mock services (no SAP needed)
│       ├── model/                   # ConnectorResponse, SapError
│       └── exception/              # GlobalExceptionHandler

Configuration

application.yml

server:
  port: 8080

sap:
  mock-mode: ${SAP_MOCK_MODE:false}
  host: ${SAP_HOST:}
  system-number: ${SAP_SYSTEM_NUMBER:00}
  client: ${SAP_CLIENT:100}
  user: ${SAP_USER:}
  password: ${SAP_PASSWORD:}
  language: ${SAP_LANGUAGE:EN}
  router-string: ${SAP_ROUTER_STRING:}
  pool-capacity: ${SAP_POOL_CAPACITY:5}

connector:
  api-key: ${CONNECTOR_API_KEY:}
All values can be overridden via environment variables.

Configuration properties

SapProperties.java binds the sap.* properties:
PropertyDefaultDescription
sap.mock-modefalseSkip JCo, use mock services
sap.hostSAP application server hostname
sap.system-number00System number (00–99)
sap.client100Mandant/client number
sap.userDefault/fallback SAP logon user (optional when using per-request headers)
sap.passwordDefault/fallback SAP logon password (optional when using per-request headers)
sap.languageENSAP logon language
sap.router-stringSAProuter string (for network routing)
sap.pool-capacity5JCo connection pool size

API endpoints

Most endpoints are POST /api/sap/* (with a few GET endpoints like probe-changes and read-source) and return a standard response wrapper:
{
  "success": true,
  "data": { ... }
}
On error:
{
  "success": false,
  "error": "Error message"
}

Health check

GET /health
Returns { status: "ok", mockMode: true/false }. No authentication required.

read-table

POST /api/sap/read-table
Reads rows from an SAP table via RFC_READ_TABLE.
ParamTypeDescription
table_namestringSAP table name (e.g., MARA)
fieldsstring[]Fields to return (optional — all if omitted)
filtersstring[]WHERE clauses (e.g., ["MTART = 'FERT'"])
limitnumberMax rows (default 100)
Response data: { table, row_count, sample_data: [{ field: value }] }

table-schema

POST /api/sap/table-schema
Returns DDIC metadata for a table (field names, types, lengths, key flags, descriptions).
ParamTypeDescription
table_namestringSAP table name
Response data: { table, field_count, fields: [{ name, type, length, key, description }] }

search-abap

POST /api/sap/search-abap
Searches for ABAP objects by name in TADIR.
ParamTypeDescription
search_stringstringSearch term
object_typesstring[]Filter by object type (optional)
Response data: { matches: [{ object_type, object_name, line, snippet }] }
The search_abap_source tool post-processes this response: it deduplicates matches by object name, strips line and snippet fields, and returns { search_pattern, object_types, match_count, matches: [{ object_type, object_name }] }.

analyze-code

POST /api/sap/analyze-code
Analyzes ABAP source for S/4HANA compatibility issues.
ParamTypeDescription
program_namestringABAP program name
analysis_typestringAnalysis type (optional)
Response data: { findings: [{ severity, line, message, recommendation }] }
The analyze_custom_code tool accepts object_name (which can be a program, function module, function group, or class). It resolves the program name before sending program_name to the connector.

apply-code-fix

POST /api/sap/apply-code-fix
Modifies ABAP program source via Editor Lock APIs.
ParamTypeDescription
programstringProgram name
changesarray[{ line, old_code, new_code }]
transportstringTransport request (optional)
Response data: { success, changes_applied }

create-transport

POST /api/sap/create-transport
Creates a transport request via BAPI_CTREQUEST_CREATE.
ParamTypeDescription
descriptionstringTransport description
target_systemstringTarget system ID
objectsarrayObjects to include (optional)
Response data: { transport_id, status }

execute-rfc

POST /api/sap/execute-rfc
Executes an arbitrary RFC function module.
ParamTypeDescription
function_modulestringRFC function name
import_paramsobjectImport parameters (optional)
tables_paramsobjectTable parameters (optional)
Response data: { export_params, return_tables, parsed_rows }

simulate-rfc

POST /api/sap/simulate-rfc
Dry-run validation of an RFC call — checks parameters without executing.
ParamTypeDescription
function_modulestringRFC function name
import_paramsobjectImport parameters (optional)
tables_paramsobjectTable parameters (optional)
Response data: { validation_passed, messages }

execute-rfc-batch

POST /api/sap/execute-rfc-batch
Executes multiple RFC calls in a single stateful session (same database LUW). Used by the ETL extract_transform_load tool for batched writes with transactional commit.
ParamTypeDescription
callsarray[{ function_module, import_params, tables_params }]
Response data: [{ function_module, success, message, export_params, return_tables }] (array of result objects)

transaction-details

POST /api/sap/transaction-details
Looks up a transaction code (TSTC table).
ParamTypeDescription
tcodestringTransaction code
Response data: { tcode, program, description, module }

probe-changes

GET /api/sap/probe-changes?since=YYYYMMDD
Checks whether metadata has changed since the given date (used to skip no-op delta runs).
ParamTypeDescription
sincestringYYYYMMDD date
Response data: { tableChanges, codeChanges }

index-custom-objects

POST /api/sap/index-custom-objects
POST /api/sap/index-custom-objects?since=YYYYMMDD
Indexes custom tables, classes, programs, and related metadata.

index-relationships

POST /api/sap/index-relationships
Indexes foreign keys and cross-reference relationships.

index-catalog-context

POST /api/sap/index-catalog-context
Indexes catalog context (package and module mappings).

index-custom-code

POST /api/sap/index-custom-code
POST /api/sap/index-custom-code?since=YYYYMMDD
Indexes custom code metadata for analysis.

index-modifications

POST /api/sap/index-modifications
Indexes user exits and modification markers.

index-enhancements

POST /api/sap/index-enhancements
Indexes enhancement spots and implementations.

index-s4-extensions

POST /api/sap/index-s4-extensions
Indexes S/4HANA extension metadata (S/4-only).

read-source

GET /api/sap/read-source?name=OBJECT_NAME&type=program|class|bapi
Reads ABAP source code for a program, class, or BAPI/FM. Response data: { object_name, object_type, source, line_count }

test-connection

POST /api/sap/test-connection
Tests connectivity and permissions. Returns system info, user info, roles, and capability test results. Response data:
{
  "system_info": { "host", "system_id", "client", "release", "database" },
  "user_info": { "username", "ustyp", "uflag", "gltgb" },
  "roles": ["ROLE_NAME", ...],
  "capability_tests": {
    "read_table": { "success": true },
    "get_schema": { "success": true },
    "read_dictionary": { "success": true },
    "analyze_code": { "success": true }
  }
}
See Connection Test Implementation for details.

JCo setup

Download

SAP JCo is proprietary and must be downloaded from the SAP Support Portal with S-user credentials. Search for “SAP JCo 3.1”. You need two files:
FilePlatformPurpose
sapjco3.jarAllJava API
libsapjco3.soLinuxNative library
sapjco3.dllWindowsNative library
libsapjco3.jnilibmacOSNative library
Place both in connector/lib/.

Maven dependency

The JCo JAR uses a system scope in pom.xml since it’s not in Maven Central:
<dependency>
    <groupId>com.sap.conn.jco</groupId>
    <artifactId>sapjco3</artifactId>
    <version>3.1</version>
    <scope>system</scope>
    <systemPath>${project.basedir}/lib/sapjco3.jar</systemPath>
</dependency>

Connection configuration

JCoConfig.java registers DynamicDestinationProvider, which supports:
  • Static destination from SapProperties (host/sysnr/client/language and optional fallback user/password)
  • Per-request dynamic destination when X-SAP-User and X-SAP-Password headers are provided
Core static properties:
Properties p = new Properties();
p.setProperty(JCO_ASHOST, props.getHost());
p.setProperty(JCO_SYSNR, props.getSystemNumber());
p.setProperty(JCO_CLIENT, props.getClient());
p.setProperty(JCO_LANG, props.getLanguage());
p.setProperty(JCO_POOL_CAPACITY, String.valueOf(props.getPoolCapacity()));
Per-request SAP credentials are applied by SapUserFilter:
  • Reads X-SAP-User and X-SAP-Password
  • Registers a thread-local dynamic destination for that request
  • Falls back to static destination when headers are absent
JCo manages its own connection pool internally. pool-capacity controls static destination pooling; per-request dynamic destinations use single-connection semantics.
JCoConfig is annotated with @ConditionalOnProperty(name = "sap.mock-mode", havingValue = "false"). When mock mode is enabled, no JCo destination is registered and no SAP connection is attempted.

Authentication

SecurityFilter.java implements a servlet filter:
  1. /health endpoint — no auth required
  2. If CONNECTOR_API_KEY is not set — all requests allowed (dev mode)
  3. Otherwise — requests must include X-Connector-Key header matching the configured key
On startup, a @PostConstruct validation ensures that CONNECTOR_API_KEY is set when running in real mode (SAP_MOCK_MODE=false). If the key is missing, the connector fails immediately with an IllegalStateException rather than silently running without authentication. The Cloudflare Worker sends connector auth via sap-connector-client.ts:
headers: {
  'Content-Type': 'application/json',
  'X-Connector-Key': connectorKey,
  'X-SAP-User': sapUser,        // optional
  'X-SAP-Password': sapPassword // optional
}
In production, always set CONNECTOR_API_KEY on the connector and store the same value in that connection’s connection_secrets.connector_key in the Worker app. Without this, any HTTP client can call the connector directly.

Mock mode

Set SAP_MOCK_MODE=true to run without a real SAP connection. Each service has a corresponding mock implementation:
ServiceMock
ReadTableServiceMockReadTableService
TableSchemaServiceMockTableSchemaService
RfcServiceMockRfcService
Spring Boot’s @ConditionalOnProperty automatically injects the correct service based on the sap.mock-mode flag. Mock services return realistic fake data. Mock mode is useful for:
  • Local development without SAP access
  • CI/CD test pipelines
  • Demo environments

Docker deployment

Build

cd connector
mvn clean package -DskipTests
docker build --platform linux/amd64 -t sap-connector .
The --platform linux/amd64 flag is required on Apple Silicon Macs because the JCo native library (libsapjco3.so) is x86-only.

Dockerfile

FROM eclipse-temurin:21-jre
WORKDIR /app
COPY target/connector-*.jar app.jar
COPY lib/sapjco3.jar /app/lib/sapjco3.jar
COPY lib/libsapjco3.so /app/lib/
ENV LD_LIBRARY_PATH=/app/lib
EXPOSE 8080
ENTRYPOINT ["java", "-Xmx1536m", "-Xms512m",
  "-Dloader.path=/app/lib/sapjco3.jar",
  "-jar", "app.jar"]

Run

docker run -p 8080:8080 \
  -e SAP_HOST=sap.example.com \
  -e SAP_SYSTEM_NUMBER=00 \
  -e SAP_CLIENT=100 \
  -e SAP_USER=your_user \
  -e SAP_PASSWORD=your_password \
  -e CONNECTOR_API_KEY=secret123 \
  sap-connector

Dual-system deployment

For dual-system support (ECC + S/4HANA), run two connector instances pointing to different SAP systems:
# S/4HANA connector on port 8080
docker run -d --name sap-connector \
  -p 8080:8080 \
  -e SAP_HOST=s4hana.example.com \
  -e SAP_CLIENT=100 \
  -e CONNECTOR_API_KEY=secret123 \
  sap-connector

# ECC connector on port 8081 (remap container's 8080 → host 8081)
docker run -d --name sap-connector-ecc \
  -p 8081:8080 \
  -e SAP_HOST=ecc.example.com \
  -e SAP_CLIENT=800 \
  -e CONNECTOR_API_KEY=secret123 \
  sap-connector
Both containers use the same Docker image. The second container remaps port 8080 inside the container to port 8081 on the host via -p 8081:8080 — no need to change SERVER_PORT.
Then configure both URLs as separate workspace_connections entries for the workspace. The Worker routes each SAP tool call to the correct connector using the tool input connection_id selected from workspace_connections (not by ecc_* / s4_* tool name prefixes).

Local development

Dev SAP systems

The project uses two hosted SAP training systems:
SystemIDDescriptionHost
S/4HANAS23S/4HANA 2023 FAA FPS00172.21.72.22
ECCIDESAP ECC 6.0 IDES172.21.72.16
Both are hosted by Michael Management / 1stBasis. If credentials expire or get locked, reset them at https://mmc-solman01.mmc.1stbasis.com:50101/m2bdiyadm# then recreate the container with the new password.

Building and running locally

cd connector
mvn clean package -DskipTests
docker build --platform linux/amd64 -t sap-connector .
S/4HANA connector (port 8080):
docker run -d --name sap-connector \
  -p 8080:8080 \
  -e SAP_SYSTEM_NUMBER=00 \
  -e SAP_CLIENT=100 \
  -e SAP_USER=STUDENT185 \
  -e "SAP_PASSWORD=Password1234" \
  -e SAP_LANGUAGE=EN \
  -e SAP_ROUTER_STRING=/H/161.38.17.212 \
  -e SAP_HOST=172.21.72.22 \
  -e CONNECTOR_API_KEY=aisi-connector-dev-key \
  sap-connector
ECC connector (port 8081):
docker run -d --name sap-connector-ecc \
  -p 8081:8080 \
  -e SAP_SYSTEM_NUMBER=01 \
  -e SAP_CLIENT=800 \
  -e SAP_USER=STUDENT008 \
  -e "SAP_PASSWORD=CA480nIv" \
  -e SAP_LANGUAGE=EN \
  -e SAP_ROUTER_STRING=/H/161.38.17.212 \
  -e SAP_HOST=172.21.72.16 \
  -e CONNECTOR_API_KEY=aisi-connector-dev-key \
  sap-connector
Health checks:
# S/4HANA — should return {"status":"ok","mockMode":false}
curl -s http://localhost:8080/health -H "X-Connector-Key: aisi-connector-dev-key"

# ECC — should return {"status":"ok","mockMode":false}
curl -s http://localhost:8081/health -H "X-Connector-Key: aisi-connector-dev-key"

# Quick status check (both containers running)
docker ps --filter name=sap-connector --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"

Rebuilding after code changes

docker stop sap-connector sap-connector-ecc
docker rm sap-connector sap-connector-ecc
cd connector && mvn clean package -DskipTests && docker build --platform linux/amd64 -t sap-connector .
# Then re-run the docker run commands above
CONNECTOR_API_KEY must match that connection’s connection_secrets.connector_key (seeded as aisi-connector-dev-key locally). By default the connector runs in real mode (connects to SAP via JCo). Only add -e SAP_MOCK_MODE=true if you explicitly need mock mode.