Skip to content

Guardrails

Every MCP tool call passes through a guardrail evaluation layer. This layer, called the Policy Enforcement Point (PEP) and Policy Decision Point (PDP), evaluates your configured rules deterministically against the tool call’s arguments before the call runs, and returns a decision: allow, deny, require approval, or mask fields from the result. An escalate rule is graded after the call returns, against the arguments and the result.

Guardrails are a deterministic safety mechanism between the agent and your MCP servers. They are not probabilistic and not model-based. Every rule is a deterministic condition that either matches or does not match.

The primary safety layer sits below the guardrails you author: an assurance gate derived from each tool’s risk classification. It runs on every call to a tool of your MCP connections, before any of your guardrail rules, and it is described first below. The guardrail rules you configure (deny, approval gate, mask, escalate) layer on top of it.

When the agent calls a tool, MCP hub evaluates the call in this order:

  1. Account isolation. Hard deny if the connection does not belong to the requesting account.
  2. Agent assignment. A custom agent reaches only the connections assigned to it, and on each one the tools its assignment names, or every tool when the assignment names none. Any other call is denied with policy_denied.
  3. Rate limit check. Deny with rate_limited if the account has exceeded the rate limit for this tool. A tool set below its default assurance also has a per-conversation limit, and a call over it is denied with rate_limited.
  4. Assurance gate. Resolve the tool’s required assurance level from its risk classification. An unclassified tool is hard-denied, and so is a tool with no scope declaration, with policy_denied. A tool declared A customer or A customer’s company is denied with scope_unverified until the caller has verified. A call below the required assurance level is denied with assurance_too_low, or with unauthenticated when it carries no identity of the caller at all. See “Risk and the assurance gate” below.
  5. Scope check. For a tool declared at the record level, every value in its declared reference arguments must be a record this conversation created, or the call is denied with scope_unbound. After the call returns, an answer that names a record or a customer the caller does not hold is withheld with scope_mismatch.
  6. Guardrail rules. Evaluate all enabled guardrails that match the tool name. Deny rules take precedence over all other types (forbid-overrides-permit).
  7. Default risk policy. If no approval_gate guardrail matched and the tool is classified as destructive, require approval automatically.
  8. Default allow. If nothing blocked the call, allow it.

When a condition evaluated before the call cannot be evaluated (malformed condition, missing field, unexpected type), the system denies the call. This is fail-closed by design.

Each guardrail has a policyType that determines what happens when the rule’s condition matches.

Policy typeExecutionBehavior
denyBlockedTool call is rejected immediately. The agent receives an error.
approval_gateBlockedTool call is paused. An approval request is created and the conversation workflow waits for a human to approve or reject.
maskAllowedTool call executes normally. Specified fields are redacted from the result before it reaches the agent.
escalateAllowedTool call executes normally. After it returns, the condition is graded against the call arguments and the result. When it matches a successful answer, the agent that called the tool receives an escalation_signal with the answer: its type is the guardrail’s escalationType, or policy_escalation when it has none, and its description is the guardrail’s description, or escalate when it has none.

Fields designated for eID verification as the person identifier or the national identity number are redacted the same way on every tool of their connection, automatically. See Returning a national identity number.

When multiple guardrails match the same tool call, the evaluation follows forbid-overrides-permit: a single deny match blocks the call regardless of any other matching rule. An approval_gate match blocks the call even if other rules would allow it.

Guardrail endpoints take a dashboard session or an API key: mcp:read to list, mcp:write to create, change or delete (from a dashboard session, an owner or admin), within the key’s brand or workspace restriction. See MCP Connections. Guardrails are scoped to an MCP connection: each guardrail applies to a specific tool (or all tools) on a specific connection.

Returns all guardrails for a connection.

