PageSourceSearch

https://waxell.ai/docs/assets/js/fb0c2d88.8a8f3274.js

js waxell.ai collected 2026-09-28 06:46:36 UTC 27,389 bytes, 1 lines download raw bytes

1"use strict";(globalThis.webpackChunkwebsite=globalThis.webpackChunkwebsite||[]).push([[4146],{43914(e,n,r){r.r(n),r.d(n,{assets:()=>l,contentTitle:()=>d,default:()=>h,frontMatter:()=>s,metadata:()=>o,toc:()=>c});const o=JSON.parse('{"id":"observe/mcp-governance/middleware-per-tool","title":"Configure Governance Per Tool","description":"Configure governance differently for each tool in your FastMCP server using @governance decorators, constructor tool_configs, or server defaults.","source":"@site/docs/observe/mcp-governance/middleware-per-tool.md","sourceDirName":"observe/mcp-governance","slug":"/observe/mcp-governance/middleware-per-tool","permalink":"/docs/observe/mcp-governance/middleware-per-tool","draft":false,"unlisted":false,"editUrl":"https://gitlab.com/waxell/agentforge/-/edit/main/website/docs/observe/mcp-governance/middleware-per-tool.md","tags":[],"version":"current","frontMatter":{"sidebar_label":"Per-Tool Configuration","title":"Configure Governance Per Tool","description":"Configure governance differently for each tool in your FastMCP server using @governance decorators, constructor tool_configs, or server defaults.","keywords":["waxell","observe","mcp","governance","middleware","per-tool","configuration","rate limiting","pii","approval","fastmcp"]},"sidebar":"mcpGovernanceSidebar","previous":{"title":"Middleware Quickstart","permalink":"/docs/observe/mcp-governance/middleware-quickstart"},"next":{"title":"Connecting to Controlplane","permalink":"/docs/observe/mcp-governance/middleware-controlplane"}}');var i=r(74848),t=r(28453);const s={sidebar_label:"Per-Tool Configuration",title:"Configure Governance Per Tool",description:"Configure governance differently for each tool in your FastMCP server using @governance decorators, constructor tool_configs, or server defaults.",keywords:["waxell","observe","mcp","governance","middleware","per-tool","configuration","rate limiting","pii","approval","fastmcp"]},d="Configure Governance Per Tool",l={},c=[{value:"Prerequisites",id:"prerequisites",level:2},{value:"Three Ways to Configure",id:"three-ways-to-configure",level:2},{value:"Method 1: <code>@governance</code> Decorator",id:"method-1-governance-decorator",level:3},{value:"Method 2: <code>tool_configs</code> Constructor Argument",id:"method-2-tool_configs-constructor-argument",level:3},{value:"Method 3: Server Defaults via <code>policy_config</code>",id:"method-3-server-defaults-via-policy_config",level:3},{value:"PII Scan Modes",id:"pii-scan-modes",level:2},{value:"Rate Limiting",id:"rate-limiting",level:2},{value:"Approval Requirements",id:"approval-requirements",level:2},{value:"ToolGovernanceConfig Reference",id:"toolgovernanceconfig-reference",level:2},{value:"Combining All Three Methods",id:"combining-all-three-methods",level:2},{value:"Next Steps",id:"next-steps",level:2}];function a(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",mermaid:"mermaid",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,t.R)(),...e.components},{Details:r}=n;return r||function(e,n){throw new Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.header,{children:(0,i.jsx)(n.h1,{id:"configure-governance-per-tool",children:"Configure Governance Per Tool"})}),"\n",(0,i.jsxs)(n.p,{children:["Not every tool needs the same governance. A ",(0,i.jsx)(n.code,{children:"read_file"})," tool might need only basic PII scanning, while ",(0,i.jsx)(n.code,{children:"send_email"})," needs strict scanning and rate limiting, and ",(0,i.jsx)(n.code,{children:"delete_user"})," requires approval before execution. GovernanceMiddleware lets you configure governance per tool using three methods that layer on top of each other."]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"Who is this for?"})," MCP server builders who have added GovernanceMiddleware (from the ",(0,i.jsx)(n.a,{href:"./middleware-quickstart",children:"Middleware Quickstart"}),") and want to fine-tune governance for individual tools."]}),"\n",(0,i.jsx)(n.h2,{id:"prerequisites",children:"Prerequisites"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:(0,i.jsx)(n.strong,{children:"Python 3.10+"})}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"A Waxell API key"})," -- get one from your ",(0,i.jsx)(n.a,{href:"https://waxell.dev",children:"Waxell dashboard"})]}),"\n",(0,i.jsx)(n.li,{children:(0,i.jsx)(n.strong,{children:"waxell-observe with server extra installed:"})}),"\n"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:"pip install waxell-observe[mcp-server]\n"})}),"\n",(0,i.jsx)(n.h2,{id:"three-ways-to-configure",children:"Three Ways to Configure"}),"\n",(0,i.jsx)(n.p,{children:"GovernanceMiddleware resolves per-tool configuration in priority order:"}),"\n",(0,i.jsx)(n.mermaid,{value:'flowchart TD\n    A["Tool call arrives"] --\x3e B{"@governance\\ndecorator?"}\n    B --\x3e|Yes| C["Use decorator config"]\n    B --\x3e|No| D{"tool_configs\\nentry?"}\n    D --\x3e|Yes| E["Use constructor config"]\n    D --\x3e|No| F["Use server defaults"]\n\n    style 
1C fill:#1a7f5a,color:#fff\n    style E fill:#2563eb,color:#fff\n    style F fill:#7c3aed,color:#fff'}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Priority"}),(0,i.jsx)(n.th,{children:"Method"}),(0,i.jsx)(n.th,{children:"Best For"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"1 (highest)"}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"@governance"})," decorator"]}),(0,i.jsx)(n.td,{children:"Tool authors configuring their own tools inline"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"2"}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"tool_configs"})," constructor arg"]}),(0,i.jsx)(n.td,{children:"Operators configuring tools they didn't write"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"3"}),(0,i.jsxs)(n.td,{children:["Server defaults via ",(0,i.jsx)(n.code,{children:"policy_config"})]}),(0,i.jsx)(n.td,{children:"Baseline governance for all tools"})]})]})]}),"\n",(0,i.jsxs)(n.p,{children:["The first match wins. If a tool has both a ",(0,i.jsx)(n.code,{children:"@governance"})," decorator and a ",(0,i.jsx)(n.code,{children:"tool_configs"})," entry, the decorator config is used."]}),"\n",(0,i.jsxs)(n.h3,{id:"method-1-governance-decorator",children:["Method 1: ",(0,i.jsx)(n.code,{children:"@governance"})," Decorator"]}),"\n",(0,i.jsxs)(n.p,{children:["Apply the ",(0,i.jsx)(n.code,{children:"@governance"})," decorator directly on your tool function. This is the most natural approach when you're the tool author and want governance config to live alongside the tool code."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-python",children:'from fastmcp import FastMCP\nfrom waxell_observe.mcp import GovernanceMiddleware, governance\n\nmcp = FastMCP("acme-server")\nmcp.add_middleware(GovernanceMiddleware(api_key="wax_sk_..."))\n\n\n@governance(pii_scan="strict", rate_limit={"max_per_minute": 10})\[email protected]\nasync def send_email(to: str, subject: str, body: str) -> str:\n    """Send an email to a recipient."""\n    return f"Email sent to {to}: {subject}"\n'})}),"\n",(0,i.jsxs)(n.p,{children:["The decorator accepts all ",(0,i.jsx)(n.code,{children:"ToolGovernanceConfig"})," fields as keyword arguments:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-python",children:'@governance(\n    approval_required=True,\n    pii_scan="strict",\n    policy_action="warn",\n    rate_limit={"max_per_minute": 5, "max_per_hour": 50},\n)\n'})}),"\n",(0,i.jsx)(n.admonition,{title:"Decorator order",type:"tip",children:(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"@governance"})," decorator works above or below ",(0,i.jsx)(n.code,{children:"@mcp.tool"})," -- both orderings are supported. By convention, place ",(0,i.jsx)(n.code,{children:"@governance"})," above ",(0,i.jsx)(n.code,{children:"@mcp.tool"}),"."]})}),"\n",(0,i.jsxs)(n.h3,{id:"method-2-tool_configs-constructor-argument",children:["Method 2: ",(0,i.jsx)(n.code,{children:"tool_configs"})," Constructor Argument"]}),"\n",(0,i.jsxs)(n.p,{children:["Pass a dictionary of ",(0,i.jsx)(n.code,{children:"ToolGovernanceConfig"})," objects to the ",(0,i.jsx)(n.code,{children:"GovernanceMiddleware"})," constructor. This is ideal when you're an operator configuring tools you didn't write, or when you want all configuration in one place."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-python",children:'from waxell_observe.mcp import GovernanceMiddleware, ToolGovernanceConfig\n\nmcp.add_middleware(\n    GovernanceMiddleware(\n        api_key="wax_sk_...",\n        tool_configs={\n            "send_email": ToolGovernanceConfig(\n                pii_scan="strict",\n                approval_required=True,\n                rate_limit={"max_per_minute": 10},\n            ),\n            "delete_user": ToolGovernanceConfig(\n                approval_required=True,\n                pii_scan="strict",\n                policy_action="block",\n                rate_limit={"max_per_minute": 5},\n            ),\n            "query_database": ToolGovernanceConfig(\n                pii_scan="standard",\n                rate_limit={"max_per_hour": 100},\n            ),\n        },\n    )\n)\n'})}),"\n",(0,i.jsxs)(n.p,{children:["The keys in ",(0,i.jsx)(n.code,{children:"tool_configs"})," are tool names -- they must match the function name registered with FastMCP."]}
1),"\n",(0,i.jsxs)(n.h3,{id:"method-3-server-defaults-via-policy_config",children:["Method 3: Server Defaults via ",(0,i.jsx)(n.code,{children:"policy_config"})]}),"\n",(0,i.jsxs)(n.p,{children:["Set baseline governance for all tools using the ",(0,i.jsx)(n.code,{children:"policy_config"})," dictionary. Any tool without a ",(0,i.jsx)(n.code,{children:"@governance"})," decorator or ",(0,i.jsx)(n.code,{children:"tool_configs"})," entry inherits these defaults."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-python",children:'mcp.add_middleware(\n    GovernanceMiddleware(\n        api_key="wax_sk_...",\n        policy_config={\n            "default_pii_scan": "standard",\n            "default_policy_action": "allow",\n            "fail_open": True,\n        },\n    )\n)\n'})}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Field"}),(0,i.jsx)(n.th,{children:"Type"}),(0,i.jsx)(n.th,{children:"Default"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"default_pii_scan"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"str"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:'"standard"'})}),(0,i.jsx)(n.td,{children:"Default PII scanning mode for unconfigured tools"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"default_policy_action"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"str"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:'"allow"'})}),(0,i.jsx)(n.td,{children:"Default policy action for unconfigured tools"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"fail_open"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"bool"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"True"})}),(0,i.jsxs)(n.td,{children:["If ",(0,i.jsx)(n.code,{children:"True"}),", governance errors let tool calls proceed. If ",(0,i.jsx)(n.code,{children:"False"}),", governance errors block tool calls."]})]})]})]}),"\n",(0,i.jsx)(n.h2,{id:"pii-scan-modes",children:"PII Scan Modes"}),"\n",(0,i.jsx)(n.p,{children:"Each tool can use a different PII scanning mode:"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Mode"}),(0,i.jsx)(n.th,{children:"Behavior"}),(0,i.jsx)(n.th,{children:"When to Use"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:'"none"'})}),(0,i.jsx)(n.td,{children:"No PII scanning"}),(0,i.jsx)(n.td,{children:"Internal tools with no user data"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:'"standard"'})}),(0,i.jsxs)(n.td,{children:["Scan and ",(0,i.jsx)(n.strong,{children:"warn"})," on all PII types"]}),(0,i.jsx)(n.td,{children:"Most tools -- visibility without blocking"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:'"strict"'})}),(0,i.jsxs)(n.td,{children:["Scan and ",(0,i.jsx)(n.strong,{children:"block"})," on all PII types"]}),(0,i.jsx)(n.td,{children:"Tools that handle sensitive data (email, payments, exports)"})]})]})]}),"\n",(0,i.jsxs)(n.p,{children:["PII types detected by the default ",(0,i.jsx)(n.code,{children:"RegexPIIScanner"}),":"]}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Type"}),(0,i.jsx)(n.th,{children:"Examples"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"SSN"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"123-45-6789"})})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"Credit card"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"4111-1111-1111-1111"})})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"Email"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"[email protected]"})})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"Phone"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"(555) 123-4567"})})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"AWS access key"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"AKIAIOSFODNN7EXAMPLE"})})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"AWS secret key"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"})})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"API key"}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"sk-abc123..."}),", ",(0,i.jsx)(n.code,{children:"wax_sk_..."})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"Private key"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"-----BEGIN RSA PRIVATE KEY-----"})})]})]})]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"Input PII scanning"})," runs before the tool executes. In ",(0,i.jsx)(n.code,{children:"strict"})," mode, a blocked PII type stops the call with a ",(0,i.jsx)(n.code,{children:"ToolError"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"Output PII scanning"})," runs after the tool executes. Output findings are always warnings -- the tool has already run, so blocking would discard valid results."]}),"\n",(0,i.jsx)(n.admonition,{title:"Custom PII scanner",type:"tip",children:(0,i.jsxs)(n.p,{children:["You can provide your own PII scanner by passing a ",(0,i.jsx)(n.code,{children:"pii_scanner"})," argument to ",(0,i.jsx)(n.code,{children:"GovernanceMiddleware"}),". The scanner must implement the ",(0,i.jsx)(n.code,{children:"PIIScanner"})," protocol (a ",(0,i.jsx)(n.code,{children:"scan(text: str) -> dict"})," method). See the ",(0,i.jsx)(n.a,{href:"./middleware-reference",children:"Middleware Reference"})," for details."]})}),"\n",(0,i.jsx)(n.h2,{id:"rate-limiting",children:"Rate Limiting"}),"\n",(0,i.jsx)(n.p,{children:"Configure per-tool rate limits to prevent abuse. Rate limits use a sliding window algorithm:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-python",children:'@governance(\n    rate_limit={\n        "max_per_minute": 10,    # Max 10 calls per 60-second window\n        "max_per_hour": 100,     # Max 100 calls per 3600-second window\n    },\n)\[email protected]\nasync def send_email(to: str, subject: str, body: str) -> str:\n    """Send an email to a recipient."""\n    return f"Email sent to {to}: {subject}"\n'})}),"\n",(0,i.jsxs)(n.p,{children:["When a rate limit is exceeded, the middleware raises a ",(0,i.jsx)(n.code,{children:"ToolError"})," with the message ",(0,i.jsx)(n.code,{children:"\"Rate limit exceeded for tool 'send_email'\""}
1)," and records ",(0,i.jsx)(n.code,{children:"waxell.mcp.rate_limited: true"})," on the span."]}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Field"}),(0,i.jsx)(n.th,{children:"Type"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"max_per_minute"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"int"})}),(0,i.jsx)(n.td,{children:"Maximum calls within a 60-second sliding window"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"max_per_hour"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"int"})}),(0,i.jsx)(n.td,{children:"Maximum calls within a 3600-second sliding window"})]})]})]}),"\n",(0,i.jsx)(n.p,{children:"Both fields are optional. You can set one or both. The stricter limit applies when both are configured."}),"\n",(0,i.jsx)(n.admonition,{title:"Rate limits are per-server-process",type:"info",children:(0,i.jsx)(n.p,{children:"Rate limits are tracked in memory per server process. If you run multiple server instances behind a load balancer, each instance maintains its own counters. For global rate limiting across instances, use controlplane policies instead."})}),"\n",(0,i.jsx)(n.h2,{id:"approval-requirements",children:"Approval Requirements"}),"\n",(0,i.jsx)(n.p,{children:"Mark tools that need human approval before execution:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-python",children:'@governance(approval_required=True)\[email protected]\nasync def delete_user(user_id: str) -> str:\n    """Permanently delete a user account."""\n    return f"User {user_id} deleted"\n'})}),"\n",(0,i.jsxs)(n.p,{children:["When ",(0,i.jsx)(n.code,{children:"approval_required=True"}),", the middleware records ",(0,i.jsx)(n.code,{children:"waxell.mcp.approval_required: true"})," on the span. The actual approval workflow is managed by the Waxell controlplane -- see ",(0,i.jsx)(n.a,{href:"./middleware-controlplane",children:"Connecting to Controlplane"})," for setup."]}),"\n",(0,i.jsx)(n.h2,{id:"toolgovernanceconfig-reference",children:"ToolGovernanceConfig Reference"}),"\n",(0,i.jsxs)(n.p,{children:["All fields available on ",(0,i.jsx)(n.code,{children:"ToolGovernanceConfig"})," (used by both ",(0,i.jsx)(n.code,{children:"@governance"})," decorator and ",(0,i.jsx)(n.code,{children:"tool_configs"}),"):"]}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Field"}),(0,i.jsx)(n.th,{children:"Type"}),(0,i.jsx)(n.th,{children:"Default"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"approval_required"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"bool"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"False"})}),(0,i.jsx)(n.td,{children:"Whether the tool call requires approval before execution"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"pii_scan"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"str"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:'"standard"'})}),(0,i.jsxs)(n.td,{children:["PII scanning mode: ",(0,i.jsx)(n.code,{children:'"none"'}),", ",(0,i.jsx)(n.code,{children:'"standard"'}),", or ",(0,i.jsx)(n.code,{children:'"strict"'})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"policy_action"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"str | None"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"None"})}),(0,i.jsxs)(n.td,{children:["Override policy action: ",(0,i.jsx)(n.code,{children:'"allow"'}),", ",(0,i.jsx)(n.code,{children:'"block"'}),", or ",(0,i.jsx)(n.code,{children:'"warn"'}),". ",(0,i.jsx)(n.code,{children:"None"})," uses the server default."]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"rate_limit"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"dict | None"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"None"})}),(0,i.jsxs)(n.td,{children:["Rate limit config: ",(0,i.jsx)(n.code,{children:'{"max_per_minute": N, "max_per_hour": N}'}),". ",(0,i.jsx)(n.code,{children:"None"})," means no rate limit."]})]})]})]}),"\n",(0,i.jsx)(n.h2,{id:"combining-all-three-methods",children:"Combining All Three Methods"}),"\n",(0,i.jsx)(n.p,{children:"In practice, you'll often use all three configuration methods together. Here's a realistic example:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-python",children:'from fastmcp import FastMCP\nfrom waxell_observe.mcp import GovernanceMiddleware, ToolGovernanceConfig, governance\n\nmcp = FastMCP("acme-server")\n\n# Server defaults: scan everything, allow by default\nmcp.add_middleware(\n    GovernanceMiddleware(\n        api_key="wax_sk_...",\n        policy_config={\n            "default_pii_scan": "standard",\n            "default_policy_action": "allow",\n            "fail_open": True,\n        },\n        # Operator overrides for tools they didn\'t write\n        tool_configs={\n            "query_database": ToolGovernanceConfig(\n                rate_limit={"max_per_hour": 500},\n            ),\n        },\n    )\n)\n\n\n# Tool author sets strict governance inline\n@governance(pii_scan="strict", approval_required=True, rate_limit={"max_per_minute": 5})\[email protected]\nasync def send_email(to: str, subject: str, body: str) -> str:\n    """Send an email to a recipient."""\n    return f"Email sent to {to}: {subject}"\n\n\n# This tool inherits from tool_configs (rate limit from operator)\[email protected]\nasync def query_database(sql: str) -> str:\n    """Execute a read-only SQL query."""\n    return f"Results for: {sql}"\n\n\n# This tool inherits server defaults (standard PII scan, allow policy)\[email protected]\nasync def read_file(path: str) -> str:\n    """Read a file from the filesystem."""\n    return f"Contents of {path}"\n'})}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.strong,{children:"Resolution for each tool:"})}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Tool"}),(0,i.jsx)(n.th,{children:"Config Source"}),(0,i.jsx)(n.th,{children:"PII Scan"}),(0,i.jsx)(n.th,{children:"Rate Limit"}),(0,i.jsx)(n.th,{children:"Approval"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"send_email"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"@governance"})," decorator"]}),(0,i.jsx)(n.td,{children:"strict"}),(0,i.jsx)(n.td,{children:"5/min"}),(0,i.jsx)(n.td,{children:"Yes"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"query_database"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"tool_configs"})," constructor"]}),(0,i.jsx)(n.td,{children:"standard (default)"}),(0,i.jsx)(n.td,{children:"500/hr"}),(0,i.jsx)(n.td,{children:"No"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"read_file"})}),(0,i.jsx)(n.td,{children:"Server defaults"}),(0,i.jsx)(n.td,{children:"standard"}),(0,i.jsx)(n.td,{children:"None"}),(0,i.jsx)(n.td,{children:"No"})]})]})]}),"\n",(0,i.jsxs)(r,{children:[(0,i.jsx)("summary",{children:"Full runnable example with mixed per-tool configuration"}),(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-python",children:'from fastmcp import FastMCP\nfrom waxell_observe.mcp import GovernanceMiddleware, ToolGovernanceConfig, governance\n\n# Create server\nmcp = FastMCP("acme-server")\n\n# Configure middleware with all three methods\nmcp.add_middleware(\n    GovernanceMiddleware(\n        api_key="wax_sk_...",\n        api_url="https://acme.waxell.dev",\n        # Method 3: Server defaults\n        policy_config={\n            "default_pii_scan": "standard",\n            "default_policy_action": "allow",\n            "fail_open": True,\n        },\n        # Method 2: Operator overrides\n        tool_configs={\n            "query_database": ToolGovernanceConfig(\n                pii_scan="standard",\n                rate_limit={"max_per_hour": 500},\n            ),\n            "delete_user": ToolGovernanceConfig(\n                approval_required=True,\n                pii_scan="strict",\n                policy_action="block",\n                rate_limit={"max_per_minute": 3},\n            ),\n        },\n    )\n)\n\n\n# Method 1: Tool author sets governance inline\n@governance(\n    pii_scan="strict",\n    approval_required=True,\n    rate_limit={"max_per_minute": 5, "max_per_hour": 50},\n)\[email protected]\nasync def send_email(to: str, subject: str, body: str) -> str:\n    """Send an email to a recipient."""\n    return f"Email sent to {to}: {subject}"\n\n\n# Configured via tool_configs (Method 2)\[email protected]\nasync def query_database(sql: str) -> str:\n    """Execute a read-only SQL query against the analytics database."""\n    return f"Results for: {sql}"\n\n\n# Configured via tool_configs (Method 2)\[email protected]\nasync def delete_user(user_id: str) -> str:\n    """Permanently delete a user account and all associated data."""\n    return f"User {user_id} deleted"\n\n\n# Inherits server defaults (Method 3)\[email protected]\nasync def read_file(path: str) -> str:\n    """Read a file from the filesystem."""\n    return f"Contents of {path}"\n\n\n# Inherits server defaults (Method 3)\[email protected]\nasync def list_users() -> str:\n    """List all users in the system."""\n    return "user1, user2, user3"\n\n\nif __name__ == "__main__":\n    mcp.run()\n'})})]}),"\n",(0,i.jsx)(n.h2,{id:"next-steps",children:"Next Steps"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:(0,i.jsx)(n.a,{href:"./middleware-controlplane",children:"Connecting to Controlplane"})})," -- Set up centralized policy management so you can change governance rules without redeploying your server"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:(0,i.jsx)(n.a,{href:"./middleware-reference",children:"Middleware Reference"})})," -- Complete API reference for GovernanceMiddleware, ToolGovernanceConfig, MiddlewareConfig, and all span attributes"]}),"\n"]})]})}function h(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(a,{...e})}):a(e)}},28453(e,n,r){r.d(n,{R:()=>s,x:()=>d});var o=r(96540);const i={},t=o.createContext(i);function s(e){const n=o.useContext(t);return o.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:s(e.components),o.createElement(t.Provider,{value:n},e.children)}}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.