Skip to main content
A custom rule engine is a webhook on your own server that MCP Manager calls when a gateway rule using the Custom provider fires. Your server inspects the message and tells the gateway one of four things: pass it through, modify it, block it, or signal that it couldn’t decide. This page is the developer reference for that webhook — the full request/response contract, so you can stand up an engine in any stack. For a guided build that takes you from an empty project to a working engine, see Build and connect your first custom rule engine. This is the build-it-yourself path. To register an engine in the UI and understand the surrounding settings — URL, HTTP method, headers, header forwarding, IP allowlisting, testing, and deletion — see Custom Rule Engines. For managed alternatives that need no code, see Amazon Bedrock and Lakera Guard.

How it works

  1. Register a rule engine under Rule Engines with Custom as the provider, point the URL at your webhook, and optionally add request headers (for example, a bearer token for your own auth).
  2. Attach that engine to a gateway rule by selecting it as the rule’s Detection method.
  3. When a tool message reaches the rule’s detection hook, the gateway POSTs the message (wrapped in a small metadata envelope) to your webhook. Your webhook returns one of four shapes. The gateway acts on that shape before forwarding the message.
The gateway calls your engine over HTTPS only, and URLs that resolve to private or loopback IP ranges are rejected up front — your engine must be reachable on a public network. See Only HTTPS, public endpoints.

The contract, as TypeScript types

Paste this block into your project verbatim. Every field below is exactly what the gateway sends and expects in return. If a coding agent is generating your webhook, this is the source of truth — feed it these types.
webhook-types.ts

What the gateway sends

A single JSON object, posted with Content-Type: application/json plus any headers you configured. The HTTP method is whatever you set on the rule engine (defaults to POST). The shape is stable across all custom engines:
On a request-direction rule, body is the tool call and the arguments live under body.params.arguments. On a response-direction rule, body is the tool result, shown above.

Variations you’ll see in body.result

MCP servers don’t all produce the same result shape. A response-direction engine should handle these:
When error is present and result is missing, there’s usually nothing to inspect — the right move is to return pass, since blocking an error response just compounds the failure.

The four responses

Return one of these JSON shapes with HTTP 200 OK.

pass — let the message through unchanged

The gateway forwards the original message with no modification. comment lands in the rule_engine_comment column in your logs.

block — reject the message

The gateway replaces the upstream message with a JSON-RPC error so the client knows the call was blocked. If the rule has alerts enabled, an alert appears with your comment attached. Keep comment short and human-readable — it’s what the alert renderer displays.

modify — rewrite the message

The modifiedPayload.body you return is what the gateway forwards in place of the original. It must be a complete, valid JSON-RPC response: jsonrpc: "2.0", the same id as the inbound request (echo metadata.requestId), and either a result or an error field.
To replace a plain-string result, return "result": "Customer email: [REDACTED]" instead of the content envelope. To surface a tailored error to the client rather than the generic block message, return an error object in place of result:
If modifiedPayload.body isn’t a valid JSON-RPC envelope — missing jsonrpc, missing id, or neither result nor error — the gateway treats it as malformed and falls through to the rule’s failure mode. Don’t include extra top-level fields beyond jsonrpc / id / result / error.

error — you can’t decide

Use this when your engine ran but couldn’t reach a verdict. The gateway falls through to the rule’s failure mode: with failure mode Allow the original message passes through unchanged; with failure mode Block the message is blocked. Failure mode is set on the gateway rule, not the engine, so the same engine can be wired to different rules with different failure-mode policies. The default failure mode for custom engines is Block.

What gets rejected as malformed

The gateway treats any of these as an error outcome and routes through the rule’s failure mode:
  • Response is not valid JSON
  • HTTP status not in the 200–299 range (after retries are exhausted)
  • Body missing the type field
  • type not one of pass / block / modify / error
  • type: "modify" without a modifiedPayload.body that is a complete JSON-RPC response
  • Response body larger than 16 MiB
When this happens the rule-engine row in your logs records the specific reason (for example invalid_json, http_error, or connection_error) so you can debug from the dashboard. MCP Manager does not attempt to repair malformed JSON — quietly fixing a response that should have failed is treated as a security risk, so a broken response always falls through to the failure mode.

Operational notes

A few things worth knowing before you write your webhook:
  • Timeout. The gateway times out at 30 seconds per attempt. Your engine sits inline on tool traffic, so aim for sub-second latency in practice; the generous ceiling exists for engines that make their own downstream calls.
  • Retries. Transient failures (timeouts, 5xx) are retried with exponential backoff, up to 3 attempts (1 initial + 2 retries). 4xx responses are deterministic and aren’t retried. Make your webhook idempotent — receiving the same envelope twice must produce the same result. Use metadata.sessionId as a dedupe key if you have side effects.
  • Concurrency. Multiple tool calls can be in flight at once; each fires an independent POST. Don’t assume one call at a time.
  • TLS. All calls go over HTTPS. There’s no way to disable it, and self-signed certs aren’t supported — use a public CA.
  • No streaming. The webhook is a single request/response — no SSE, no chunked streaming. The envelope arrives fully buffered.
  • HTTP status codes. Return 200 OK for every shape, including block and error — those describe an outcome, not an HTTP failure. Reserve non-2xx for actual webhook failures (your service is down, the request was malformed).

Auth and headers

Anything you add in the Headers section of the rule engine is sent on every request, encrypted at rest until call time. Common patterns: a bearer token (Authorization: Bearer <your-token>), a custom API key (X-Api-Key: <your-key>), or a static signing secret you verify on your end. For defense in depth, you can also allowlist MCP Manager’s static IP. See Authenticating your engine.

Helpers — TypeScript

These helpers handle the body-shape variations above, so your route handler stays a clean four-branch switch.
helpers.ts

End-to-end example: Express + TypeScript

A complete webhook covering all four response shapes. Drop into a Node 20+ project with express and @types/express installed.
server.ts
Two things this demonstrates that you’ll want in your own version: authenticate before parsing (verify the secret before running your classifier), and stay idempotent (read the body once and return deterministically, so a retry of the same envelope produces the same result).

Common pitfalls

block and error are outcomes, not HTTP failures. Return 200 OK and put the outcome in the JSON type. A non-2xx status is treated as http_error and routed through the rule’s failure mode instead.
MCP clients correlate requests to responses by id. A mismatch makes the client wait forever and then time out. Always set modifiedPayload.body.id to the inbound metadata.requestId.
Only jsonrpc, id, result, and error are accepted. Anything else gets the response rejected as malformed and falls through to the failure mode.
Retries mean the same envelope can arrive more than once. If you log to an immutable audit store, insert a billing row, or fire an alert from inside the handler, key it on metadata.sessionId so a retry doesn’t double-count.

When to use a custom engine vs a built-in provider

Use Custom when you want full control: your own classifier, your own retraining pipeline, your own audit trail. If you’d rather drop in a managed service, MCP Manager has built-in providers — Amazon Bedrock Guardrails and Lakera Guard — that translate to and from a specific vendor’s API for you. You pick those from the same provider dropdown and only configure auth (and, for Bedrock, a couple of identifying fields); MCP Manager handles the request/response translation.

Further reading

Custom Rule Engines

Registering, testing, and managing the engine you build here.

Gateway Rules Overview

Detection methods, hooks, failure modes, actions, and rule ordering.

Amazon Bedrock Guardrails

A managed alternative to building your own engine.

Lakera Guard

A security-first managed alternative to a custom engine.