Terminal window
curl -X GET https://api.ohallo.eu/api/mcp-connections/{connectionId}/guardrails \
-H "Authorization: Bearer <token>"
[
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"tenantId": "5b6c7d8e-9f01-4234-8567-89abcdef0123",
"mcpConnectionId": "c9876543-dcba-4edc-ba98-765432109876",
"toolName": "submit_return",
"policyType": "approval_gate",
"config": {
"condition": "args.refund_amount > 500",
"description": "Require approval for returns over 500 EUR"
},
"enabled": true,
"createdAt": "2026-04-10T14:30:00.000Z",
"updatedAt": "2026-04-10T14:30:00.000Z"
}
]

Creates a new guardrail on a connection. If a condition is provided, it is validated as a filtrex expression at creation time. Invalid expressions return a 422 error.

Terminal window
curl -X POST https://api.ohallo.eu/api/mcp-connections/{connectionId}/guardrails \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"toolName": "submit_return",
"policyType": "approval_gate",
"condition": "args.refund_amount > 500",
"config": {
"description": "Require approval for returns over 500 EUR"
},
"enabled": true
}'

Request body:

FieldTypeRequiredDescription
toolNamestringYesThe tool this guardrail applies to. Use * to match all tools on the connection.
policyTypestringYesOne of: approval_gate, deny, mask, escalate.
conditionstringNoA filtrex expression evaluated against the tool call arguments, or for escalate type against the arguments and the result after the call. If omitted, the guardrail matches every call to the tool.
configobjectNoAdditional configuration. Use description to explain the rule, maskFields for mask type, escalationType for escalate type, as the type of its escalation_signal.
enabledbooleanNoDefaults to true. Set to false to disable the guardrail without deleting it.

Response (201):

{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"tenantId": "5b6c7d8e-9f01-4234-8567-89abcdef0123",
"mcpConnectionId": "c9876543-dcba-4edc-ba98-765432109876",
"toolName": "submit_return",
"policyType": "approval_gate",
"config": {
"condition": "args.refund_amount > 500",
"description": "Require approval for returns over 500 EUR"
},
"enabled": true,
"createdAt": "2026-04-10T14:30:00.000Z",
"updatedAt": "2026-04-10T14:30:00.000Z"
}

Error (422), invalid condition:

{
"error": "Invalid condition expression",
"details": "Parse error on line 1:\nargs.refund_amount >> 500\n--------------------^\nExpecting '-', 'not', 'if', '(', 'Number', 'Symbol', 'String', got '>'"
}

Updates an existing guardrail. Only the fields you include in the request body are changed.

Terminal window
curl -X PATCH https://api.ohallo.eu/api/mcp-call-guardrails/{guardrailId} \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"condition": "args.refund_amount > 1000",
"config": {
"description": "Require approval for returns over 1000 EUR"
}
}'

Request body (all fields optional):

FieldTypeDescription
toolNamestringChange the tool this guardrail applies to.
policyTypestringChange the policy type.
conditionstringChange the condition expression. Validated at update time.
configobjectReplace the configuration object. The condition is stored in it, so send condition with config to keep a condition.
enabledbooleanEnable or disable the guardrail.

Response (200): The updated guardrail object.

Error (404): Guardrail not found or does not belong to your account.

Terminal window
curl -X DELETE https://api.ohallo.eu/api/mcp-call-guardrails/{guardrailId} \
-H "Authorization: Bearer <token>"

Response (200):

{
"ok": true
}

Replaces all guardrails for a connection at once, in one transaction: the existing guardrails on the connection are deleted and the provided list is inserted, each with its condition inside config. A save that fails leaves the previous guardrails in place, and two saves to the same connection run one after the other. This is the endpoint the oHallo dashboard uses when saving guardrail configuration.

Terminal window
curl -X PUT https://api.ohallo.eu/api/mcp-connections/{connectionId}/policies \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"guardrails": [
{
"toolName": "submit_return",
"policyType": "approval_gate",
"config": {
"condition": "args.refund_amount > 500",
"description": "Require approval for returns over 500 EUR"
},
"enabled": true
},
{
"toolName": "delete_customer",
"policyType": "deny",
"config": {
"description": "Never allow customer deletion via the agent"
}
}
]
}'

