Motivation
LibreFang's MCP taint scanner protects against credential/PII exfiltration through tool calls. Today the only server-level knob is:
[mcp_servers.my_server]
taint_scanning = false # all-or-nothing
This forces operators into an uncomfortable trade-off: if any tool on a given MCP server produces false positives (e.g. a browser-automation server whose navigate tool returns opaque tab handles that look like tokens), the only recourse is to disable scanning for the entire server — losing protection for every other tool on that server.
The existing taint_policy field already supports per-tool, per-path rule exemptions (skipping individual TaintRuleIds like opaque_token or sensitive_key_name for specific JSONPath expressions). That mechanism is powerful but has several gaps:
- No tool-level default action — there is no way to say "skip scanning entirely for tool X, but keep scanning everything else on this server" without enumerating every argument path of tool X.
- No path-pattern wildcards — exemption keys are exact JSONPath strings; glob-style patterns like
$.items[*] partially work via prefix matching but are not documented or tested as a first-class feature.
- No named, reusable rule sets — each server's
taint_policy is self-contained. There is no way to define a shared named rule (with a custom severity action) and reference it from multiple servers or tools.
- No dashboard support — the taint policy is config-only; the dashboard has no UI for viewing or editing per-tool/per-path exemptions.
Proposed Design
1. Tool-level default action
Add a default field to McpTaintToolPolicy that controls what happens to arguments not matched by any path rule:
[mcp_servers.camofox.taint_policy.tools.navigate]
default = "skip" # skip all taint scanning for this tool
# OR
default = "scan" # (current implicit default) scan everything
This lets operators exempt a noisy tool with a single line rather than pattern-matching every argument path.
Rust shape (additive, non-breaking):
pub struct McpTaintToolPolicy {
/// "scan" (default) or "skip" — applies to paths not covered by `paths`.
#[serde(default)]
pub default: McpTaintToolAction,
pub paths: HashMap<String, McpTaintPathPolicy>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum McpTaintToolAction { #[default] Scan, Skip }
2. Path-pattern wildcards
Document and test glob-style wildcards in path exemption keys:
[mcp_servers.camofox.taint_policy.tools.read_file.paths]
"$.content" = { skip_rules = ["opaque_token"] }
"$.metadata.*" = { skip_rules = ["sensitive_key_name"] }
$.metadata.* should exempt any immediate child key of metadata (e.g. $.metadata.etag, $.metadata.x-request-id). This already partially works via prefix matching in resolve_skip_rules but is undocumented and has no glob semantics for [*] within arrays.
3. Named, reusable taint rule sets
Add a top-level [[taint_rules]] table in config.toml for defining named rule groups with a severity action:
[[taint_rules]]
name = "browser_handles"
action = "skip" # skip, warn, or block (default: block)
rules = ["opaque_token"] # TaintRuleId variants to match
[[taint_rules]]
name = "pii_relaxed"
action = "warn" # log but don't block
rules = ["pii_email", "pii_phone"]
Reference from per-tool policy:
[mcp_servers.camofox.taint_policy.tools.navigate]
rule_sets = ["browser_handles"] # apply named sets on top of path rules
The action field extends the current binary block/allow into three tiers:
block — current behaviour: abort the tool call and return an error to the LLM.
warn — emit a structured log event at WARN level but allow the call through.
log / audit — emit at INFO level, allow through, useful for building baselines before enabling enforcement.
4. Dashboard tree editor
Add a taint policy panel to the MCP server configuration page in the React dashboard. The panel should render a three-level tree:
▼ camofox (MCP server)
▼ navigate (tool) [default: scan ▾]
$.tabId skip: opaque_token [edit] [×]
$.sessionId skip: opaque_token [edit] [×]
[+ add path exemption]
▼ read_file (tool) [default: scan ▾]
[+ add path exemption]
[+ add tool policy]
Requirements for the UI:
- Query the existing
/api/agents / config endpoints (or a new /api/mcp/servers endpoint if needed) to populate server and tool lists.
- Mutations must call
invalidateQueries with factory keys per the dashboard data-layer rule.
- All state goes through hooks in
src/lib/queries/ and src/lib/mutations/; no inline fetch() in page components.
- Rule IDs (
TaintRuleId variants) are shown as human-readable labels with a tooltip explaining what each rule catches.
What Already Exists
For context, the following is already in tree and working:
McpTaintPolicy { tools: HashMap<String, McpTaintToolPolicy> } — per-tool map.
McpTaintToolPolicy { paths: HashMap<String, McpTaintPathPolicy> } — per-path map within a tool.
McpTaintPathPolicy { skip_rules: Vec<TaintRuleId> } — rule-level exemption at a specific path.
TaintRuleId enum with nine variants: AuthorizationLiteral, KeyValueSecret, WellKnownPrefix, OpaqueToken, PiiEmail, PiiPhone, PiiCreditCard, PiiSsn, SensitiveKeyName.
resolve_skip_rules() in librefang-runtime-mcp applies path policies during scanning.
The proposals above are additive on top of this foundation and are backward-compatible (all new fields have #[serde(default)]).
Acceptance Criteria
Open Questions
- Should
warn/log severity actions write to a dedicated audit log table (SQLite via librefang-memory) so operators can replay what would have been blocked? Or is structured tracing output sufficient for an MVP?
- Should the dashboard tree editor support drag-and-drop reordering of rule sets, or is alphabetical display sufficient?
- Is there appetite for a
dry_run = true server-level flag that applies warn to all rules globally, to help operators build exemption lists before going to block?
Motivation
LibreFang's MCP taint scanner protects against credential/PII exfiltration through tool calls. Today the only server-level knob is:
This forces operators into an uncomfortable trade-off: if any tool on a given MCP server produces false positives (e.g. a browser-automation server whose
navigatetool returns opaque tab handles that look like tokens), the only recourse is to disable scanning for the entire server — losing protection for every other tool on that server.The existing
taint_policyfield already supports per-tool, per-path rule exemptions (skipping individualTaintRuleIds likeopaque_tokenorsensitive_key_namefor specific JSONPath expressions). That mechanism is powerful but has several gaps:$.items[*]partially work via prefix matching but are not documented or tested as a first-class feature.taint_policyis self-contained. There is no way to define a shared named rule (with a custom severity action) and reference it from multiple servers or tools.Proposed Design
1. Tool-level default action
Add a
defaultfield toMcpTaintToolPolicythat controls what happens to arguments not matched by any path rule:This lets operators exempt a noisy tool with a single line rather than pattern-matching every argument path.
Rust shape (additive, non-breaking):
2. Path-pattern wildcards
Document and test glob-style wildcards in path exemption keys:
$.metadata.*should exempt any immediate child key ofmetadata(e.g.$.metadata.etag,$.metadata.x-request-id). This already partially works via prefix matching inresolve_skip_rulesbut is undocumented and has no glob semantics for[*]within arrays.3. Named, reusable taint rule sets
Add a top-level
[[taint_rules]]table inconfig.tomlfor defining named rule groups with a severity action:Reference from per-tool policy:
The
actionfield extends the current binary block/allow into three tiers:block— current behaviour: abort the tool call and return an error to the LLM.warn— emit a structured log event at WARN level but allow the call through.log/audit— emit at INFO level, allow through, useful for building baselines before enabling enforcement.4. Dashboard tree editor
Add a taint policy panel to the MCP server configuration page in the React dashboard. The panel should render a three-level tree:
Requirements for the UI:
/api/agents/ config endpoints (or a new/api/mcp/serversendpoint if needed) to populate server and tool lists.invalidateQuerieswith factory keys per the dashboard data-layer rule.src/lib/queries/andsrc/lib/mutations/; no inlinefetch()in page components.TaintRuleIdvariants) are shown as human-readable labels with a tooltip explaining what each rule catches.What Already Exists
For context, the following is already in tree and working:
McpTaintPolicy { tools: HashMap<String, McpTaintToolPolicy> }— per-tool map.McpTaintToolPolicy { paths: HashMap<String, McpTaintPathPolicy> }— per-path map within a tool.McpTaintPathPolicy { skip_rules: Vec<TaintRuleId> }— rule-level exemption at a specific path.TaintRuleIdenum with nine variants:AuthorizationLiteral,KeyValueSecret,WellKnownPrefix,OpaqueToken,PiiEmail,PiiPhone,PiiCreditCard,PiiSsn,SensitiveKeyName.resolve_skip_rules()inlibrefang-runtime-mcpapplies path policies during scanning.The proposals above are additive on top of this foundation and are backward-compatible (all new fields have
#[serde(default)]).Acceptance Criteria
McpTaintToolPolicygains adefault: McpTaintToolActionfield (scan|skip);skipbypasses scanning for the tool without requiring path entries..*,[*]) are documented inMcpTaintPathPolicyrustdoc and covered by unit tests.[[taint_rules]]config table added withname,action(block|warn|log), andrulesfields;McpTaintToolPolicygains arule_setsfield referencing them by name.cargo clippy --workspace --all-targets -- -D warningspasses with zero new warnings.Open Questions
warn/logseverity actions write to a dedicated audit log table (SQLite vialibrefang-memory) so operators can replay what would have been blocked? Or is structuredtracingoutput sufficient for an MVP?dry_run = trueserver-level flag that applieswarnto all rules globally, to help operators build exemption lists before going toblock?