ARC-1 Log Analysis Guide¶
Enabling File Logging¶
Set the ARC1_LOG_FILE environment variable to enable JSON line audit logging:
# Local development
ARC1_LOG_FILE=/tmp/arc1-audit.jsonl npm run dev
# Docker
docker run -v /data/logs:/logs -e ARC1_LOG_FILE=/logs/arc1-audit.jsonl ghcr.io/arc-mcp/arc-1
# BTP Cloud Foundry (in manifest.yml)
env:
ARC1_LOG_FILE: /tmp/arc1-audit.jsonl
On BTP Cloud Foundry, ARC-1's stderr logs are always available via cf logs arc1-mcp-server
(live) and cf logs arc1-mcp-server --recent (buffer) β no service binding required. The
deprecated Application Logging Service (Kibana) is off by default (SAP Note 3557260); see
BTP Cloud Foundry Deployment to opt back in, or use SAP Cloud
Logging for a managed observability stack.
Log Levels¶
Control stderr verbosity with ARC1_LOG_LEVEL:
ARC1_LOG_LEVEL=debug # Show everything (HTTP requests, CSRF fetches)
ARC1_LOG_LEVEL=info # Default β tool calls, auth events
ARC1_LOG_LEVEL=warn # Only warnings and errors
ARC1_LOG_LEVEL=error # Only errors
The file sink always receives ALL events regardless of stderr level.
Event Types¶
| Event | Level | Description |
|---|---|---|
tool_call_start |
info | MCP tool call received |
tool_call_end |
info/error | Tool call completed (with status, duration, error details) |
http_request |
debug/warn | HTTP request to SAP ADT |
http_csrf_fetch |
debug | CSRF token fetch |
auth_scope_denied |
warn | Tool blocked by insufficient auth scope |
auth_pp_created |
info/error | Per-user ADT client created via principal propagation |
safety_blocked |
warn | Operation blocked by safety system |
server_start |
info | ARC-1 server started |
elicitation_sent |
info | Elicitation prompt sent to client |
elicitation_response |
info | User response to elicitation |
What a Healthy Startup Looks Like¶
After you deploy (or run locally), the startup transcript is the fastest way to confirm SAP
connectivity and authorization are working β before you ever make a tool call. On BTP Cloud Foundry,
read it with cf logs arc1-mcp-server --recent (or the Logs tab of the app in the BTP Cockpit).
A healthy startup at the default info level looks like this (real output, S/4HANA 2023 / ABAP
Platform 2025):
INFO: [server_start] {"version":"0.9.x","transport":"stdio","allowWrites":...,"url":"http://your-sap:50000"}
INFO: ARC-1 starting {"version":"0.9.x","transport":"...","url":"..."}
INFO: SAP semaphore {"maxConcurrent":10,"scope":"server-wide"}
INFO: Object cache enabled {"mode":"auto",...}
INFO: ARC-1 MCP server running on stdio # (or: "ARC-1 HTTP server started" on BTP)
INFO: Startup auth preflight succeeded for shared SAP credentials. {"endpoint":"/sap/bc/adt/core/discovery"}
INFO: Authorization probe: object search access is available
INFO: Authorization probe: transport access is available
The two green-light signals¶
INFO: Authorization probe: object search access is available
INFO: Authorization probe: transport access is available
These two lines mean your SAP authorizations are correct. If you see them, ARC-1 reached SAP,
authenticated, and the SAP user can search the repository and read transports β the foundation every
tool call builds on. (Under principal propagation the preflight is skipped β each user authenticates at
runtime β so you'll instead see Skipped startup auth preflight: principal propagation mode is enabled;
the per-user authorization probe then runs on that user's first call.)
If instead you see either of:
WARN: Authorization probe: object search access denied β <reason>
INFO: Authorization probe: transport access is not available β <reason>
β¦the SAP user is missing an authorization (not an ARC-1 bug). Search/read needs S_DEVELOP and
S_ADT_RES (read-only users need S_ADT_RES with ACTVT = 01 AND 02 β several ADT reads are POSTs).
See Authorization and Principal Propagation.
"Feature not available" is normal, not an error¶
ARC-1 probes optional capabilities at startup (abapGit, AMDP, RAP/CDS, UI5, HANA info, source search,
β¦). Any capability your system doesn't have simply returns 404 (not installed / ICF service not
active) or 400 β this is expected and is recorded as data, not an error. These probe misses are
logged at debug, so they do not appear at the default info level. A clean startup has no
WARN lines from probing.
If you run with ARC1_LOG_LEVEL=debug, you'll see them β and they're still harmless:
DEBUG: [http_request] {"method":"GET","path":"/sap/bc/adt/abapgit/repos","statusCode":404,...}
DEBUG: [http_request] {"method":"GET","path":"/sap/bc/adt/debugger/amdp","statusCode":404,...}
DEBUG: [http_request] {"method":"GET","path":"/sap/bc/adt/ddic/ddl/sources","statusCode":400,...}
These just mean abapGit/AMDP aren't installed and the RAP probe returned its expected 400 β ARC-1
disables those features gracefully and serves the rest. The resolved feature set is what matters, not
the individual probe responses.
A genuine problem looks different: a
WARN/errorauth_scope_denied, a401on the auth preflight, anAuthorization probe: β¦ deniedline, orStartup auth preflight failedβ those are worth investigating; a404probe miss atdebugis not.
OAuth scope errors on the MCP client (not SAP)¶
A different failure class: the MCP client (Claude, Copilot, β¦) can't complete OAuth and reports an
invalid_scope / scope error even though your user has the right role collection. This is almost always
a stale cache, not a missing authorization:
- Log out of the MCP client's OAuth session and reconnect β or use a fresh/incognito browser window for the consent step. A previous deployment's XSUAA/DCR client registration is often cached.
- Verify the role collection is assigned under the correct identity provider. If your subaccount
uses a custom IdP (e.g. SAP IAS), assign the role collection to the user under that IdP
(
--of-idp <your-idp>), not the default SAP ID service β otherwise the JWT carries no ARC-1 scopes. - After a redeploy that recreated the XSUAA service, give the client one clean re-login; cached
client_ids from the old service instance produce scope errors until they re-register.
Analyzing Logs with jq¶
Recent Errors¶
# All errors in the last hour
jq 'select(.level == "error")' arc1-audit.jsonl
# Failed tool calls with error details
jq 'select(.event == "tool_call_end" and .status == "error")' arc1-audit.jsonl
# Failed tool calls grouped by error class
jq -s '[.[] | select(.event == "tool_call_end" and .status == "error")] | group_by(.errorClass) | map({errorClass: .[0].errorClass, count: length})' arc1-audit.jsonl
Bad/Wrong Tool Calls (for improving LLM feedback)¶
# Tool calls with unknown types (LLM sent wrong type parameter)
jq 'select(.event == "tool_call_end" and .status == "error" and (.errorMessage | contains("Unknown")))' arc1-audit.jsonl
# Tool calls blocked by safety (LLM tried a blocked operation)
jq 'select(.event == "tool_call_end" and .errorClass == "AdtSafetyError")' arc1-audit.jsonl
# Auth scope denials (LLM called a tool the user can't access)
jq 'select(.event == "auth_scope_denied")' arc1-audit.jsonl
# All error messages β useful to find patterns in LLM mistakes
jq -s '[.[] | select(.event == "tool_call_end" and .status == "error") | .errorMessage] | group_by(.) | map({message: .[0], count: length}) | sort_by(-.count)' arc1-audit.jsonl
Slow Operations¶
# Tool calls taking >5 seconds
jq 'select(.event == "tool_call_end" and .durationMs > 5000)' arc1-audit.jsonl
# HTTP requests taking >10 seconds
jq 'select(.event == "http_request" and .durationMs > 10000)' arc1-audit.jsonl
# Average duration by tool
jq -s '[.[] | select(.event == "tool_call_end")] | group_by(.tool) | map({tool: .[0].tool, avgMs: (map(.durationMs) | add / length | round), count: length})' arc1-audit.jsonl
Correlating Events by Request ID¶
Every tool call generates a unique requestId (e.g., REQ-42). All HTTP requests made during that tool call share the same ID:
# Trace a specific tool call through all its HTTP requests
jq 'select(.requestId == "REQ-42")' arc1-audit.jsonl
# Find tool calls that made many HTTP requests (potential performance issue)
jq -s '[.[] | select(.event == "http_request")] | group_by(.requestId) | map({requestId: .[0].requestId, httpCalls: length}) | sort_by(-.httpCalls) | .[:10]' arc1-audit.jsonl
HTTP-Level Analysis¶
# Failed HTTP requests (4xx/5xx)
jq 'select(.event == "http_request" and .statusCode >= 400)' arc1-audit.jsonl
# HTTP requests with error bodies (SAP error messages)
jq 'select(.event == "http_request" and .errorBody != null)' arc1-audit.jsonl
# Most common ADT paths called
jq -s '[.[] | select(.event == "http_request") | .path] | group_by(.) | map({path: .[0], count: length}) | sort_by(-.count) | .[:10]' arc1-audit.jsonl
User Activity¶
# Tool calls per user
jq -s '[.[] | select(.event == "tool_call_start" and .user != null)] | group_by(.user) | map({user: .[0].user, calls: length})' arc1-audit.jsonl
# What tools a specific user called
jq 'select(.event == "tool_call_start" and .user == "john.doe@company.com")' arc1-audit.jsonl
BTP Audit Log Service¶
When deployed on BTP with the Audit Log Service premium plan bound, ARC-1 automatically sends audit events to the BTP Audit Log Viewer. Events are categorized as:
- security-events: auth failures, scope denials, safety blocks
- data-accesses: tool calls that read SAP data (SAPRead, SAPSearch, SAPQuery)
- data-modifications: tool calls that write data (SAPWrite, SAPManage)
- configuration-changes: transport and activation operations (SAPTransport, SAPActivate)
View these in the BTP cockpit under Instances and Subscriptions > Audit Log Viewer.
Docker Volume Mount Example¶
# Run with persistent log file
docker run -d \
-v /data/arc1-logs:/logs \
-e ARC1_LOG_FILE=/logs/audit.jsonl \
-e SAP_URL=http://sap:50000 \
-e SAP_USER=admin \
-e SAP_PASSWORD=secret \
ghcr.io/arc-mcp/arc-1
# Tail logs in real-time
tail -f /data/arc1-logs/audit.jsonl | jq .
# Watch for errors only
tail -f /data/arc1-logs/audit.jsonl | jq 'select(.level == "error")'