Response (200): The full list of created guardrail objects.

Guardrail conditions are filtrex expressions, evaluated against the tool call arguments. An escalate condition is evaluated after the call, against the arguments and the result. In it, result is the MCP result your tool returned, so the tool’s data is at result.structuredContent.<field>, as in result.structuredContent.status == "shipped".

The oHallo dashboard provides a visual condition builder that compiles to filtrex automatically. Operators enter a field name, select an operator, and type a value, and the args. prefix and expression syntax are handled by the UI. The API accepts the compiled filtrex expression directly.

Conditions access tool call arguments through the args namespace using dot notation:

ExpressionDescription
args.refund_amount > 500Matches when the refund amount exceeds 500
args.quantity > 100Matches when quantity exceeds 100
args.country == "US"Matches when the country is US
args.priority == "critical"Matches when priority is critical

You can combine conditions with logical operators:

OperatorExample
andargs.refund_amount > 500 and args.currency == "EUR"
orargs.priority == "critical" or args.priority == "urgent"
notnot (args.is_verified == 1)

Comparisons: ==, !=, <, >, <=, >=.

Arithmetic: +, -, *, /.

If a guardrail has no condition (the field is omitted), it matches every call to the specified tool. Use this for blanket rules like “never allow deletion” or “always require approval for this tool.”

If a deny, approval_gate or mask condition references a field that does not exist in the tool call arguments, or if the expression is malformed, the evaluation fails and the tool call is denied. This is fail-closed: a broken condition blocks execution rather than silently allowing it.

An escalate condition is graded after the call has run, so one that cannot be evaluated adds no escalation_signal to the answer.

The assurance gate is the primary safety layer. It runs on every tool call, before any guardrail rule you author. The tool’s risk classification supplies its default assurance level, and an administrator sets the level per tool, above or below that default. Two bounds hold whatever is configured: a destructive tool is never anonymous, and a tool lowered below identified needs a declared scope.

Every tool on an MCP connection has a risk classification. There are five:

ClassificationMeaning
readPublic or non-personal data.
read_piiReturns personal data.
writeState-changing action.
write_piiState-changing action that also touches personal data. Handled the same as write; the distinct value exists for audit clarity.
destructiveIrreversible or financially material.

The classification sets a default assurance level

Section titled “The classification sets a default assurance level”

Each risk classification carries a default assurance level. A call whose resolved assurance is below the level the tool requires is denied with assurance_too_low. The defaults:

ClassificationDefault assurance
readanonymous (no identification required)
read_piiidentified
writeidentified
write_piiidentified
destructivehigh-assurance

The level a tool requires is set per tool, in either direction. Raising it is a plain write. A level below the default is set on the dashboard, where an administrator acknowledges the change and the row shows Below default; a reclassification that drops the level a tool enforces takes the same acknowledgement. Raise the assurance one tool requires covers the endpoint and its refusals. The same resolution runs in MCP hub, the API and the dashboard: what is displayed is what is enforced.

If a tool has no risk classification, it is hard-denied on every call and is non-callable until you classify it. An unknown capability is treated as maximum risk. There is no “no classification, so allow” fallback and no default to write. Classify every tool before assigning it to an agent, and declare whose records it touches (Declare the scope of each tool): a tool with no scope declaration is hard-denied the same way.

Terminal window
curl -X PATCH https://api.ohallo.eu/api/mcp-connections/{connectionId}/tool-risk-classifications \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"classifications": {
"get_order": "read",
"get_customer": "read_pii",
"update_order": "write",
"delete_order": "destructive",
"submit_return": "write",
"cancel_subscription": "destructive"
}
}'

Response (200): The full connection object with the updated classifications.

