The Four Guard Proxy Transformers: What Each One Intercepts, In Order
Published April 28, 2026
Guard Proxy is the MCP interception layer that sits between an agent and its tool servers. Most of the published material on it talks about where it sits — cloud, Docker, stdio. This post is about what runs inside it on every call: a deterministic four-transformer pipeline, sorted by integer order, applied to every tool request and response that crosses the proxy.
The four built-ins are PIITransformer, ContextInjectTransformer, FieldMaskTransformer, and SchemaValidateTransformer. They live in packages/guard-proxy/src/transformers/ and all subclass BaseTransformer (base.py). The same interface — transform(payload, context) -> payload plus an applies_to(direction, tool_name) -> bool predicate — is the contract any custom transformer needs to satisfy to slot into the pipeline.
The pipeline runner sorts by .order, evaluates applies_to() against the current direction (request or response) and tool name, and skips anything that doesn't apply. The remainder runs in deterministic sequence.
1. PIITransformer — order 0, direction both
The first thing every payload hits. PIITransformer walks the arguments dict on the request side and the result field on the response side, calling api_client.tokenize_pii() for any string value longer than three characters. When tokens are created, the value is replaced in place; when nothing PII-shaped is found, the value passes through untouched.
The implementation detail that matters: tokenization is session-scoped. The first PII string in a request seeds a session_id that subsequent strings reuse, so a name appearing in three different argument fields gets the same token in all three. The session ID is stashed on the payload under _pii_sessions["request"], and the response transformer threads its own session for outbound rehydration.
The actual NER work is delegated to the VeriSwarm Guard API (Microsoft Presidio under the hood) — the transformer is a thin orchestration layer. That separation is what makes it safe to run in stdio mode against a local agent: the model and the policy live server-side, the transformer just routes.
What you can't configure here: PII tokenization is on or off, governed at the proxy layer (GUARD_PII_ENABLED), not via per-tenant transformer config. Custom redaction rules belong in FieldMaskTransformer.
2. ContextInjectTransformer — order 5, direction request only
Five units later in the order, ContextInjectTransformer adds VeriSwarm trust metadata to the outbound tool call. By default it injects trust_score and policy_tier from the active TransformContext into a _veriswarm_context key inside the arguments dict.
"arguments": {
"user_id": "...",
"_veriswarm_context": {
"trust_score": 87,
"policy_tier": "review"
}
}
Configuration options:
inject_fields: which oftrust_score,policy_tier,agent_id,tenant_idto includetarget_key: rename the injected dict (default_veriswarm_context)
This is request-only — there's no analogous response-side injection. The intended use case is downstream tool servers that want to make their own decisions based on the calling agent's trust posture (e.g., a CRM tool that allows reads at any tier but blocks writes when policy_tier == "review"). It's the pipeline equivalent of an X-Trust-Score header for MCP.
If your downstream tools don't read this metadata, the transformer is a no-op — but it costs nothing to leave on, and the moment you ship a tool that does care, the data is already there.
3. FieldMaskTransformer — order 10, direction both
Ten units in. FieldMaskTransformer is the per-tenant escape hatch for the cases that PII tokenization doesn't cover — internal identifiers, API keys, schema-specific secrets, or any field where the right answer is "the agent should never see this in clear form."
It takes a list of dot-notation paths and a strategy. Three strategies ship:
redact— replace with[REDACTED]. The default.hash— SHA-256 truncated to 16 hex characters. Useful when you want a stable join key but not the underlying value.partial— preserve the last four characters, mask the rest with asterisks. Card numbers, account numbers, anything where humans expect a "...1234" suffix.
config = {
"paths": ["arguments.password", "result.account_number"],
"strategy": "partial",
}
applies_to() honors an optional tool_name filter, so the same transformer instance can be installed once and only fire against the tool it's meant for. Order 10 is deliberately after PII tokenization — by the time field mask runs, anything Presidio caught is already in token form, so masking is a precision tool for the things NER doesn't model.
This is where most Max-tier custom rules end up living. The transformer interface is small (a path, a strategy, a tool filter); the engineering surface for adding a new redaction is minutes, not architecture.
4. SchemaValidateTransformer — order 20, direction both
Last in the pipeline. SchemaValidateTransformer validates either the request arguments (when direction == "request") or the parsed JSON of the response result (when direction == "response") against a configured JSON schema.
The validator is intentionally simple: required-field presence and primitive type checks (string, integer, number, boolean, array, object). It is not a full JSON Schema implementation — no oneOf, no regex pattern, no nested schema composition. The decision was deliberate: the use case is "does this payload look like the shape we agreed on," not "does it conform to a 600-line schema."
The interesting field is on_failure:
warn— log the violation and append it topayload["_warnings"]. The call still proceeds.block— setpayload["_blocked"] = Trueand write_block_reason. The proxy halts the call without forwarding.
Order 20 means schema validation runs after PII tokenization, context injection, and field masking — which is the right call. If a tool requires a string field and the upstream transformer chain has already rewritten that field's value to [VS:EMAIL:a1b2c3], schema validation should still see a string, and it does. Validating against the original payload would flag legitimate transformations as schema breaks.
This is the layer that catches MCP servers returning unexpectedly-shaped responses — the early signal that a downstream tool has been updated, swapped, or compromised. A response that suddenly fails schema validation against last week's contract is exactly the signal you want to surface before the agent acts on it.
Adding a custom transformer
The contract is the four lines of BaseTransformer:
class MyTransformer(BaseTransformer):
name = "my_transformer"
order = 15 # between field_mask (10) and schema_validate (20)
direction = "request"
async def transform(self, payload, context):
# ... your logic
return payload
Pick an order that places your transformer where you want it relative to the four built-ins. Implement transform(). Optionally override applies_to() to scope by direction or tool name. The pipeline runner picks it up automatically.
The deterministic ordering is the contract you're buying into. PII tokens are guaranteed to exist by the time your transformer runs if your order is greater than 0. Trust context is guaranteed to be in the args if you're past 5. Mask redactions have happened if you're past 10. Schema validation hasn't run yet unless you're past 20. Knowing which prior steps have already executed is half the value of the pipeline.
What this isn't
The transformer pipeline is not the policy decision layer. Whether the call is allowed at all is decided upstream, by the policy check that runs before any transformer touches the payload. Cedar policies live one layer above that, evaluated by Gate.
The pipeline is what runs after the call has been authorized — the four-stage shaping, masking, validation, and metadata enrichment that determines what data actually crosses the wire and what comes back from the tool server.
Deploy it
Guard Proxy is part of Guard, on the Max plan. The four built-in transformers run on every tool call by default — no configuration required to get the baseline pipeline. Field mask paths, schema definitions, and custom transformers are configured per-tenant.
The full configuration matrix — environment variables, allowlist/blocklist, per-protection toggles — is in the Guard Proxy README.
VeriSwarm Guard Proxy ships four built-in MCP transformers (PII, context inject, field mask, schema validate) plus an extension interface for tenant-specific rules. Cloud, Docker, and stdio modes share the same pipeline.