pyveil

Model Context Protocol guide

MCP PII redaction: protect tools, results, and resources.

Keep protocol payloads structurally intact while removing sensitive values before they enter an AI agent's context.

Published July 11, 2026 · Uses pyveil 0.2.1 · Provider-neutral

MCP has more than one sensitive-data boundary

An MCP server can move data in both directions. Treat each direction separately:

SurfaceRiskpyveil channel
Tool argumentsA model attempts to send credentials into a tooltool.call.arguments
Tool resultsAn API or database returns PII or hidden tokenstool.call.result
Resource contentFiles, records, or logs enter agent contextmcp.resource.content
MCP payload channel policy agent-safe payload

Create one scoped redactor for the server

from pyveil import Veil

veil = Veil.high(
    secret=b"tenant-secret-from-your-secret-manager",
    scope="tenant-123/mcp-server",
)

Use separate instances or scopes when tenants or runs must not share stable placeholder identities.

Block credential-like tool arguments

The default high policy blocks authorization headers, API keys, private keys, JWTs, and token-like values before model-controlled tools execute.

from pyveil import BlockedSensitiveData, Channel

def run_mcp_tool(name: str, arguments: dict) -> object:
    try:
        safe = veil.redact_data(
            arguments,
            channel=Channel.TOOL_CALL_ARGUMENTS,
        )
    except BlockedSensitiveData as exc:
        raise PermissionError(exc.summary()) from exc

    return dispatch_tool(name, safe.data)

Do not catch the exception and then run the tool with the original arguments. A block is a refusal decision, not a warning.

Redact tool results before returning them to the model

from pyveil.integrations.mcp import redact_tool_result

raw_result = {
    "owner": "alice@example.com",
    "callback": "https://example.test/cb?access_token=demo-only-token",
}

safe_result = redact_tool_result(raw_result, veil)
return safe_result

The helper uses tool.call.result. You can call veil.redact_data(...) directly when your server needs a custom channel or policy.

Redact resource content at read time

from pyveil.integrations.mcp import redact_resource

def read_resource_for_agent(uri: str) -> object:
    raw = resource_store.read(uri)
    safe = redact_resource(raw, veil)
    return safe

Apply the boundary after authorization and retrieval, but before content is serialized into an MCP response. This avoids storing a second redacted copy while still keeping raw data out of agent context.

Test the response object the agent receives

def test_mcp_resource_hides_email():
    raw = {"text": "Owner alice@example.com"}
    result = veil.redact_data(raw, channel="mcp.resource.content")

    assert "alice@example.com" not in str(result.data)
    assert "[EMAIL:" in result.data["text"]
    assert result.findings[0].path == "/text"
    assert result.findings[0].raw is None

Also test the block path for tool arguments and ensure logs contain only exc.summary(), never the original payload.

What this MCP boundary does not do

  • It does not authorize the caller or resource. Run normal auth checks first.
  • It does not stop prompt injection or malicious tool selection.
  • It does not discover every unknown name or address. Add known-value rules or an upstream NER layer.
  • It does not provide reversible unmasking to the model.
Keep raw data server-side

Do not expose an "unmask" MCP tool. That turns the redaction layer into a model-accessible secret oracle.