The MCP Security Checklist: How to Audit Your AI Agent's Browser Actions
MCP servers run with broad permissions. How do you know what your AI agent actually did? Screenshot, inspect, and record tamper-evident audit trails.
You just gave your AI agent access to your production servers via MCP.
It can:
- Click buttons
- Fill forms
- Read page content
- Interact with APIs
But you can't see what it actually did.
The problem: MCP servers run with broad permissions. Without visual proof, you have no way to audit whether an agent:
- Clicked the right button or hallucinated a selector
- Read the correct data or misinterpreted the page
- Made the intended action or took a detour
This is a security and governance nightmare. Your compliance team asks: "Show me what happened." You have logs, but no proof.
Here's the MCP security checklist: screenshot, inspect, and audit trail every action.
The MCP Security Problem: Broad Permissions, No Visibility
MCP servers grant agents access to real systems. Unlike constrained APIs, MCP is a general-purpose protocol for browser automation, file access, and system commands.
An agent using MCP can:
# Example: Agent interacts with your dashboard via MCP
result = await mcp_server.use_tool("click", selector="#approve-payment")
But what actually happened? Did the agent find the right button? Was it a payment approval or a fraud alert? Did the page state change as expected?
You don't know. You only have the agent's word.
For production systems handling payments, access control, or data deletion, this is unacceptable.
The Solution: Visual Proof at Every Step
Add PageBolt to your MCP security stack. After every action, capture:
- Screenshot — What does the page look like now?
- Inspect — What elements exist and what are their states?
- Video — Record the entire automation for tamper-evident audit trails
This transforms your agent from a black box into an auditable system.
The MCP Security Checklist
✅ Pre-Deployment Verification
- Agent can screenshot the target page after login
- Agent can inspect page structure (find selectors, elements, form fields)
- Agent understands the difference between expected and actual page state
- Agent logs every action with timestamp and visual proof
- Failed actions trigger alerts (screenshot shows error state)
✅ Per-Action Security
For each action (click, fill, navigate):
- Take a screenshot before the action — Establish baseline state
- Execute the action — Agent performs the operation
- Inspect the page — Verify the action had the expected effect
- Take a screenshot after the action — Prove the state changed
- Compare the two screenshots — Detect unexpected outcomes
✅ Audit Trail
- All screenshots timestamped and logged
- Video recording of entire sequence available for compliance review
- Action log includes: timestamp, selector used, element text, page URL, screenshot hash
- Video is encrypted and tamper-evident (hash included in log)
Real Example: Agent Approves a Payment
Here's what secure MCP automation looks like:
import asyncio
import os
import json
from anthropic import Anthropic
import hashlib
client = Anthropic()
PAGEBOLT_API_KEY = os.getenv("PAGEBOLT_API_KEY")
PAGEBOLT_BASE_URL = "https://pagebolt.dev/api/v1"
async def audit_agent_action(url: str, action_description: str, screenshot_before: bytes = None):
"""
Secure MCP action: Take visual proof before and after every action
"""
# Step 1: Screenshot BEFORE the action
if not screenshot_before:
screenshot_before = await take_screenshot(url)
screenshot_before_hash = hashlib.sha256(screenshot_before).hexdigest()
print(f"[AUDIT] Before screenshot hash: {screenshot_before_hash}")
# Step 2: Inspect the page to find the correct selector
inspect_response = await inspect_page(url)
print(f"[AUDIT] Page elements found: {len(inspect_response['elements'])} items")
# Step 3: Ask Claude to validate the action
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=500,
messages=[{
"role": "user",
"content": f"""
You are a security auditor validating MCP agent actions.
TASK: {action_description}
PAGE INSPECTION: {json.dumps(inspect_response, indent=2)}
Identify the correct element and whether this action is safe.
Respond in JSON: {{"selector": "#id", "safe": true, "reasoning": "...", "risks": []}}
"""
}]
)
action_plan = json.loads(response.content[0].text)
print(f"[AUDIT] Claude validation: {action_plan['reasoning']}")
if not action_plan["safe"]:
print(f"[SECURITY] Action blocked: {action_plan['risks']}")
return None
# Step 4: Execute the action via MCP
print(f"[ACTION] Clicking selector: {action_plan['selector']}")
# await mcp_server.click(action_plan['selector'])
# Step 5: Screenshot AFTER the action
await asyncio.sleep(2)
screenshot_after = await take_screenshot(url)
screenshot_after_hash = hashlib.sha256(screenshot_after).hexdigest()
print(f"[AUDIT] After screenshot hash: {screenshot_after_hash}")
# Step 6: Log the action
log_entry = {
"timestamp": "2026-03-26T14:23:45Z",
"action": action_description,
"selector": action_plan["selector"],
"url": url,
"screenshot_before_hash": screenshot_before_hash,
"screenshot_after_hash": screenshot_after_hash,
"status": "SUCCESS" if screenshot_before_hash != screenshot_after_hash else "NO_CHANGE_DETECTED"
}
print(f"[AUDIT] Logged: {json.dumps(log_entry, indent=2)}")
return log_entry
async def take_screenshot(url: str) -> bytes:
import requests
response = requests.post(
f"{PAGEBOLT_BASE_URL}/screenshot",
headers={"x-api-key": PAGEBOLT_API_KEY, "Content-Type": "application/json"},
json={"url": url, "format": "png"}
)
if not response.ok:
raise Exception(f"Screenshot failed: {response.status_code}")
return response.content
async def inspect_page(url: str) -> dict:
import requests
response = requests.post(
f"{PAGEBOLT_BASE_URL}/inspect",
headers={"x-api-key": PAGEBOLT_API_KEY, "Content-Type": "application/json"},
json={"url": url}
)
if not response.ok:
raise Exception(f"Inspect failed: {response.status_code}")
return response.json()
Why it's secure:
- Visual proof (screenshots) show exactly what happened
- Inspection prevents selector hallucination
- Claude validates the action before execution
- Audit log is tamper-evident (hash-based)
Preventing Selector Hallucination with /inspect
Agents hallucinate. They see a page and guess at selectors.
// Agent might hallucinate a selector that doesn't exist
await page.click("#approve-payment-btn"); // Wrong! No such element.
The /inspect endpoint prevents this by providing the actual page structure:
{
"elements": [
{
"type": "button",
"text": "Approve Payment",
"selector": "#payment-approve-7q3x",
"visible": true,
"x": 450,
"y": 320
}
]
}
Now Claude knows the real selector. No hallucination.
Tamper-Evident Audit Trails with /video
For compliance audits, a static screenshot isn't enough. You need a video.
import requests, hashlib
response = requests.post(
"https://pagebolt.dev/api/v1/video",
headers={"x-api-key": os.getenv("PAGEBOLT_API_KEY")},
json={
"steps": [
{"action": "navigate", "url": "https://dashboard.example.com"},
{"action": "click", "selector": "#login"},
{"action": "fill", "selector": "#email", "value": "user@example.com"},
{"action": "click", "selector": "#approve-btn"}
],
"format": "mp4",
"cursor": {"visible": True, "style": "classic"}
}
)
video_bytes = response.content
with open("audit-trail.mp4", "wb") as f:
f.write(video_bytes)
# Hash for tamper-evidence
video_hash = hashlib.sha256(video_bytes).hexdigest()
print(f"Audit trail hash: {video_hash}")
# Store hash in log — if video is modified, hash won't match
A video provides:
- Proof of execution — See the agent's actions in real-time
- Tamper detection — Hash mismatch = video was modified
- Compliance evidence — Play it for auditors
Pre-Deployment Security Check (Executable)
async def pre_deployment_security_check(agent_url: str):
checks = {
"can_screenshot": False,
"can_inspect": False,
"can_record_video": False,
"detects_hallucinated_selectors": False,
"logs_every_action": False
}
try:
await take_screenshot(agent_url)
checks["can_screenshot"] = True
print("✅ Screenshot capability verified")
except Exception as e:
print(f"❌ Screenshot failed: {e}")
try:
inspect_data = await inspect_page(agent_url)
if "elements" in inspect_data:
checks["can_inspect"] = True
print("✅ Inspect capability verified")
except Exception as e:
print(f"❌ Inspect failed: {e}")
try:
import requests as req
r = req.post(
f"{PAGEBOLT_BASE_URL}/video",
headers={"x-api-key": PAGEBOLT_API_KEY},
json={"steps": [{"action": "navigate", "url": agent_url}]}
)
if r.status_code == 200:
checks["can_record_video"] = True
print("✅ Video recording verified")
except Exception as e:
print(f"❌ Video failed: {e}")
if checks["can_inspect"]:
checks["detects_hallucinated_selectors"] = True
print("✅ Hallucination detection enabled")
if all([checks["can_screenshot"], checks["can_inspect"], checks["can_record_video"]]):
checks["logs_every_action"] = True
print("✅ Full action logging enabled")
if all(checks.values()):
print("\n✅ SECURITY CHECK PASSED: Agent is safe for production")
return True
else:
failed = sum(not v for v in checks.values())
print(f"\n❌ SECURITY CHECK FAILED: {failed} checks failed")
return False
Summary: The MCP Security Stack
| Layer | Tool | What It Does |
|---|---|---|
| Proof | Screenshot | Captures page state before & after actions |
| Prevention | Inspect | Shows real page structure, prevents hallucination |
| Evidence | Video | Records entire sequence for compliance audits |
| Validation | Claude API | Approves actions before execution |
| Logging | Audit trail | Timestamps, hashes, and logs every action |
MCP agents are powerful. But power without visibility is risk.
Add PageBolt to your MCP security stack:
- Screenshot after every action (baseline + proof)
- Inspect before every action (prevent hallucination)
- Record video for compliance audits (tamper-evident trails)
Your agents gain confidence. Your compliance team gets proof.
Audit your MCP agents — free
100 requests/month, no credit card. Screenshot, inspect, and video for every agent action.
Get API key free →