Skip to main content

MCP Gateway Log Export API

Use the Log Export API to read MCP Gateway tool call logs from your own data platform, SIEM, warehouse, or scheduled export job.

The API returns newline-delimited JSON. Each response line is one complete JSON object, so clients can stream, parse, and checkpoint logs incrementally.

GET https://<your-gateway-host>/mcpgateway/logs/export

Response content type:

Content-Type: application/x-ndjson

Authentication​

Pass a log export key from MCP Gateway:

X-Quilr-Log-Export-Key: sk-export-...

Do not use your MCP API token or admin API key as the request credential for this endpoint. The log export key is separate from tool-call authentication and management API authentication.

MCP Gateway exposes two export scopes:

Export keyScope
log_export_keyExports logs only for the MCP backend it belongs to.
tenant_log_export_keyExports logs for all MCP backends in the tenant.

Both scopes use the same endpoint, header, query parameters, pagination model, and response format. In tenant-scoped exports, each mcpgateway.tool_call event still includes the concrete backend in backend.id and backend.name.

Admin backend management responses include log_export_key for each backend and tenant_log_export_key for the tenant. Treat both as bearer secrets. They cannot call MCP tools, but they can read exportable tool call logs for their scope.

Query Parameters​

All query parameters are optional.

ParameterDescription
start_timeISO 8601 lower bound for exported logs. Naive timestamps are treated as UTC.
end_timeISO 8601 upper bound for exported logs. Naive timestamps are treated as UTC.
cursorOpaque cursor from the previous checkpoint.next_cursor. When provided, it wins over start_time.
limitMaximum tool call rows to export in this response. Default 1000, maximum 5000.

Logs are available for a maximum of 15 days. Choose start_time within that retention window when backfilling. Requests with an effective start_time, end_time, or cursor timestamp before the retention window fail with 400.

If neither start_time nor cursor is provided, the API exports a default 24-hour window ending at the effective export end time.

Export Lag​

The API does not export logs newer than 15 minutes. Gateway logs and prediction payloads are written asynchronously, so this lag keeps exported rows stable.

If end_time is newer than now - 15 minutes, the server clamps it to the maximum exportable time. The request still succeeds. The export_started and checkpoint events include the effective export bounds.

Request Examples​

Start a backend-scoped export window, here the hour that ended two hours ago (logs are kept for 15 days, so fixed dates go stale):

# macOS (BSD date) first, GNU date as fallback
START=$(date -u -v-3H +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -d '3 hours ago' +%Y-%m-%dT%H:%M:%SZ)
END=$(date -u -v-2H +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -d '2 hours ago' +%Y-%m-%dT%H:%M:%SZ)

curl -N \
-H "X-Quilr-Log-Export-Key: sk-export-..." \
"https://<your-gateway-host>/mcpgateway/logs/export?start_time=$START&end_time=$END&limit=1000"

Start a tenant-scoped export window:

curl -N \
-H "X-Quilr-Log-Export-Key: sk-exportl-..." \
"https://<your-gateway-host>/mcpgateway/logs/export?start_time=$START&end_time=$END&limit=1000"

Resume from the previous checkpoint:

curl -N \
-H "X-Quilr-Log-Export-Key: sk-export-..." \
"https://<your-gateway-host>/mcpgateway/logs/export?cursor=<next_cursor>"

When resuming with cursor, you do not need to pass start_time or end_time.

Pagination​

Rows are ordered by:

started_at ASC, id ASC

The cursor is opaque. Store it exactly as returned in checkpoint.next_cursor and send it back as the cursor query parameter on the next request.

If checkpoint.has_more is true, call the endpoint again immediately with cursor=<next_cursor>.

If checkpoint.has_more is false, there are no more rows in the current effective window. Store next_cursor and poll later with that cursor to continue incremental export.

When an initial request returns zero rows, the API still returns a checkpoint cursor pinned to the effective end time. This lets exporters store one cursor value even for empty windows.

