MCP Policy Files¶
This page documents the YAML schema consumed by assay mcp wrap.
Assay policy files decide:
- which MCP tools are allowed or denied
- how tool arguments are validated with JSON Schema
- which tools need extra controls such as approval, scope restriction, or argument redaction
Supported Versions¶
version: "2.0": current format, with per-tool JSON Schema underschemas:version: "1.0": legacyconstraints:format
Assay still reads v1 policies, warns once, and can migrate them with assay policy migrate.
Minimal v2 Policy¶
version: "2.0"
name: "starter"
tools:
allow: ["read_file", "list_dir"]
deny: ["exec", "shell", "write_file"]
schemas:
read_file:
type: object
additionalProperties: false
properties:
path:
type: string
pattern: "^/workspace/.*"
minLength: 1
maxLength: 4096
required: ["path"]
list_dir:
type: object
additionalProperties: false
properties:
path:
type: string
pattern: "^/workspace/.*"
minLength: 1
maxLength: 4096
required: ["path"]
enforcement:
unconstrained_tools: warn
Top-Level Fields¶
| Field | Type | Meaning |
|---|---|---|
version | string | Policy schema version. Use "2.0" for new files. |
name | string | Optional human-readable label. |
tools | object | Allow/deny lists and obligation controls. |
allow / deny | list | Legacy aliases merged into tools.allow / tools.deny on load. |
schemas | map | JSON Schema per tool. $defs is reserved for shared definitions. |
constraints | list | Legacy v1 regex constraints. Deprecated. |
enforcement | object | What to do with allowed tools that have no schema. |
limits | object | Optional request and tool-call ceilings. |
signatures | object | Optional tool-description integrity checks. |
tool_pins | map | Cryptographic pins for expected tool identity. |
discovery | object | Advanced runtime discovery settings. |
runtime_monitor | object | Advanced runtime monitoring rules. |
kill_switch | object | Advanced kill-switch triggers. |
Unknown fields are ignored with a warning, so it is worth keeping this page and your checked-in policies aligned.
tools: Fields¶
The tools section handles both filtering and extra controls.
| Field | Type | Meaning |
|---|---|---|
allow | list | Allowed tool names or wildcard patterns. |
deny | list | Blocked tool names or wildcard patterns. |
allow_classes | list | Allow by tool taxonomy class. |
deny_classes | list | Deny by tool taxonomy class. |
approval_required | list | Tools that require a valid approval artifact. |
approval_required_classes | list | Approval requirement by tool class. |
restrict_scope | list | Tools whose arguments must match a scope contract. |
restrict_scope_classes | list | Scope restriction by tool class. |
restrict_scope_contract | object | Shared contract used for restrict_scope. |
redact_args | list | Tools whose arguments should be redacted. |
redact_args_classes | list | Redaction by tool class. |
redact_args_contract | object | Shared contract used for redact_args. |
Wildcards¶
Assay uses simple * wildcards, and the language has exactly five forms:
"*"matches all tools"read_*"matches by prefix"*_file"matches by suffix"*search*"matches by substring- patterns without
*are exact matches
In the four wildcard forms a * is unbounded: it crosses any character, including the . or __ a server may use to namespace its tools. "*" therefore admits tools that do not exist yet, including ones a future upstream adds.
A * that is neither the first nor the last character is a literal asterisk. There is no fifth wildcard form. read_*_file does not match read_config_file; it matches only a tool literally named read_*_file. The same goes for several interior stars, such as a*b*c, and for interior stars inside the substring form: *a*b* searches for the three characters a*b.
That one fails in the dangerous direction. In allow, an interior-star pattern matches nothing and the tool is refused, which is visible. In deny, it matches nothing and the tool is permitted -- by a line whose author believed it was blocking something. If a deny entry looks like a glob and is not one of the five forms above, it is not blocking what it names.
For the four forms that do expand, the risk is one-sided and a policy file does not show it. A wildcard in deny over-blocks, which fails visibly and safely. The same wildcard in allow over-permits, which fails silently. Azure RBAC measured this direction: about half of the 15,481 catalogued actions reach across Resource Providers under non-obvious wildcards, and the recommendation there was explicit enumeration rather than a more careful pattern language (arXiv 2506.10755v3). Prefer naming the tools you mean in allow, and keep "*" for the cases where admitting the unknown is the intent rather than the default.
Two corners, so they are stated rather than discovered. "**" (and any longer run of stars) is the substring form with an empty substring, so it matches everything, exactly as "*" does — it is not the mandate surface's crossing operator. An empty pattern "" has no star, so it is an exact match against the empty name and matches no real tool.
A different pattern language applies to mandate
tool_patterns, where*stops at a.and**crosses it, the way a filesystem glob treats/. The two are deliberately different and pinned against each other by a test; do not carry a pattern from one surface to the other without re-reading it.
JSON Schema in schemas:¶
Each tool can have a full JSON Schema for its argument object:
schemas:
create_ticket:
type: object
additionalProperties: false
properties:
title:
type: string
minLength: 5
maxLength: 120
priority:
type: string
enum: ["low", "medium", "high"]
labels:
type: array
items:
type: string
maxLength: 32
required: ["title", "priority"]
Recommended defaults for security-sensitive tools:
additionalProperties: falseminLength: 1on required strings- explicit
required: [...] - bounded arrays and strings
If a call violates the schema, Assay denies it with E_ARG_SCHEMA.
Shared Definitions With $defs¶
Assay supports local shared definitions via $defs. Use #/$defs/... references inside tool schemas. File and HTTP references are not retrieved during evaluation. Bundle external definitions into the policy document before execution.
schemas:
$defs:
safe_path:
type: string
pattern: "^/workspace/.*"
minLength: 1
maxLength: 4096
read_file:
type: object
additionalProperties: false
properties:
path:
$ref: "#/$defs/safe_path"
required: ["path"]
Enforcement¶
Allowed tools can still be considered unsafe if you do not attach a schema. enforcement.unconstrained_tools decides what happens then:
Supported values:
warn: allow the tool, but emitE_TOOL_UNCONSTRAINEDdeny: block allowed tools that have no schemaallow: silently allow unconstrained tools
Limits¶
Exceeding these limits produces E_RATE_LIMIT.
Approval, Scope Restriction, and Redaction¶
These controls sit next to ordinary allow/deny rules:
tools:
allow: ["read_file", "deploy_release", "create_ticket"]
approval_required: ["deploy_release"]
restrict_scope: ["read_file"]
redact_args: ["create_ticket"]
restrict_scope_contract:
scope_type: "path_prefix"
scope_value: "/workspace"
scope_match_mode: "prefix"
redact_args_contract:
redaction_target: "args"
redaction_mode: "mask"
redaction_scope: "sensitive_fields"
Use these when you want:
- explicit human approval for risky tools
- runtime enforcement that file or resource arguments stay in-bounds
- redaction of secrets before downstream logging or evidence export
When an approval_required obligation is evaluated from a supplied _meta.approval artifact, the decision event also carries a digest/profile for that structured approval artifact. This binds the projected approval summary fields to the retained _meta.approval basis under assay.approval_artifact.structured_meta_jcs.v0. It is not a rendered UI view claim, not raw byte retention, and not proof of what a user saw or intended.
Tool Pins¶
tool_pins protect against tool-definition drift by pinning the expected server, tool name, and hashes:
tool_pins:
read_file:
server_id: "filesystem-prod"
tool_name: "read_file"
# Replace these example values with the full 64-character lowercase SHA-256 hashes.
schema_hash: "9f4d4d0f9f4d4d0f9f4d4d0f9f4d4d0f9f4d4d0f9f4d4d0f9f4d4d0f9f4d4d0f"
meta_hash: "42f5df3e42f5df3e42f5df3e42f5df3e42f5df3e42f5df3e42f5df3e42f5df3e"
schema_hash is the hex SHA-256 of the RFC 8785 (JCS) canonical bytes of the tool's inputSchema, or of null when the tool declares none. meta_hash is the hex SHA-256 of the description bytes verbatim, or of the empty string when there is none. Because schema_hash is taken over canonical bytes, a server that reorders its keys or re-emits 100 as 1e2 does not drift; a changed schema does, with one exception.
RFC 8785 renders every number as an IEEE 754 double, so two numeric literals with the same double value become one pin. Below 2^53 that is only a spelling difference and there is nothing to detect. Above it, two literals can be different integers and still share a double, and then the pin stops seeing a real edit: a schema changed only from {"maximum": 9007199254740993} to {"maximum": 9007199254740992} keeps the same schema_hash while admitting a different set of arguments, because a validator still reads the two literals as different integers. A tool_pins entry does not detect that edit.
The exception is a property of the number, not of the keyword, so do not read any keyword list as the boundary. It applies wherever such a pair of integers is compared against an argument for order or identity — maximum, minimum, exclusiveMaximum, exclusiveMinimum, const and enum are where it is measured to bite today, and that is an example set, not a limit. It is bounded on the other side: past the 64-bit integer range the two literals stop being distinguishable anywhere, so they are one value rather than a missed edit.
An argument constraint written in this policy is unaffected, because it is validated from the schema you wrote here and not from the one the server declares.
Legacy v1 Compatibility¶
Legacy v1 policies use constraints::
version: "1.0"
deny: ["exec"]
constraints:
- tool: "read_file"
params:
path:
matches: "^/workspace/.*"
Assay loads this shape, warns, normalizes allow / deny into tools.*, and auto-migrates constraints into in-memory JSON Schemas.
To write the v2 form to disk:
To fail CI if deprecated constructs are still present: