MCP Response Redaction

Overview

When you connect sqry to a cloud-hosted AI assistant, MCP tool responses may contain absolute file paths, source code snippets, documentation strings, and workspace layout details. The sqry-mcp-redaction library lets you filter these before they reach the model.

The library ships as a standalone crate and can be used client-side or integrated into the MCP server pipeline. It uses a whitelist-first security model: every field in a JSON response is redacted unless it appears on an explicit allow list. This inverts the usual “block known-bad” approach — unknown fields are redacted by default, so new tool outputs are safe without configuration changes.

Status: The redaction library is fully implemented and tested but not yet wired into the MCP server by default. Configure it via environment variables when launching sqry-mcp.


Presets

Four presets cover common deployment scenarios. Set the preset with SQRY_REDACTION_PRESET:

PresetPathsCodeDocsFilenamesUse case
noneTrusted local tools (Claude Code, local Codex)
minimalredactedkeptkeptkeptCloud LLMs that need code context
standardredactedredactedkeptkeptCloud LLMs, code is confidential (default)
strictredactedredactedredactedhashedUntrusted external services

What gets redacted

Path data (P0)

Absolute paths, file URIs, workspace root paths, Windows paths, and UNC network paths are all detected and redacted. The engine also scans arbitrary string fields for embedded paths using pattern matching:

/home/user/project/src/main.rs      → src/main.rs
file:///home/user/project/src/lib.rs → src/lib.rs
C:\Users\dev\project\src\app.ts     → src/app.ts
\\server\share\project\README.md     → README.md

When SQRY_REDACT_WORKSPACE is set, the workspace root itself (e.g., /srv/repos/internal/acme/backend) is stripped from all responses.

Security hardening: The path canonicalizer rejects null bytes, control characters, paths longer than 4,096 characters, Windows device paths (\\.\COM1), and UNC escape attempts.

Source code (P1)

Code context blocks — function bodies, snippets, and source excerpts — are replaced with a line-count placeholder:

fn main() {              →  [REDACTED: 3 lines of code]
    println!("Hello");
}

The detector recognizes common language patterns (fn, func, def, class, import, var, let, etc.) to distinguish code from plain text.

Documentation

Doc comments, docstrings, and inline documentation are replaced with [REDACTED: documentation]. The detector matches ///, //!, /**, @param, @returns, Python docstrings, and similar patterns.

Unknown fields

In whitelist mode (the default for all presets except none), any JSON field not on the whitelist is redacted. This means new tool outputs or unexpected fields are safe by default.


Whitelists

Each preset defines which response fields pass through unmodified:

Always preserved (all presets):

Minimal adds: code, code_context, snippet, source_code, documentation, doc, docstring, comment, results, items, data, symbols, from, to

Standard adds documentation fields but not code context fields.

Strict allows only minimal semantic, position, and structural fields.

You can extend any preset with SQRY_WHITELIST_FIELDS:

export SQRY_WHITELIST_FIELDS="my_custom_field,another_field"

Configuration

All settings are controlled via environment variables. Set them when launching sqry-mcp:

# Use the standard preset (default)
SQRY_REDACTION_PRESET=standard sqry-mcp

# Override individual toggles
SQRY_REDACTION_PRESET=minimal \
  SQRY_REDACT_CODE=1 \
  sqry-mcp

Environment variables

VariableValuesDefaultDescription
SQRY_REDACTION_PRESETnone, minimal, standard, strictstandardBase preset
SQRY_REDACT_PATHS0 / 1presetStrip absolute paths
SQRY_REDACT_WORKSPACE0 / 1presetStrip workspace root from all strings
SQRY_REDACT_URIS0 / 1presetStrip file:// URIs
SQRY_REDACT_CODE0 / 1presetReplace code blocks with line-count placeholder
SQRY_REDACT_DOCS0 / 1presetReplace documentation strings
SQRY_REDACT_PATTERNS0 / 1presetDetect and redact paths embedded in strings
SQRY_HASH_FILENAMES0 / 10Replace filenames with SHA-256 hashes
SQRY_HASH_SALTstringrandomSalt for filename hashing (set for deterministic output)
SQRY_WORKSPACE_ROOTpathautoWorkspace root for relative-path conversion
SQRY_WHITELIST_FIELDSCSVpresetAdditional fields to preserve
SQRY_PRESERVE_PATHSCSVJSONPath expressions for fields to never redact

Individual toggles override the preset. For example, SQRY_REDACTION_PRESET=minimal SQRY_REDACT_CODE=1 uses the minimal preset but also redacts code.


JSONPath rules

For surgical control, use JSONPath expressions via SQRY_PRESERVE_PATHS to exempt specific fields from redaction, regardless of other rules:

# Always preserve metadata, even in strict mode
export SQRY_PRESERVE_PATHS="$.result.metadata,$..summary"

Supported syntax:

PatternMeaning
$.fieldRoot-level field
$.a.b.cNested path
$..fieldRecursive descent (any depth)
$[0]Array index
$[*]All array elements
$[0,1,2]Multiple indices

Example: Claude Desktop with redaction

{
  "mcpServers": {
    "sqry": {
      "command": "/usr/local/bin/sqry-mcp",
      "env": {
        "SQRY_MCP_WORKSPACE_ROOT": "/home/dev/project",
        "SQRY_REDACTION_PRESET": "standard",
        "SQRY_REDACT_WORKSPACE": "1"
      }
    }
  }
}

This configuration gives Claude access to all 33 MCP tools while stripping absolute paths, workspace root, and source code from every response. Symbol names, kinds, positions, relationships, and documentation pass through — enough for the model to reason about code structure without seeing the code itself.


Redaction statistics

Every redacted response includes a statistics summary (logged at debug level):

{
  "paths_redacted": 12,
  "uris_redacted": 3,
  "code_contexts_redacted": 5,
  "docs_redacted": 2,
  "pattern_paths_redacted": 1,
  "unknown_fields_redacted": 0,
  "workspace_path_redacted": true
}

Use this to audit what’s being filtered and tune your preset.


Programmatic API

For custom integrations, use the Rust API directly:

use sqry_mcp_redaction::{Redactor, RedactionConfig};

// From environment variables
let redactor = Redactor::from_env()?;

// Or from a preset
let redactor = Redactor::new(RedactionConfig::standard())?;

// Redact in-place
let stats = redactor.redact(&mut json_value);

// Redact a clone (original preserved)
let (redacted, stats) = redactor.redact_clone(&json_value);

// Dry-run preview
let preview = redactor.preview(&json_value);

// Stream processing for large responses
let stats = redactor.redact_stream(reader, writer)?;