securitymcpfinancial

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.

8 min

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

Python
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 this

Pattern 2: Confirmation gates

If write operations are necessary, require explicit human confirmation before execution. Never let an agent execute a financial transaction autonomously.

Python
@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

Python
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

Python
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.