A reliable collector​

A minimal Python collector (standard library only) that you can run on a schedule or as a service. It:

  • stores rows and the cursor in one SQLite transaction, so the cursor only advances after the page is safely written;
  • writes idempotently (INSERT OR IGNORE on a unique id), so a replayed page never creates duplicates;
  • treats a mid-stream error event or a stream without a checkpoint as a failed page, and retries it with exponential backoff and jitter;
  • stops on 400, 401 and 403, which retrying cannot fix.
collector.py
"""Minimal MCP Gateway log collector: cursor checkpoint, retries, idempotent writes."""
import json, os, random, sqlite3, time, urllib.error, urllib.parse, urllib.request
from datetime import datetime, timedelta, timezone

URL = os.environ["EXPORT_URL"] # https://<your-gateway-host>/mcpgateway/logs/export
KEY = os.environ["QUILR_LOG_EXPORT_KEY"]
EVENT = "mcpgateway.tool_call"
row_id = lambda e: str(e["request"]["log_id"]) # unique per tool call log row

db = sqlite3.connect("quilr_logs.db")
db.execute("CREATE TABLE IF NOT EXISTS events (id TEXT PRIMARY KEY, ts TEXT, body TEXT)")
db.execute("CREATE TABLE IF NOT EXISTS state (k TEXT PRIMARY KEY, v TEXT)")

def saved_cursor():
row = db.execute("SELECT v FROM state WHERE k = 'cursor'").fetchone()
return row[0] if row else None

def fetch_page(params):
"""Return (events, checkpoint) for one complete page, or raise."""
req = urllib.request.Request(URL + "?" + urllib.parse.urlencode(params),
headers={"X-Quilr-Log-Export-Key": KEY})
events, checkpoint = [], None
with urllib.request.urlopen(req, timeout=300) as r: # raises HTTPError on 4xx/5xx
for line in r:
if not line.strip():
continue
msg = json.loads(line)
if msg["type"] == "error": # can arrive after HTTP 200
raise RuntimeError(f"export error: {msg['error']}")
if msg["type"] == EVENT:
events.append(msg)
elif msg["type"] == "checkpoint":
checkpoint = msg
if checkpoint is None:
raise RuntimeError("stream ended without a checkpoint")
return events, checkpoint

def with_retries(fn, *args, attempts=6):
for n in range(attempts):
try:
return fn(*args)
except urllib.error.HTTPError as e:
if e.code in (400, 401, 403) or n == attempts - 1:
raise # fix the key or the window; retrying will not help
except (OSError, RuntimeError, ValueError): # network, mid-stream error, truncation
if n == attempts - 1:
raise
time.sleep(min(60, 2 ** n) + random.random()) # backoff with jitter

def run_once():
cursor = saved_cursor()
if cursor:
params = {"cursor": cursor, "limit": 1000}
else: # first run: start one hour back, well inside the 15-day retention
start = datetime.now(timezone.utc) - timedelta(hours=1)
params = {"start_time": start.strftime("%Y-%m-%dT%H:%M:%SZ"), "limit": 1000}
while True:
events, checkpoint = with_retries(fetch_page, params)
with db: # one transaction: rows and cursor commit together
db.executemany(
"INSERT OR IGNORE INTO events VALUES (?, ?, ?)",
[(row_id(e), e["request"]["started_at"], json.dumps(e)) for e in events])
db.execute("INSERT OR REPLACE INTO state VALUES ('cursor', ?)",
(checkpoint["next_cursor"],))
print(f"stored {checkpoint['rows']} rows up to {checkpoint['effective_end_time']}")
if not checkpoint["has_more"]:
return
params = {"cursor": checkpoint["next_cursor"], "limit": 1000}

