How a policy reaches a verdict
A policy is an ordered list of rules. Every tool call an agent makes runs down that list, and the first rule that matches decides the call — allow it, deny it, or hold it for a human’s approve. Rules carry a priority and evaluate lowest-first, so specific exceptions go near the top and broad rules below. A project always has one default policy at the very bottom; it decides anything nothing else matched, so no call ever falls through unanswered.
Each rule matches on some combination of these dimensions. Leave one out and it means “any”:
- Agent — named agents, or any agent carrying a matching tag.
- Server and tool — a whole upstream server, or specific tools, optionally narrowed by a name glob (
delete_*). - Arguments — the actual values in the call, addressed by path (
$.amount,$.items[*].qty, or a bareamount) with an operator:eq,neq,gt,gte,lt,lte,matches,contains,exists,in. - Annotation hints — the read-only / destructive / idempotent / open-world flags the upstream server declared for a tool, matched exactly as declared.
- Caller IP (CIDR) and request headers.
The recipes below are written in those terms — a match, an optional condition on the call, and an action. There is no separate policy language to learn; each recipe maps directly onto the fields in the Add policy dialog.
Recipe 1 — Block every delete
Goal: no agent can ever call a destructive delete operation.
When a server exposes one tool per operation, match the tool name with a glob and deny it. Globs are anchored to the whole name, so delete_* matches delete_user but not bulk_delete_user.
Action: deny
Match: tool_name = "delete_*"
Priority: high (near the top)
Some servers instead collapse a whole API into one generic write tool and put the real operation in an argument (Stripe’s hosted MCP does this — the operation rides in stripe_api_operation_id). There, match on the argument instead, using matches with a regular expression:
Action: deny
Tool: stripe_api_write
Condition: $.stripe_api_operation_id matches "^Delete"
Only one of
matches‘s value is a regular expression; every other name field in the system is a glob. Anchor it (^Delete) so it means what you drew.
Recipe 2 — Require approval for large amounts
Goal: a person signs off before any refund or payment over a threshold runs.
Because the call’s arguments are structured, a rule can compare the real parameter value. Match the tool, add a numeric condition on the amount, and set the action to approve — the call pauses mid-flight, the owner is notified, and it only runs upstream once a human approves. Deny or let it time out and it never runs; either way the decision is logged against this rule.
Action: approve
Tool: refund
Condition: $.amount gt 10000
Put the broad “let ordinary refunds through” rule below this one, so the threshold rule wins first:
1. refund, $.amount gt 10000 → approve
2. everything else → allow
Ordering operators like
gtneed both sides numeric. For a list of line items,$.items[*].qty gt 1000fires when any item exceeds — the intuitive reading for a deny or an approval. That “any” reading is the wrong direction for an allow, so write allows against a specific index instead.
Recipe 3 — Make a server read-only
Goal: agents may read from a server but never write to it.
Meandr surfaces the read-only / destructive hints a server declares for each of its tools and matches them exactly as declared. Scope a rule to the whole server, allow the tools the server marked read-only, and let a server-scoped deny catch the rest:
1. server = "reports-mcp", hints.read_only = true → allow
2. server = "reports-mcp" → deny
Hints are matched verbatim: a tool that never declared
readOnlyHintmatches nothing, so the deny in rule 2 catches it. That is deliberate — Meandr never invents a hint the server didn’t send. Hints are self-declared and untrusted by the MCP spec, so for a hard guarantee pair the allow withapproverather than trusting the flag alone.
Recipe 4 — Cap how fast a project or agent can call
Goal: brake a runaway integration without touching individual rules.
Rate limits are set as windows — a count over a duration from one second up to an hour — and they compose across scopes. You can cap a whole project, a single agent, or a specific upstream server, on top of the plan-wide ceiling every tenant carries. The limits are enforced accurately across the entire proxy fleet, so a 60-per-minute cap means 60 per minute total, not 60 on each node.
Scope: agent "batch-importer"
Window: 200 calls / 60s
Rate limits are a per-scope control, not a policy rule and not per individual tool. The scopes are tenant (your plan), project, agent, and server — nothing finer. To throttle a specific tool, deny or approve it with a policy instead.
Recipe 5 — Reject calls that don’t fit the tool’s schema
Goal: malformed calls never reach the upstream at all.
Every tool ships an inputSchema. Meandr can validate the agent’s arguments against that schema and refuse a non-conforming call before it leaves the gateway — the wrong type, or a missing required field, is turned back at the door rather than forwarded. Because the upstream is never reached, a rejected call isn’t billed.
Validation is only as strict as the upstream’s own schema. Many tools omit
additionalProperties: false, so schema validation catches type garbage and missing requireds but won’t reject an unexpected extra key unless the schema forbids it. For value-level guarantees, add an argument condition (Recipe 2) on top.
The order is the policy
Since the first match wins, the arrangement is the behaviour. A good default shape is: specific denies at the top (Recipes 1, 3), approvals for the risky middle ground (Recipe 2), a few explicit allows, and a catch-all default at the bottom. If a rule ever fails to compile, Meandr excludes just that rule and reports it, rather than taking the whole project’s traffic down.
Keep reading
- Write your first policy — the three moves every policy makes, and what happens when a rule says “require approval.”
- MCP governance — why argument-level policy beats a blind HTTP filter, and how it solves the coarse-tool problem.
- Argument-level policy vs allowlists — why matching the real call beats an allowlist that can’t tell “update a product” from “delete an account.”