Policy recipe library

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 bare amount) 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 gt need both sides numeric. For a list of line items, $.items[*].qty gt 1000 fires 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 readOnlyHint matches 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 with approve rather 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.”