if __name__ == "__main__":
while True:
run_once()
time.sleep(300) # new rows appear about 15 minutes after the request
export EXPORT_URL='https://<your-gateway-host>/mcpgateway/logs/export'
export QUILR_LOG_EXPORT_KEY='sk-export-...'
python3 collector.py

To forward to a SIEM instead of SQLite, replace the with db: block with your sink's write, and persist next_cursor only after the sink acknowledges the batch. If the collector is down for longer than the 15-day retention, the saved cursor fails with 400; delete it to restart from a recent start_time.

Verify delivery​

  1. Call one tool through an MCP covered by the export key and note the time and tool name.
  2. After at least 15 minutes, run the collector once.
  3. Check that the row arrived: sqlite3 quilr_logs.db "SELECT id, ts, json_extract(body, '$.tool.name') FROM events ORDER BY ts DESC LIMIT 5".

Coverage​

The export covers MCP Gateway tool call traffic for the selected export scope, including:

Data typeExported
Tool call request metadataYes
Tool nameYes
Tool argumentsYes, with credential redaction
Tool response contentYes, with credential redaction
Input guardrail outcome and predictionsYes, with credential redaction
Output guardrail outcome and predictionsYes, with credential redaction
User emailYes
Agent identity from User-AgentYes
Extra metadataYes, with credential redaction
Raw bearer tokensNo
Raw request headersNo
Backend transport URLsNo

Response Events​

Every successful response starts with export_started, contains zero or more mcpgateway.tool_call events, and ends with checkpoint.

export_started​

The first line describes the export scope and effective export window.

{
"type": "export_started",
"schema_version": "v1",
"backend": {
"id": "backend_a1b2c3",
"name": "My Jira MCP"
},
"effective_start_time": "2026-05-14T00:00:00.000000Z",
"effective_end_time": "2026-05-14T01:00:00.000000Z",
"max_exportable_time": "2026-05-14T10:45:00.000000Z",
"end_time_clamped": false,
"limit": 1000
}
FieldTypeDescription
typestringAlways export_started.
schema_versionstringEvent schema version. Current value is v1.
backendobject or omittedMCP backend metadata for a backend-scoped export. Omitted for tenant-scoped exports.
scopeobject or omittedTenant export scope metadata for a tenant-scoped export. Omitted for backend-scoped exports.
effective_start_timestringISO 8601 timestamp where this export starts.
effective_end_timestringISO 8601 timestamp where this export ends.
max_exportable_timestringNewest timestamp eligible for export after the 15-minute lag.
end_time_clampedbooleantrue when the requested end_time was newer than max_exportable_time.
limitnumberMaximum tool call rows returned in this response.

For tenant-scoped export, the first line uses this scope shape:

{
"type": "export_started",
"schema_version": "v1",
"scope": {
"type": "tenant",
"tenant_id": "tenant_abc123",
"backend_count": 3
},
"effective_start_time": "2026-05-14T00:00:00.000000Z",
"effective_end_time": "2026-05-14T01:00:00.000000Z",
"max_exportable_time": "2026-05-14T10:45:00.000000Z",
"end_time_clamped": false,
"limit": 1000
}

scope​

FieldTypeDescription
typestringAlways tenant for tenant-scoped exports.
tenant_idstringTenant ID for the exported logs.
backend_countnumberNumber of tenant backends included in the export scope.

mcpgateway.tool_call​

Each tool call row is emitted as one mcpgateway.tool_call event.

{
"type": "mcpgateway.tool_call",
"schema_version": "v1",
"cursor": "<opaque-cursor>",
"backend": {
"id": "backend_a1b2c3",
"name": "My Jira MCP"
},
"request": {
"id": "jsonrpc-request-id",
"log_id": 123,
"started_at": "2026-05-14T00:00:01.123456Z",
"completed_at": "2026-05-14T00:00:01.573456Z",
"duration_ms": 450
},
"auth": {
"mode": "token"
},
"tool": {
"name": "create_issue"
},
"guardrails": {
"input": {
"outcome": "allowed",
"is_blocked": false,
"predictions": []
},
"output": {
"outcome": "allowed",
"is_blocked": false,
"predictions": []
}
},
"payload": {
"tool_arguments": {},
"response_content": {}
},
"response": {
"success": true,
"error_message": null
},
"metadata": {
"user_email": "dev@company.com",
"agent": "cursor",
"extra_data": {}
}
}

