This document describes every data storage container used by mcpbridge-wrapper: the SQLite metrics database, the in-memory metrics collector, and the audit log files.
mcpbridge-wrapper collects operational telemetry through three complementary layers:
| Layer | Class | Scope | Persistence |
|---|---|---|---|
| SQLite database | SharedMetricsStore |
Cross-process | Durable on disk |
| In-memory collector | MetricsCollector |
Single process | Lost on exit |
| Audit log files | AuditLogger |
Single process (loaded at startup) | Durable on disk |
All three layers are populated from the same interception point in __main__.py and exposed to the Web UI dashboard via server.py.
MCP client (stdin)
│
▼
__main__.py ─── on_request() ──► MetricsCollector.record_request()
│ ──► SharedMetricsStore.record_request()
│ ──► AuditLogger.log(direction="request")
│
(forward to mcpbridge)
│
▼
__main__.py ─── on_response() ──► MetricsCollector.record_response()
──► SharedMetricsStore.record_response()
──► AuditLogger.log(direction="response")
▼
server.py ── GET /api/metrics/summary
── GET /api/metrics/timeseries
── GET /api/audit/entries
── GET /api/audit/export/csv
__main__.py also calls set_client_info() on both stores when the MCP initialize handshake is received (from clientInfo.name / clientInfo.version in the request params).
Source file: src/mcpbridge_wrapper/webui/shared_metrics.py
Purpose: Provides process-safe, durable storage so that all wrapper processes (e.g. multiple Zed/Cursor connections) write to the same metrics backend and the Web UI can aggregate across them.
Default path: ~/.cache/mcpbridge-wrapper/metrics.db
The path can be overridden by passing a custom db_path to SharedMetricsStore(db_path=...), which WebUIConfig does when the user sets a custom data directory.
Connection model: Each thread gets its own sqlite3.Connection via threading.local(). All writes go through a _transaction() context manager that commits on success and rolls back on exception. The connection timeout is 10 seconds.
Stores one row per MCP tool call event. A request and its corresponding response are recorded as a single row: the request inserts the row, and the response updates it with latency_ms and error information.
| Column | Type | Nullable | Description |
|---|---|---|---|
id |
INTEGER PK AUTOINCREMENT |
no | Internal row identifier |
request_id |
TEXT |
yes | JSON-RPC request ID (null for notifications) |
tool_name |
TEXT |
no | MCP tool name (e.g. XcodeGrep) |
timestamp |
REAL |
no | Unix epoch (seconds) when the request arrived |
latency_ms |
REAL |
yes | End-to-end latency in milliseconds; NULL until the response is recorded |
error |
BOOLEAN DEFAULT 0 |
no | 1 if the response contained a JSON-RPC error |
error_code |
INTEGER |
yes | JSON-RPC error code (e.g. -32601); NULL for successful responses |
error_message |
TEXT |
yes | JSON-RPC error message string; NULL for successful responses |
Indexes:
idx_requests_toolontool_name— accelerates per-tool aggregation queriesidx_requests_timeontimestamp— accelerates time-window queries
Migration note: error_code and error_message columns were added in P12-T3 via ALTER TABLE … ADD COLUMN (wrapped in contextlib.suppress for idempotency on existing databases).
A single-row table (always id = 1) that records the identity of the most recently connected MCP client.
| Column | Type | Nullable | Description |
|---|---|---|---|
id |
INTEGER PK DEFAULT 1 |
no | Always 1; enforces single-row constraint |
client_name |
TEXT |
yes | Client name from initialize handshake (e.g. "Cursor") |
client_version |
TEXT |
yes | Client version string (e.g. "1.2.3") |
updated_at |
REAL |
yes | Unix epoch of the last update |
Rows are written with INSERT … ON CONFLICT(id) DO UPDATE (upsert), so there is always at most one row.
- Window:
get_summary()andget_timeseries()accept awindow_secondsparameter (default 3 600 s = 1 hour) that filters rows bytimestamp > now - window_seconds. Rows outside the window are still in the database but excluded from aggregation. - Reset:
SharedMetricsStore.reset()issuesDELETE FROM requestsandDELETE FROM client_info, removing all rows. The database file itself is not deleted. - No automatic purge: Old rows are not automatically deleted;
reset()must be called explicitly (e.g. viaPOST /api/metrics/resetin the Web UI).
Source file: src/mcpbridge_wrapper/webui/metrics.py
Purpose: Fast, lock-protected in-process accumulator for real-time dashboard metrics. Complementary to SharedMetricsStore — this layer provides accurate per-process latency percentiles and in-flight request counts that are difficult to compute from SQLite alone.
Thread safety: All mutations and reads are guarded by a single threading.Lock.
| Field | Type | Description |
|---|---|---|
_total_requests |
int |
Cumulative count of all requests since start or last reset |
_total_errors |
int |
Cumulative count of all error responses |
_start_time |
float |
Unix epoch when the collector was created or last reset |
| Field | Type | Description |
|---|---|---|
_tool_counts |
Dict[str, int] |
Request count per tool name |
_tool_errors |
Dict[str, int] |
Error count per tool name |
_tool_latencies |
Dict[str, List[float]] |
All recorded latencies (ms) per tool, capped at max_datapoints entries |
All deques have maxlen=max_datapoints (default 3 600). When full, the oldest entry is evicted automatically.
| Field | Type | Contents |
|---|---|---|
_request_times |
Deque[float] |
Unix epoch timestamp of each request |
_error_times |
Deque[float] |
Unix epoch timestamp of each error response |
_latency_series |
Deque[Tuple[float, float]] |
(timestamp, latency_ms) pairs for all responses with known latency |
| Field | Type | Description |
|---|---|---|
_in_flight |
Dict[str, float] |
Maps request_id → start timestamp for requests that have been received but not yet responded to. Used to auto-compute latency_ms when record_response() is called without an explicit latency. Entries are removed on record_response(). |
| Field | Type | Default | Description |
|---|---|---|---|
_client_name |
str |
"unknown" |
MCP client name from initialize |
_client_version |
str |
"unknown" |
MCP client version from initialize |
| Field | Type | Description |
|---|---|---|
_error_counts_by_code |
Dict[int, int] |
Maps JSON-RPC error code → count of occurrences |
The module-level categorize_error(code) function maps a JSON-RPC error code to a severity bucket used by the dashboard for colour-coding:
| Category | Condition |
|---|---|
"protocol" |
-32699 ≤ code ≤ -32600 (standard JSON-RPC errors) |
"timeout" |
code == -32001 |
"tool" |
code ≥ 1 (Xcode-side tool execution errors) |
"unknown" |
Any other code, or None |
MetricsCollector.reset() clears all fields:
_total_requestsand_total_errorsreset to0_start_timeset to current time- All per-tool dicts cleared
- All deques cleared
_in_flightcleared_client_nameand_client_versionreset to"unknown"_error_counts_by_codecleared
Data is lost on process exit — there is no persistence.
Source file: src/mcpbridge_wrapper/webui/audit.py
Purpose: Provides a durable, human-readable record of every MCP tool call for compliance, debugging, and post-hoc analysis. Survives process restarts.
- Encoding: UTF-8 newline-delimited JSON (
.jsonl) - File naming:
audit_YYYYMMDD_HHMMSS.jsonl(UTC timestamp) - Default directory:
logs/audit/(normalized to an absolute path at startup). Relative values resolve from the--web-ui-configfile directory when provided, otherwise from the process working directory. - Rotation: A new file is opened when the current file exceeds
max_file_size_mb(default 10 MB). Up tomax_files(default 10) rotated files are retained; the oldest is deleted when the limit is exceeded. - Startup load: At initialisation, all
audit_*.jsonlfiles inlog_dirare read in chronological order. The most recent 10 000 entries are loaded into_entriesin memory so the Web UI dashboard can display history from sibling processes.
Each line is a JSON object. Fields are written only when non-None:
| Field | Type | Always present | Description |
|---|---|---|---|
timestamp |
float |
yes | Unix epoch (seconds) |
timestamp_iso |
str |
yes | ISO 8601 UTC string (e.g. "2026-02-15T10:30:00Z") |
tool |
str |
yes | MCP tool name |
direction |
str |
yes | "request" or "response" |
request_id |
str |
when present | JSON-RPC request ID |
request |
object |
when captured | Sanitised request payload (requires capture_payload=True) |
response |
object |
when captured | Sanitised response payload (requires capture_payload=True) |
latency_ms |
float |
on response | Round-trip latency in milliseconds |
error |
str |
on error | Error message string |
error_code |
int |
on error | JSON-RPC error code |
Payload capture: Disabled by default (capture_payload=False). When enabled, payloads larger than 64 KB are truncated to {"_truncated": true, "raw": "<truncated JSON>"}. The payload ring buffer holds at most 500 entries (oldest evicted).
AuditLogger.export_csv() writes a subset of fields. Extra fields in the JSONL records are silently ignored:
| Column | Description |
|---|---|
timestamp_iso |
ISO 8601 UTC timestamp |
tool |
MCP tool name |
direction |
"request" or "response" |
request_id |
JSON-RPC request ID |
latency_ms |
Latency in milliseconds (empty for request-direction rows) |
error |
Error message (empty for successful responses) |
- File rotation: Automatic at
max_file_size_mbper file (default 10 MB), keepingmax_filesmost recent (default 10). Total maximum disk usage: ~100 MB. - In-memory cap:
_entriesis trimmed to the most recent 10 000 entries. - No reset API:
AuditLoggerhas noreset()method. Log files on disk persist until rotated out. The in-memory_entrieslist is repopulated from disk on restart. - Closing:
AuditLogger.close()flushes and closes the current.jsonlfile. This is called automatically when the Web UI server shuts down.
- Architecture Overview — system-level data flow diagram
- Web UI Setup — how to enable the dashboard
- Environment Variables — configuration options including custom data paths