Overview
GetMCP signs every call log entry with an HMAC-SHA256 signature at the moment it is written. If any stored field is later modified — whether accidentally or deliberately — the signature no longer matches, and the verifier flags the row as tampered.
This makes the call log effectively append-only: entries can be deleted (and that deletion is itself detectable by gaps), but they cannot be silently edited.
How Signing Works
When a call log row is inserted into wp_getmcp_call_logs, GetMCP:
- Assembles a canonical string from the row’s key fields in a fixed order.
- Derives a signing key from WordPress’s
AUTH_KEY constant using HKDF with the domain string getmcp-log-signing-v1.
- Computes
HMAC-SHA256(canonical_string, signing_key).
- Stores the 64-character hex digest in the
signature column of the same row.
The signing key is zeroed from memory after use (sodium_memzero) when the PHP sodium extension is available.
Canonical Fields
The canonical string includes the following fields (joined with |):
| Field | Description |
|---|
id | Auto-increment row ID |
server_id | Server that received the call |
tool_id | Tool that was called |
method | MCP method (tools/call) |
arguments | Sanitized input arguments (JSON) |
response_status | success or error |
response_time_ms | Round-trip time in milliseconds |
status_code | Upstream HTTP status code |
client_type | Detected AI client |
client_ip | Caller IP address |
user_agent | Caller user-agent string |
error_message | Error text (if applicable) |
response_data | Full response (if Log Response Data is on) |
created_at | UTC timestamp |
Fields not included in the canonical form (so adding them later doesn’t break old signatures):
upstream_bytes / delivered_bytes
replay_data
Verifying the Audit Log
Go to GetMCP → Logs, open the More actions (⋮) kebab menu in the toolbar, and choose Verify integrity. GetMCP will:
- Fetch all rows matching your current filter (date range, server, status).
- Recompute the HMAC for each row.
- Compare it to the stored
signature.
- Return a summary report.
Verification result states
| State | Meaning |
|---|
| Verified | Signature matches — row is intact |
| Tampered | Signature does not match — field values were modified after write |
| Unsigned (legacy) | Row has no signature — written before audit log was introduced (DB < 1.12) |
| Unsigned (signer unavailable) | AUTH_KEY is missing or too short; signing was skipped at write time |
Rows marked Unsigned (legacy) are not evidence of tampering — they simply pre-date the feature. Only rows with a stored signature can be verified.
Requirements
| Requirement | Detail |
|---|
| AUTH_KEY | Must be defined in wp-config.php and at least 16 characters — standard on all healthy WordPress installs |
hash_hmac | PHP core since 5.1.2 — always available |
sodium_memzero | Optional — used to zero the key from memory after signing; signing still works without it |
| DB version ≥ 1.12 | The signature column is added by the 1.12 migration |
Security Considerations
Key rotation
The signing key is derived from AUTH_KEY. If you rotate AUTH_KEY (e.g. after a security incident), all existing signatures will fail verification — they were signed with the old key. This is expected behaviour. After rotation, treat all pre-rotation rows as unsigned legacy rows.
What tampering looks like
A tampered row has a stored signature value that no longer matches the HMAC of its current field values. The verifier flags it as Tampered and shows the row ID so you can investigate.
Common accidental causes of failed verification:
- Directly editing rows in phpMyAdmin or a MySQL client
- A database migration that modified existing rows
- Restoring a partial DB backup that mixed rows from two different
AUTH_KEY periods
What the audit log does not prevent
The audit log detects tampering — it does not prevent it. A user with direct MySQL access can delete rows or the entire table. Use database-level access controls and regular off-site backups for defence-in-depth.
Daily Automated Verification
A WordPress cron job (getmcp_daily_audit_verify) runs once per day and re-verifies every signed row from the previous calendar day (UTC). If any row’s signature does not match, GetMCP sends an alert via wp_mail() to the notification email configured at GetMCP → Settings → Notifications (falls back to the WordPress admin_email if unset). No email is sent on a clean run.
The same scan can be triggered ad-hoc against any filter range from GetMCP → Logs → More actions → Verify integrity, or via POST /wp-json/getmcp/v1/analytics/audit/verify for scripted full-history sweeps.
A getmcp_audit_tampering_detected action also fires with the list of tampered row IDs, so you can hook into it from a custom plugin (e.g. to forward to a SIEM).