ARC-1 Deployment Best Practices¶
One Instance Per SAP System¶
ARC-1 follows the one instance per SAP backend pattern by default. This is the recommended model for writes and for hard security/capacity isolation, and matches Eclipse ADT, SAP Business Application Studio, and SAP GUI. A separate experimental multi-target mode offers reads across BTP subaccount destinations and opt-in scoped pinned PP writes when application sprawl is the larger operational risk. Separate instances retain stronger operational isolation.
Why one-per-system?¶
| Concern | One-per-system | Multi-backend gateway |
|---|---|---|
| Security | Blast radius = one system | One breach = all systems |
| Auth | Clean: one auth flow per instance | N destinations + N auth flows |
| Safety gates | Per-system: allowWrites, allowedPackages, denyActions |
Scoped pinned PP writes require exact user grants; destination restrictions narrow the instance ceiling |
| Tool descriptions | Tailored to system type (BTP vs on-premise) | Must be generic for all |
| Audit trail | Clear per-system logs | Mixed across systems |
| Scaling | Scale independently | Heavy-use system affects all |
Multi-user within each instance¶
Each ARC-1 instance serves multiple users via principal propagation (on-premise) or a per-user BTP Destination token exchange (BTP ABAP). The MCP client authenticates the user, and ARC-1 maps that to a SAP user identity.
┌─────────────────┐
│ MCP Client │
│ (Claude, etc.) │
└──┬──────────┬───┘
│ │
▼ ▼
┌─────────────────────┐ ┌──────────────────────┐
│ arc1-ecc-dev │ │ arc1-btp-dev │
│ on-premise, PP │ │ BTP ABAP, OAuth2 UTE │
│ allowWrites=true │ │ allowWrites=true │
│ 50 developers │ │ 50 developers │
└──────┬──────────────┘ └──────┬───────────────┘
▼ ▼
┌──────────────┐ ┌──────────────────┐
│ SAP ECC Dev │ │ BTP ABAP Env │
└──────────────┘ └──────────────────┘
Scaling out: what changes at more than one instance¶
ARC-1 is stateless in the sense that matters for load balancing — any instance can serve any
request, no sticky sessions, no session store (the MCP HTTP transport runs in stateless mode, and
OAuth client_ids are HMAC-derived rather than stored). The tracked mta.yaml pins instances: 1;
raising it is safe, but four things are per-process, so they divide (or break) when you do:
| Per-process state | Effect at N instances | What to do |
|---|---|---|
ARC1_RATE_LIMIT (Layer 2, per-user) and ARC1_MCP_HTTP_RATE_LIMIT (per-IP) |
Each instance counts independently → the effective limit is N× the configured value | Divide the configured value by N, or enforce the real ceiling at the router/API gateway |
ARC1_MAX_CONCURRENT (Layer 3 SAP semaphore) |
Same — total concurrent SAP requests is N× the value | Size N × ARC1_MAX_CONCURRENT against rdisp/wp_no_dia, not the single-instance number |
Feature probe + source/ETag cache (ARC1_CACHE=auto → in-memory) |
Cold per instance; a user's cache hit rate drops roughly 1/N, and each instance runs its own startup probe | Accept per-instance caches. SQLite persistence is instance-local; a shared filesystem is not a distributed cache |
| ADR-0007 shared-Basic multi-target guard | Hard requirement: exactly one instance. The generation/lockout guard that prevents a shared technical user from being locked out is process-local | Do not scale out. This mode is single-instance by contract |
Scale up (memory/CPU per instance) before scaling out unless you have measured that one instance is the bottleneck — a single ARC-1 instance is almost always waiting on SAP, not on itself.
What to alert on¶
ARC-1 emits structured audit events to stderr (and optionally a file or the BTP Audit Log); it does not alert. Wire these into your APM/log platform:
| Signal | Event / field | Why |
|---|---|---|
| Error-rate spike | tool_call_end with status: "error", grouped by errorClass |
A backend or authorization regression shows up here before users report it |
| Latency degradation | tool_call_end.durationMs p95, and http_request.durationMs for the SAP leg |
Separates "ARC-1 is slow" from "SAP is slow" |
| Authentication failures | auth_pp_created with success: false, pp_exchange_failed, sap_authentication_failed |
A broken destination/trust config fails closed and silently until someone looks |
| Authorization denials | auth_scope_denied, safety_blocked, target_policy_denied |
A burst means a misconfigured client or a model probing beyond its scope |
| Rate-limit saturation | mcp_rate_limited |
A runaway agent loop; also the signal that your limit is too tight |
| Attribution | clientId (registered OAuth client), clientAgent (client software), user on every event |
Answers "which agent, acting for whom" — see Trace context and agent attribution |
Trace context and agent attribution¶
These capabilities support SAP's guidance for third-party MCP access. See the control assessment for requirements that remain partial or deployment-dependent:
- W3C trace context passes through. A valid
traceparent(and itstracestate) on an inbound MCP request is forwarded verbatim on every outbound SAP call, so one trace spans agent → ARC-1 → SAP. ARC-1 runs no tracer of its own, so per the W3C spec it behaves as a non-participating pass-through: it never rewritesparent-idand never originates a trace when the client sent none. Malformed values are dropped, not repaired. ARC-1's ownrequestIdcorrelates its logs either way. No configuration. clientAgentrecords the calling agent. On stdio it is the MCP handshakeclientInfo(name/version); over HTTP the transport is stateless, so it is theUser-Agent— best-effort, caller-controlled, and never an authorization input. It sits next toclientId(the registered OAuth client) on every audit event and in the BTP Audit Log.- Registered agent identity remains a gap. ARC-1's supported principal-propagation flow obtains
a per-user SAP credential through a destination-dependent exchange.
clientAgentis only an ARC-1 audit label; it does not establish the registered Cloud Identity Services agent identity SAP's architecture requires in the downstream credential. Tool scopes also do not prove that the SAP credential is narrowed separately for each tool. Validate the tenant-supported identity flow; correlate current user/client audit records throughrequestId/traceparent.
Example: enterprise with multiple SAP systems¶
Use one mta.yaml with different .mtaext files per landscape. The .gitignore matches any mta-*.mtaext, so per-landscape extension files (mta-ecc-dev.mtaext, mta-ecc-prod.mtaext, …) stay local — only the mta-overrides.mtaext.example template is tracked. Copy it once per landscape:
cp mta-overrides.mtaext.example mta-ecc-dev.mtaext # edit: writes enabled
cp mta-overrides.mtaext.example mta-ecc-prod.mtaext # edit: read-only
# Build once
mbt build
# Target the DEV CF space first; reusing the same MTA ID in one space updates the existing app
# Deploy to dev — writes enabled
cf deploy mta_archives/arc1-mcp_*.mtar -e mta-ecc-dev.mtaext
# Target the separate PROD CF space before this command
# Deploy to prod — read-only
cf deploy mta_archives/arc1-mcp_*.mtar -e mta-ecc-prod.mtaext
Route URL: pin a
host:in each.mtaextso the app gets the short, predictable URL its MCP clients connect to (arc1-ecc-dev→https://arc1-ecc-dev.cfapps.<region>.hana.ondemand.com/mcp). Without a pinned host the deploy service assigns a long, globally-unique auto-route that you only learn after deploy viacf app <name>. The host must be free across the whole sharedcfapps.<region>.hana.ondemand.comdomain (unique per region, not per subaccount), so use landscape-specific names. See the "Route host" block inmta-overrides.mtaext.example.
CF Apps:
┌──────────────────────────────────┐
│ arc1-ecc-dev │ ECC Dev, read+write, PP
│ allowWrites=true │
├──────────────────────────────────┤
│ arc1-ecc-prod │ ECC Prod, read-only, PP
│ allowWrites=false, allowFreeSQL=false │
├──────────────────────────────────┤
│ arc1-s4-dev │ S/4 Dev, read+write, PP
│ allowWrites=true │
├──────────────────────────────────┤
│ arc1-btp-dev │ BTP ABAP, read+write, OAuth2UserTokenExchange
│ SAP_SYSTEM_TYPE=btp │
│ allowWrites=true │
└──────────────────────────────────┘
MCP client config for developers:
{
"mcpServers": {
"sap-ecc-dev": {
"type": "http",
"url": "https://arc1-ecc-dev.cfapps.us10.hana.ondemand.com/mcp"
},
"sap-ecc-prod": {
"type": "http",
"url": "https://arc1-ecc-prod.cfapps.us10.hana.ondemand.com/mcp"
},
"sap-s4-dev": {
"type": "http",
"url": "https://arc1-s4-dev.cfapps.us10.hana.ondemand.com/mcp"
},
"sap-btp": {
"type": "http",
"url": "https://arc1-btp-dev.cfapps.us10.hana.ondemand.com/mcp"
}
}
}
The LLM sees separate tool sets from each server. Identify the intended target explicitly; a model can choose the wrong connection.
Identify each direct-connect instance: every ARC-1 advertises the server name
arc-1in the MCPinitializehandshake by default. Clients such as VS Code derive tool prefixes from that announced name and add numeric suffixes when several servers announce the same value. Set a uniqueARC1_SERVER_NAMEper instance (arc1-ecc-dev,arc1-ecc-prod, …) in each.mtaextso the tool prefix identifies the target system. Some remote-connector clients replace that name with an opaque ID; for those, also setARC1_SYSTEM_LABEL(for exampleERP production (read-only)) so the same identity is visible in the model-facing server instructions. This is the direct-connect alternative to native multi-target mode, which exposes pinned and aggregate routes from one ARC-1 deployment and ignores the single-target label.
System Type Detection¶
ARC-1 auto-detects whether it's connected to a BTP ABAP Environment or an on-premise system.
How it works¶
On first SAPManage probe, ARC-1 reads /sap/bc/adt/system/components (already called for ABAP release detection — zero extra HTTP requests). If the SAP_CLOUD component is present, the system is BTP. Otherwise, on-premise.
Manual override¶
For immediate correct tool definitions at startup (before the first probe), set:
# Environment variable
SAP_SYSTEM_TYPE=btp # or: onprem, auto (default)
# CLI flag
--system-type btp
When SAP_SYSTEM_TYPE=btp is set, tool definitions are adapted at server startup:
- SAPRead removes PROG, INCL, VIEW, TRAN, TEXT_ELEMENTS, VARIANTS, SOBJ, AUTH, FEATURE_TOGGLE/FTG2, ENHO, VERSIONS, and VERSION_SOURCE from the type enum
- SAPWrite removes PROG, INCL, FUNC, and FUGR from the type enum
- SAPQuery description warns about blocked SAP standard tables
- SAPTransport description explains gCTS behavior
- SAPContext removes PROG and FUNC from the type enum
What changes on BTP¶
| Tool | What changes |
|---|---|
| SAPRead | Keeps cloud-facing types: CLAS, INTF, FUNC/FUGR where released/custom, DDLS, DCLS, DDLX, BDEF, SRVD, SRVB, SKTD, TABL, DOMA, DTEL, TABLE_CONTENTS, TABLE_QUERY, DEVC, SYSTEM, COMPONENTS, MSAG, BSP/BSP_DEPLOY, API_STATE, INACTIVE_OBJECTS. Removes classic-only types and returns a helpful error if the LLM tries them anyway. |
| SAPWrite | Supports CLAS, INTF, DDLS, DCLS, DDLX, BDEF, SRVD, SRVB, SKTD, TABL/TABL/DT/TABL/DS, DOMA, DTEL, MSAG. Must use ABAP Cloud syntax and custom namespaces. |
| SAPQuery | Warns that SAP standard tables (DD02L, TADIR, etc.) are blocked. Suggests CDS views. |
| SAPSearch | Notes that only released and custom objects are returned. |
| SAPTransport | Explains gCTS: release = Git push, not TMS export. |
| SAPContext | Supports CLAS, INTF, and DDLS. CDS impact analysis is available for DDLS. |
| SAPManage | Returns systemType in probe results. |
| SAPActivate | No change. |
| SAPNavigate | Notes released object scope. |
| SAPLint | No change. |
| SAPDiagnose | No change. |
Authentication Options¶
Local development¶
| Target | Auth | Config |
|---|---|---|
| On-premise SAP | Basic Auth | SAP_URL, SAP_USER, SAP_PASSWORD |
| BTP ABAP Environment | Service Key + Browser OAuth | SAP_BTP_SERVICE_KEY_FILE |
Deployed on BTP Cloud Foundry¶
| Target | Auth | Config |
|---|---|---|
| On-premise SAP (via Cloud Connector) | Principal Propagation | SAP_BTP_DESTINATION, SAP_PP_ENABLED=true, SAP_PP_STRICT=true |
| BTP ABAP Environment | Destination OAuth2UserTokenExchange |
SAP_BTP_DESTINATION, SAP_PP_ENABLED=true, SAP_PP_STRICT=true, SAP_SYSTEM_TYPE=btp |
Configuration examples¶
Local dev connecting to on-premise:
{
"mcpServers": {
"sap": {
"command": "arc1",
"env": {
"SAP_URL": "http://sap-dev:50000",
"SAP_USER": "DEVELOPER",
"SAP_PASSWORD": "..."
}
}
}
}
Local dev connecting to BTP ABAP:
{
"mcpServers": {
"sap-btp": {
"command": "arc1",
"env": {
"SAP_BTP_SERVICE_KEY_FILE": "~/.config/arc-1/btp-service-key.json",
"SAP_SYSTEM_TYPE": "btp"
}
}
}
}
Deployed on CF connecting to on-premise (multi-user):
# manifest.yml
applications:
- name: arc1-ecc-dev
env:
SAP_BTP_DESTINATION: SAP_ECC_DEV
SAP_PP_ENABLED: true
SAP_PP_STRICT: true
SAP_TRANSPORT: http-streamable
SAP_XSUAA_AUTH: true
Security Recommendations¶
- Use
SAP_ALLOW_WRITES=falsefor production systems — prevents object, transport, and Git mutations - Use
SAP_ALLOW_FREE_SQL=falsefor sensitive systems — blocks arbitrary SQL queries - Use
SAP_ALLOWED_PACKAGES=Z*,Y*,$TMP— restricts write operations to custom code packages (default is$TMPonly — local objects) - Choose the PP/API-key identity topology explicitly — separate strict PP and least-privileged API-key instances are recommended; supported mixed instances set
SAP_PP_STRICT=false - Deploy separate instances per system — limits blast radius
- Use XSUAA auth for deployed instances — proper OAuth 2.0 with scopes (read/write/data/sql/transports/git/admin)
- Set
SAP_SYSTEM_TYPEexplicitly in production — ensures correct tool definitions from startup - Keep
SAP_INSECURE=falseon CA-signed landscapes — the trackedmta.yaml/manifest.ymlship"false"; only set it"true"in isolated development when you deliberately accept all SAP certificates - Set
ARC1_RATE_LIMIT(e.g.60) on multi-user instances — the per-user MCP quota is off by default, so one runaway agent loop can saturate the shared SAP request semaphore - Set a stable
ARC1_DCR_SIGNING_SECRETon XSUAA OAuth instances — otherwise the DCR signing key derives from the XSUAAclientsecret, so everycf deploythat recreates the service binding rotates it and invalidates all cached MCPclient_ids. Users then hitinvalid_clientafter each redeploy, and some clients (Eclipse Copilot, Cursor) can't recover without manual cache surgery. Set it once withcf set-env arc1-mcp-server ARC1_DCR_SIGNING_SECRET "$(openssl rand -base64 48)"; see Stable DCR signing key
Why the package allowlist matters
ARC-1 feeds SAP-resident content (source, comments, errors) to the LLM, which then issues tool calls under the user's identity. SAP_ALLOWED_PACKAGES is the backstop that contains a prompt-injected model writing outside its scope — prefer a DEVCLASS subtree (ZTEAM/**) over * so the containment survives even a steered model.
Security Hardening¶
For a comprehensive security hardening checklist covering TLS, header validation, token handling, and production lockdown, see the Security Guide.
If you deploy ARC-1 behind a reverse proxy (nginx, Envoy, etc.) outside of Cloud Foundry, ensure the proxy strips or sanitizes inbound X-Forwarded-* and Forwarded headers before forwarding to ARC-1. Unsanitized forwarded headers can lead to SSRF or authentication bypass if ARC-1 or downstream services trust them for request routing.
Key Files Reference¶
| File | Purpose | Customize? |
|---|---|---|
mta.yaml |
MTA build descriptor — services, conservative SAP_ALLOW_* defaults, and no active/fake destination. Tracked. Ships SAP_INSECURE: "false"; prefer NODE_EXTRA_CA_CERTS for internal CAs over disabling verification. |
Rarely — use .mtaext for overrides |
mta-overrides.mtaext.example |
Tracked template documenting every overridable property. | No — copy it to mta-overrides.mtaext (gitignored) and edit that |
mta-overrides.mtaext (or any mta-*.mtaext) |
Per-landscape MTA extension (real destinations, safety flags). Gitignored. | Yes — uncomment and set values for your environment |
manifest.yml |
CF deployment manifest (on-premise via Cloud Connector) | Yes — change SAP_URL, destination name, safety flags |
manifest-btp-abap.yml |
CF deployment manifest (BTP ABAP via per-user destination) | Yes — set the destination name and safety flags; do not mount the ABAP service key into ARC-1 |
Dockerfile |
Multi-stage Alpine build, all env vars documented | Rarely — use env vars for config |
.env.example |
Template for local .env file |
Yes — copy to .env and fill in |
xs-security.json |
XSUAA scopes, roles, and localhost development callback | Manual deployments: copy it to a landscape-specific file and register its exact public /oauth/callback and /oauth/logged-out URLs. MTA injects these for the default route; custom routes need an override. MCP client callbacks are enforced separately at runtime/DCR. |
bin/arc1.js |
npm global CLI entry point | No |
Deploying Without Docker¶
If the Docker image doesn't fit your needs (custom certs, patching, compliance), deploy as a Node.js app using CF's nodejs_buildpack. See BTP CF Deployment for the full guide.
Quick summary:
1. git clone + npm ci + npm run build
2. Create a manifest-nodejs.yml with buildpacks: [nodejs_buildpack] and command: node dist/index.js
3. cf push -f manifest-nodejs.yml
4. Set secrets via cf set-env
BTP ABAP Environment Setup¶
See BTP ABAP Environment guide for:
- Configuring deployed ARC-1 with OAuth2UserTokenExchange (recommended)
- Configuring ARC-1 locally with service-key browser OAuth
- System type detection and tool adaptation
See BTP ABAP Prerequisites for the SAP-side groundwork:
- Provisioning the BTP ABAP instance
- Running the "Prepare an Account for ABAP Development" booster and assigning SAP_BR_DEVELOPER
- Creating a service key for local OAuth or destination credentials
See BTP CF Deployment for: - Cloud Foundry deployment with Docker - Destination Service and Cloud Connector setup - Principal Propagation configuration - Deploying without Docker (Node.js buildpack)