Top-Level Fields​

FieldTypeDescription
typestringAlways mcpgateway.tool_call.
schema_versionstringEvent schema version. Current value is v1.
cursorstringOpaque cursor for this tool call row.
backendobjectMCP backend metadata.
requestobjectJSON-RPC request and log metadata.
authobjectAuthentication mode used for the tool call.
toolobjectTool metadata.
guardrailsobjectInput and output guardrail outcomes and predictions.
payloadobjectTool arguments and response content, with credential redaction.
responseobjectTool call success and error metadata.
metadataobjectUser, agent, and extra request metadata.

backend​

FieldTypeDescription
idstringMCP backend ID.
namestringMCP backend display name.

request​

FieldTypeDescription
idstring or nullJSON-RPC request ID.
log_idnumberInternal log row ID used for stable pagination ordering.
started_atstringTool call start timestamp in ISO 8601 format.
completed_atstring or nullTool call completion timestamp in ISO 8601 format.
duration_msnumber or nullTool call duration in milliseconds.

auth​

FieldTypeDescription
modestring or nullAuthentication mode used by the gateway, such as token or OAuth-backed proxy auth.

tool​

FieldTypeDescription
namestring or nullMCP tool name.

guardrails​

FieldTypeDescription
inputobjectGuardrail result for tool call arguments.
outputobjectGuardrail result for tool response content.

Each guardrail result uses this shape:

FieldTypeDescription
outcomestring or nullFinal guardrail outcome for that direction.
is_blockedbooleanWhether that side of the tool call was blocked.
predictionsarray or nullPrediction results, with credential fields redacted.

payload​

FieldTypeDescription
tool_argumentsobject, array, string, or nullTool call arguments, with credential fields redacted.
response_contentobject, array, string, or nullTool response content, with credential fields redacted.

The export redacts common credential fields and token patterns inside payloads, predictions, and metadata. Header-like objects are replaced with [REDACTED_HEADERS].

response​

FieldTypeDescription
successboolean or nullWhether the tool call completed successfully.
error_messagestring or nullError message when the tool call failed, with credential patterns redacted.

metadata​

FieldTypeDescription
user_emailstring or nullEnd-user email associated with the tool call, when provided.
agentstring or nullAgent identity extracted from the request User-Agent header.
extra_dataobjectAdditional request metadata, with credential fields redacted.

checkpoint​

The final line on a successful response is a checkpoint.

{
"type": "checkpoint",
"schema_version": "v1",
"next_cursor": "<opaque-cursor>",
"rows": 1000,
"has_more": true,
"effective_end_time": "2026-05-14T01:00:00.000000Z",
"max_exportable_time": "2026-05-14T10:45:00.000000Z"
}
FieldTypeDescription
typestringAlways checkpoint.
schema_versionstringEvent schema version. Current value is v1.
next_cursorstringOpaque cursor to store and use on the next request.
rowsnumberNumber of mcpgateway.tool_call events emitted in this response.
has_morebooleantrue when another page is available for the same effective export window.
effective_end_timestringEffective upper bound used for this export response.
max_exportable_timestringNewest timestamp eligible for export after the 15-minute lag.

Errors​

Errors are returned as NDJSON too.

{"type":"error","error":{"message":"<message>","code":"<code>"}}
FieldTypeDescription
typestringAlways error.
error.messagestringHuman-readable error message.
error.codestringMachine-readable error code.

Errors before streaming starts use HTTP status codes. Errors after streaming has started are emitted as an error event line because the HTTP response has already been committed.