Risk classifications and guardrails work together. The assurance gate runs first: an unclassified tool is denied, and a call below the assurance the tool requires is denied, before any guardrail is consulted. A read tool with a deny guardrail is still blocked when the condition matches. A destructive tool requires approval on every call: through a matching approval_gate guardrail when one matches, and through the default otherwise, unless a matching deny guardrail blocks it.

When a tool call triggers an approval_gate guardrail (or the default destructive-tool policy), the following sequence occurs:

The agent receives a response indicating that the tool call needs human approval:

{
"success": false,
"error_type": "approval_required",
"reason": "Require approval for returns over 500 EUR",
"policy_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"approval_request_id": "0a1b2c3d-4e5f-4a6b-8c7d-8e9f0a1b2c3d",
"expires_at": "2026-04-11T14:30:00.000Z",
"policy_decision": "require_approval"
}

The conversation workflow creates an attention item visible in the oHallo dashboard and waits for a human decision. The customer is not sent a reply while approval is pending.

A team member reviews the pending tool call in the dashboard. They see the tool name, its arguments and the reason, which is the description of the guardrail that triggered. They approve or reject via the dashboard UI or the API:

Terminal window
curl -X POST https://api.ohallo.eu/api/conversations/{conversationId}/approve \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"approvalRequestId": "0a1b2c3d-4e5f-4a6b-8c7d-8e9f0a1b2c3d",
"approved": true,
"reviewerNote": "Confirmed with warehouse, return is valid"
}'

Request body:

FieldTypeRequiredDescription
approvalRequestIdstringYesThe ID from the approval_required response.
approvedbooleanYestrue to approve, false to reject.
reviewerNotestringNoOptional note explaining the decision. Stored for audit.

Response (200):

{
"ok": true
}

After approval, the conversation workflow resumes. The agent retries the tool call with the same arguments. MCP hub finds the approved request record and allows the call through without triggering the guardrail again.

After rejection, the workflow resumes and the agent is informed that the action was denied. It composes a response to the customer accordingly.

Approval requests expire after 24 hours. If no decision is made within that window, the request expires and the tool call does not run.

If the agent retries a tool call that already has a pending approval request with the same arguments, MCP hub returns the existing request instead of creating a duplicate. Arguments are matched by a SHA-256 hash of the sorted argument keys and values.

Prevent the agent from ever calling delete_customer, regardless of arguments:

Terminal window
curl -X POST https://api.ohallo.eu/api/mcp-connections/{connectionId}/guardrails \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"toolName": "delete_customer",
"policyType": "deny",
"config": {
"description": "Customer deletion is never allowed via the agent"
}
}'

Require human approval for refunds over 500 EUR:

Terminal window
curl -X POST https://api.ohallo.eu/api/mcp-connections/{connectionId}/guardrails \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"toolName": "submit_return",
"policyType": "approval_gate",
"condition": "args.refund_amount > 500",
"config": {
"description": "Require approval for returns over 500 EUR"
}
}'

Allow the tool call but redact national identity numbers and bank account details from the result:

Terminal window
curl -X POST https://api.ohallo.eu/api/mcp-connections/{connectionId}/guardrails \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"toolName": "get_customer",
"policyType": "mask",
"config": {
"description": "Redact sensitive financial data",
"maskFields": ["national_id", "bank_account", "credit_card"]
}
}'

Allow the tool call, and give the agent that called it an escalation_signal with a successful answer for an order above 10,000 EUR:

Terminal window
curl -X POST https://api.ohallo.eu/api/mcp-connections/{connectionId}/guardrails \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"toolName": "create_order",
"policyType": "escalate",
"condition": "args.total > 10000",
"config": {
"description": "Order above 10,000 EUR",
"escalationType": "high_value_order"
}
}'
  • Build an MCP Server: create the server your guardrails will protect
  • Tool Schema: define the input schemas that guardrail conditions evaluate against