> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mcpmanager.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Build and connect your first custom rule engine

> A hands-on MCP Manager lesson for developers: write a minimal webhook that redacts emails, expose it over public HTTPS, register it under Rule Engines, verify it with the built-in Test action, attach it to a gateway rule, and confirm the modify verdict in your logs.

This tutorial connects your own code to the gateway. You'll write a tiny webhook that redacts email addresses from tool results, expose it over HTTPS, register it under **Rule Engines**, prove it works with the built-in **Test** action, then attach it to a gateway rule and watch its `modify` verdict land in your logs. By the end you have the full loop — your service deciding, in-line, what passes the gateway.

It assumes you've seen a rule fire before; if not, do [Add your first gateway rule](/tutorials/first-gateway-rule) first. The full webhook contract — every field, every verdict — lives in [Building a Custom Rule Engine](/advanced/building-a-custom-rule-engine); this lesson walks one path end to end.

<Info>
  Registering engines uses the **Manage integrations** capability; if you don't see **Rule Engines** in the left-hand navigation, your role lacks it. See the [capabilities reference](/deployment/rbac-and-roles/capabilities). Your engine must be reachable on **public HTTPS** — MCP Manager rejects `http://` and private, loopback, or link-local addresses.
</Info>

## What you'll need

* Node.js 18+ and a terminal.
* A way to expose a local port over public HTTPS — a tunnel such as `ngrok`, or a quick deploy to any host.
* A gateway with a server whose tool returns text. The **Quickstart** gateway from [Your first governed tool call](/tutorials/first-tool-call) works.
* About 25 minutes.

## How a custom engine fits

A custom rule engine is a webhook the gateway POSTs to when a rule using it fires. The gateway sends the message wrapped in a metadata envelope; your webhook returns one of four verdicts — `pass`, `block`, `modify`, or `error` — and the gateway acts on it before forwarding. This tutorial uses **modify**: your webhook rewrites the tool result and hands back a complete replacement.

## Step 1: Write the webhook

Create a project and install Express:

```bash terminal theme={null}
mkdir email-redactor && cd email-redactor
npm init -y && npm pkg set type=module
npm install express
```

Save this as `server.js`. It authenticates with a shared secret, pulls the text out of the tool result, and returns a `modify` verdict that redacts any email — echoing the inbound request `id` so the response stays valid JSON-RPC:

```js server.js theme={null}
import express from 'express';

const app = express();
app.use(express.json({ limit: '16mb' })); // matches the gateway's body cap

const SECRET = process.env.RULE_ENGINE_SECRET ?? 'change-me';

app.post('/inspect', (req, res) => {
  // Authenticate before doing any work.
  if (req.headers['x-api-key'] !== SECRET) {
    return res.status(401).json({ error: 'unauthorized' });
  }

  const { metadata, body } = req.body;
  const result = body?.result;
  const email = /\b[\w.+-]+@[\w.-]+\.[A-Za-z]{2,}\b/g; // defined per-call: no shared lastIndex

  // Pull text out of the result — a content envelope or a plain string.
  const text =
    typeof result === 'string'
      ? result
      : Array.isArray(result?.content)
        ? result.content.filter((c) => c?.type === 'text').map((c) => c.text).join('\n')
        : null;

  if (!text || !email.test(text)) {
    return res.json({ type: 'pass', comment: 'no email found' });
  }

  // Rebuild the result with emails redacted, keeping the SAME id.
  const redacted = text.replace(email, '[REDACTED EMAIL]');
  const newResult =
    typeof result === 'string'
      ? redacted
      : { ...result, content: result.content.map((c) => (c?.type === 'text' ? { ...c, text: redacted } : c)) };

  res.json({
    type: 'modify',
    comment: 'redacted email address(es)',
    modifiedPayload: { body: { jsonrpc: '2.0', id: metadata.requestId, result: newResult } },
  });
});

app.listen(3000, () => console.log('rule engine on :3000'));
```

Run it: `RULE_ENGINE_SECRET=my-secret node server.js`.

