modifiedPayload.body validation, timeouts, and retries. Read that first — everything here builds on its contract and reuses its shared types (WebhookRequest, WebhookResponse) and helpers (extractResponseText, buildResponseWithReplacedText).
Every recipe below is a function you’d call from the /inspect route handler in that page’s Express example. They sort into three motivations:
How the transform fits in
All five recipes fire on a response-direction rule, inspectbody.result, and hand the gateway a rewritten (or rejected) result. The agent only ever sees what your webhook returns.
These examples assume the tool returns its structured data as a JSON string inside
body.result.content[0].text — the most common shape, and the one extractResponseText and buildResponseWithReplacedText are built for. If your server populates result.structuredContent instead, apply the same parse → transform → re-serialize logic to that object and set it back on result. See the result variations for the shapes you might encounter.A rule engine runs on every
tools/call flowing through the gateway. There is no per-server or per-tool selector in the rule UI, so your webhook sees every tool from every server on that gateway — including tools that legitimately return non-JSON. Each recipe therefore checks metadata.serverGuid and metadata.toolName first and returns pass immediately for anything it wasn’t written for, so unrelated traffic is waved through untouched and cheaply. Only after confirming it’s the target server and tool do we parse the result; because that tool is contracted to return JSON, a result that isn’t parseable JSON is a real anomaly and gets blocked rather than passed.Slim verbose responses to cut token cost
Goal: cut token cost. MCP servers tend to return everything they know about an object — twenty-plus fields when your agent needs three. Every unused field is input tokens the model pays to read on every call. An allowlist keeps only the fields a given agent or gateway actually uses and drops the rest, including expensive rich-text fields likedescription.
slim-fields.ts
Raw Tool Response
Modified Tool Response
Strip or redact fields the model should never see
Goal: data governance. Some fields must never reach the model, and you know them by name — anssn, an internal creditScore, a compensation figure. Because the field name is a stable, unchanging identifier, a denylist by key is exact and predictable. You have two strategies:
- Delete the key entirely — the model never knows it existed.
- Redact — keep the key but replace its value with a placeholder like
{{REDACTED}}. The model can see that the field was present but withheld, which stops it from assuming the data is simply missing and retrying the call a different way.
strip-fields.ts
Raw Tool Response
Modified Tool Response — Redact
Modified Tool Response — Delete
Summarize long fields in place
Goal: cut token cost while keeping the gist. Sometimes the model doesn’t need a field gone — it needs it shorter. A 4,000-tokendescription, body, or notes field where the agent only needs the gist is pure waste. Call your own model to summarize the value and splice the summary back into the response.
summarize-field.ts
Raw Tool Response
Modified Tool Response
Scope results to the calling identity
Goal: identity-aware data governance. A shared tool —list_opportunities, search_documents — often returns everything, regardless of who asked. The envelope’s metadata.userGuid tells you which user triggered the call, so you can filter the result down to the records that user is allowed to see. The envelope also carries metadata.userEmail — the same caller’s email address — which is often a more convenient join key than the GUID when your access model is keyed on email (an identity provider, a CRM owner field, a directory lookup). This recipe resolves on userGuid; swap in userEmail wherever you’d resolve the owner if that maps more cleanly to your data. Combine either with runtime header forwarding to receive the inbound connection’s identity headers and map them to your own access model.
identity-filter.ts
Raw Tool Response
Modified Tool Response — Caller USR-jane
Block and audit on a policy violation
Goal: data governance and compliance. Some responses must never pass at all — a document classified above the caller’s clearance, or one carrying a restricted data class. Detect the condition and return block: the gateway replaces the result with a JSON-RPC error, and if the rule has alerts enabled, yourcomment is what the alert renders. The same comment lands in the rule_engine_comment column in your logs, giving you an audit trail of every blocked call.
block-on-classification.ts
Raw Tool Response
Engine Verdict — sent back to the gateway
modify with an error body instead of block — for example, an error whose message reads “This document can’t be accessed through this assistant.” Either way the data never leaves the gateway.
The custom provider’s response contract is
type + comment + (for modify) modifiedPayload. There’s no structured detections field to return — that’s reserved for the built-in Presidio and Lakera providers. Put what a human needs to know into comment.Combining recipes
These aren’t mutually exclusive. A single webhook can run several in sequence — block on classification first, then strip fields by name, then slim and summarize what’s left — returning a singlemodify with the cumulative result. You can also split them across several gateway rules on the same gateway, each pointed at the same engine or different ones; rules fire in order, and the first block short-circuits the rest. Keep each transform small and idempotent, and let the rule ordering compose them.
Further reading
Building a Custom Rule Engine
The full webhook contract these recipes build on: envelope, response shapes, validation, and limits.
Custom Rule Engines
Registering, testing, header forwarding, and managing the engine in the UI.
Gateway Rules Overview
Detection methods, hooks, failure modes, and how rules compose in order.
Viewing Logs
Where rule-engine comments and outcomes land for auditing.

