Agent Security Risks in Financial MCP Servers
Financial MCP servers need read-only tokens, confirmation gates, input validation, and audit logging. Never expose write operations.
Financial MCP servers -- tools that give AI agents access to banking APIs, trading platforms, and payment processors -- are the highest-risk MCP category. A prompt injection or tool misuse can trigger real financial transactions. Here are the security patterns every financial MCP server must implement.
The threat model
- Prompt injection: malicious content in search results or documents tricks the agent into calling financial tools
- Tool misuse: agent calls a transfer function when asked to check a balance
- Credential exposure: API keys for financial services embedded in MCP configuration
- Scope creep: agent authorized for read-only access inadvertently gets write access
- Data leakage: financial data returned by tools gets logged or cached insecurely
Pattern 1: Read-only by default
from mcp.server import Server
from mcp.types import TextContent
app = Server("financial-readonly")
@app.tool()
async def check_balance(account_id: str) -> list[TextContent]:
"""Check account balance. Read-only, no transactions."""
# Only GET requests, never POST/PUT/DELETE
resp = requests.get(
f"{BANK_API}/accounts/{account_id}/balance",
headers={"Authorization": f"Bearer {READ_ONLY_TOKEN}"},
)
balance = resp.json()
return [TextContent(type="text", text=f"Balance: ${balance['amount']:.2f}")]
# DO NOT expose transaction endpoints as MCP tools
# @app.tool()
# async def transfer_funds(...): # NEVER do thisPattern 2: Confirmation gates
If write operations are necessary, require explicit human confirmation before execution. Never let an agent execute a financial transaction autonomously.
@app.tool()
async def prepare_transfer(
from_account: str, to_account: str, amount: float
) -> list[TextContent]:
"""Prepare a transfer for human review. Does NOT execute."""
# Generate a confirmation token, do not execute
token = generate_confirmation_token({
"from": from_account,
"to": to_account,
"amount": amount,
})
return [TextContent(
type="text",
text=f"Transfer prepared: ${amount:.2f} from {from_account} to {to_account}. "
f"Confirmation required. Token: {token}. "
f"Human must approve at /confirm/{token}",
)]Pattern 3: Token scoping
- Use separate API tokens for MCP servers with minimal permissions
- Read-only tokens for balance checks and transaction history
- No transfer or payment tokens in MCP configuration
- Rotate tokens on a schedule (monthly minimum)
- Never embed tokens in the MCP server code -- use environment variables
Pattern 4: Input validation
import re
def validate_account_id(account_id: str) -> bool:
"""Validate account ID format before API call."""
# Only allow expected format: 10-digit numeric
return bool(re.match(r"^d{10}$", account_id))
def validate_amount(amount: float) -> bool:
"""Validate transfer amount."""
return 0 < amount <= 10000 # Hard cap at $10K
@app.tool()
async def check_balance(account_id: str) -> list[TextContent]:
"""Check account balance with input validation."""
if not validate_account_id(account_id):
return [TextContent(type="text", text="Invalid account ID format.")]
# Safe to proceed with validated input
resp = requests.get(
f"{BANK_API}/accounts/{account_id}/balance",
headers={"Authorization": f"Bearer {READ_ONLY_TOKEN}"},
)
return [TextContent(type="text", text=f"Balance: ${resp.json()['amount']:.2f}")]Pattern 5: Audit logging
import logging, json
from datetime import datetime
audit_logger = logging.getLogger("financial_audit")
audit_logger.setLevel(logging.INFO)
handler = logging.FileHandler("financial_mcp_audit.log")
audit_logger.addHandler(handler)
def audit_log(tool_name, params, result, user_context):
entry = {
"timestamp": datetime.utcnow().isoformat(),
"tool": tool_name,
"params": params,
"result_summary": result[:200],
"user": user_context,
}
audit_logger.info(json.dumps(entry))What to search for, not transact
For financial agents, search is the safe capability. Searching for stock prices, market data, company financials, and economic indicators carries zero transaction risk. Keep MCP tools in the search/read category and handle writes through separate, human-gated interfaces.
Bottom line
Financial MCP servers need defense in depth: read-only tokens, input validation, confirmation gates, and audit logging. Never expose write operations (transfers, trades, payments) as direct MCP tools. The cost of a single unauthorized transaction far exceeds the convenience of automated access.