<Warning>
  Two things make this contract-correct, and your real engine needs both: **echo `metadata.requestId`** as the `id` of `modifiedPayload.body` (a mismatch makes the client hang), and return `modifiedPayload.body` as a **complete** JSON-RPC response with only `jsonrpc`, `id`, and `result` (or `error`). Extra fields get the response rejected as malformed. See the [contract](/advanced/building-a-custom-rule-engine#the-four-responses).
</Warning>

## Step 2: Expose it over public HTTPS

The gateway calls your engine over the public internet, so a `localhost` URL won't register. Put a public HTTPS address in front of port 3000 — the fastest way is a tunnel:

```bash terminal theme={null}
ngrok http 3000
```

Copy the `https://…` forwarding URL ngrok prints. Your webhook is now reachable at `https://<your-tunnel>/inspect`. (Any host works — deploy the same file to a platform that gives you an HTTPS URL if you prefer.)

## Step 3: Register the engine

<Steps>
  <Step title="Add a Custom engine">
    Open **Rule Engines** in the left-hand navigation and click **Add**. Choose the **Custom** provider.
  </Step>

  <Step title="Set the endpoint and method">
    Set **Name** to `Email redactor`, the **Endpoint URL** to `https://<your-tunnel>/inspect`, and leave the **HTTP method** at **POST**.
  </Step>

  <Step title="Add the shared secret as a header">
    Under **Headers**, add `X-Api-Key` with the value you used for `RULE_ENGINE_SECRET` (for example `my-secret`). MCP Manager stores it encrypted and sends it on every call, so your webhook can authenticate the caller. Save.
  </Step>
</Steps>

## Step 4: Test the engine before wiring it up

Every engine has a **Test** action — use it to confirm your webhook answers correctly without touching a gateway.

<Steps>
  <Step title="Open Test">
    On the `Email redactor` engine, click **Test**. A modal opens with a sample-text box (up to 500 characters).
  </Step>

  <Step title="Enter text with an email">
    Type something like `Contact alice@example.com for details` and run it. The engine returns a **modify** verdict with the comment `redacted email address(es)`, and you see how long the call took. A `pass` verdict on text with no email confirms the other branch.
  </Step>
</Steps>

<Note>
  The Test runs **outside a live gateway session**, so it can't forward runtime identity headers. This engine doesn't use them, so Test reflects production behavior exactly. An engine that depends on [forwarded headers](/features/gateway-rules/custom-rules-engines#forwarding-runtime-headers) would behave differently under Test.
</Note>

## Step 5: Attach it to a gateway rule

<Steps>
  <Step title="Open the Rules tab">
    Open your gateway from [Gateways](https://app.mcpmanager.ai/settings/gateways), go to its **Rules** tab, and click **Add new rule**.
  </Step>

  <Step title="Select your engine as the detection method">
    Name the rule `Email redactor (custom)` and pick **Email redactor** from the **Detection method** dropdown — your registered engines appear below the two built-in methods. There's **no action picker**: the engine's verdict decides what happens.
  </Step>

  <Step title="Set the hook and failure mode">
    Set the **Detection hook** to **Response**, so the rule scans tool results. Leave the **Failure mode** at **Block** (the default for custom engines) so an unreachable engine fails closed. Turn **Alerts** on, confirm it's **Enabled**, and save.
  </Step>
</Steps>

## Step 6: Trigger it and read the verdict

Make a tool call whose result contains an email. With the docs server connected:

```text theme={null}
Search the MCP Manager docs for an example email address and quote the matching text exactly.
```

The result comes back through the gateway, your webhook redacts the address, and the client receives `[REDACTED EMAIL]` in place of it. Then open [Logs](https://app.mcpmanager.ai/settings/logging/logs), find the `tools/call`, and open its details:

* The entry is a `policy_enforced_mutation`.
* `rule_engine_type` reads `modify`, and `rule_engine_comment` shows `redacted email address(es)` — the `comment` your webhook returned.
* The **Body** shows the redacted text.

<Check>
  You wrote a webhook, exposed it over HTTPS, registered it as a custom engine, verified it with Test, attached it to a response-leg rule, and saw its `modify` verdict and comment in the audit trail. Your own code is now an in-line policy on the gateway — swap the email regex for any classifier, model call, or field-level logic you need.
</Check>

## Where to take it next

* **Return `block` or `error`.** Block a result outright, or signal that you couldn't decide and let the rule's [failure mode](/features/gateway-rules/overview#failure-mode-what-happens-when-a-detection-method-fails) take over.
* **Scope to the caller.** Turn on [header forwarding](/features/gateway-rules/custom-rules-engines#forwarding-runtime-headers) and filter results by the requesting user's identity.
* **Borrow a recipe.** The [custom rule engine examples](/advanced/custom-rule-engine-examples) cover slimming verbose responses, summarizing long text, and identity-scoped filtering.
* **Or skip the code.** [Amazon Bedrock](/features/amazon-bedrock) and [Lakera Guard](/features/lakera-guard) plug in as managed engines with no webhook to host.

## Further reading

<CardGroup cols={2}>
  <Card title="Building a Custom Rule Engine" icon="code" href="/advanced/building-a-custom-rule-engine">
    The full webhook contract — envelope, the four verdicts, types, and operational limits.
  </Card>

  <Card title="Custom Rule Engines" icon="plug" href="/features/gateway-rules/custom-rules-engines">
    Registering, configuring, testing, and deleting engines in the UI.
  </Card>

  <Card title="Custom Rule Engine Examples" icon="lightbulb" href="/advanced/custom-rule-engine-examples">
    Five runnable recipes for the modify and block verdicts.
  </Card>

  <Card title="Trace a call in your logs" icon="magnifying-glass" href="/tutorials/trace-in-logs">
    Read the rule-engine columns your verdict populated.
  </Card>
</CardGroup>
