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 --env-file .env -v /data/logs:/logs -e ARC1_LOG_FILE=/logs/arc1-audit.jsonl ghcr.io/arc-mcp/arc-1
# .env must include SAP connection settings and HTTP authentication, e.g. ARC1_API_KEYS
# 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¶
Set ARC1_LOG_LEVEL (or --log-level) to debug, info, warn or error to select
the minimum level printed by the logger on stderr. The default is info;
SAP_VERBOSE=true / --verbose forces debug even when another level is set.
Debug includes HTTP requests and CSRF probes. Set the option on the ARC-1 process
and restart it; on CF, use an .mtaext property for a durable setting.
At warn or error, INFO tool-call audit events are hidden on stderr. Keep info
if stderr is your only audit destination, or configure a separate audit sink.
File audit output is independent of the stderr level; it records emitted audit events,
not ordinary log messages. E2E runs retain it as mcp-audit.ndjson beside mcp-server.log
in the uploaded log directory. CSRF events describe
each probe: a HEAD 400 followed by GET 200 with a usable token is a healthy fallback.
Event Types¶
The exact event inventory and field contract is maintained in Security Guide: What Gets Logged. Important operator groups include:
| Group | Events |
|---|---|
| Tool and SAP HTTP lifecycle | tool_call_start, tool_call_end, http_request, http_csrf_fetch |
| Authorization and safety | auth_scope_denied, safety_blocked, data_response_limited, auth_rate_limited, mcp_rate_limited |
| Selected identity | auth_pp_created, auth_shared_created |
| Multi-target failure stage | target_resolution_failed, pp_exchange_failed, shared_auth_failed, cloud_connector_access_denied, sap_service_unavailable, sap_authentication_failed, sap_authorization_failed, target_policy_denied |
| Server/client protocol | server_start, OAuth/DCR, and CORS events |
In the opt-in target-authorization mode, grant denials include targetAccessMode, grantMode,
the stable errorCode, and exactGrantCount for exact grants. These are bounded operator
diagnostics, not the raw grant list. targetAccessMode is deliberately distinct from an
Authorization header; central credential redaction remains unchanged for every sink.
Within a selected multi-target call, use requestId to correlate events and target, destination,
and identity to identify the selected route and identity model. Failure-stage events also carry a
safe errorCode; they do not contain destination credentials or SAP response bodies.
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 authentication/CSRF bootstrap succeeded; each tool still checks authorization. {"endpoint":"/sap/bc/adt/core/discovery"}
INFO: Authorization probe: object search access is available
INFO: Authorization probe: transport access is available
An empty HTTP 200 discovery response does not count as a successful bootstrap. Older backends
may use /sap/bc/adt/discovery; the log records the endpoint that supplied the token.
If neither path supplies a token, startup reports an inconclusive result and keeps GET reads available.
The two green-light signals¶
INFO: Authorization probe: object search access is available
INFO: Authorization probe: transport access is available
These two lines confirm only the search and transport probes. 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>
…investigate authorization, endpoint availability, and the reported status. A probe failure alone does not rule out an ARC-1 or connectivity regression. Search/read needs S_DEVELOP and
S_ADT_RES. The latter checks allowed URI prefixes and has no ACTVT field. A read
implemented as HTTP POST does not imply create/change authorization; trace the
backend checks on S_DEVELOP and other objects separately. See the
SAP authorization guidance.
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 SAP_VERBOSE=true, 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. Check the requested scopes, current XSUAA configuration, role assignments, and then cached client state:
- 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 error-level audit events in this file (no time filter)
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
Multi-Target Failures¶
# Every selected-target failure stage
jq 'select(.event as $e | ["target_resolution_failed", "pp_exchange_failed", "shared_auth_failed", "cloud_connector_access_denied", "sap_service_unavailable", "sap_authentication_failed", "sap_authorization_failed", "target_policy_denied"] | index($e))' arc1-audit.jsonl
# One public target across identity, policy, and SAP failure stages
jq 'select(.target == "A4H/100" and .errorCode? != null)' arc1-audit.jsonl
# Failure counts by safe error code
jq -s '[.[] | select(.errorCode? != null) | .errorCode] | group_by(.) | map({errorCode: .[0], count: length}) | sort_by(-.count)' arc1-audit.jsonl
# Successful shared-identity canaries, grouped by destination
jq -s '[.[] | select(.event == "auth_shared_created")] | group_by(.destination) | map({destination: .[0].destination, successes: length})' arc1-audit.jsonl
target is the public SID/client or alias/client ID, while destination is the internal BTP
destination name. identity distinguishes per-user Principal Propagation from a shared Basic
technical user. Correlate the failure-stage event with the same requestId's tool_call_end; do not
expect raw SAP response bodies in these events.
Bad/Wrong Tool Calls (for improving LLM feedback)¶
# Tool calls that returned client-visible handler errors (unknown tool/action, validation, etc.)
jq 'select(.event == "tool_call_end" and .status == "error" and .errorClass == "result-path")' 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
# Error counts by class — errorMessage content is redacted before sink writes
jq -s '[.[] | select(.event == "tool_call_end" and .status == "error") | .errorClass] | group_by(.) | map({errorClass: .[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
# Non-authentication failures for which a redacted error-body placeholder was retained
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
The errorBody query deliberately excludes SAP HTTP 401 and 403 responses. Those bodies can expose
technical usernames, echoed login material, or SAP security details, so ARC-1 omits them even when
ARC1_LOG_HTTP_DEBUG=true. Use the status code, selected-target failure event, errorCode, and
requestId instead. Other error bodies are replaced with a length-only redacted placeholder before
sink writes; their content is never present in the audit file.
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 forwards categorized security and tool-call events to the BTP Audit Log Viewer. Low-level HTTP, startup, and elicitation events remain in stderr/file logs. Forwarded events are categorized as:
- security-events: auth/target/service failures, scope denials, safety blocks, shared-identity use
- 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. For
programmatic verification or scheduled reviews, read the same records through the Audit Log Retrieval
API; see Audit Log delivery evidence. Tool-call
records carry object.type = "MCP Tool Call" and, on invocation, redacted arguments in args (first 500 characters plus ...
when truncated). Completion records carry the outcome.
Docker Volume Mount Example¶
# Generate ARC1_LOCAL_KEY with `openssl rand -hex 32` before running.
# Run with persistent log file; the mounted directory must be writable by the container user.
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 \
-e ARC1_API_KEYS="$ARC1_LOCAL_KEY:admin" \
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")'