Skip to main content

Sluice

The MCP gate that remembers what your agent already saw.

PyPI version


Agents do not just answer questions. They reach into files, databases, APIs, and inboxes through MCP. Each hop is another place data can end up where you did not intend.

Sluice is the gate on that path. It reads what goes out to tools and what comes back, applies your rules, and keeps a short memory of sensitive values for the rest of the conversation. If the model tries to ship the same value somewhere new, the gate stays shut.

How it works

Every JSON-RPC message passes through one pipeline: recall check → detectors → policy → audit → router.

flowchart LR
    subgraph client [Your machine]
        A[Cursor / Claude / curl]
    end

    subgraph sluice [Sluice process]
        T[Transport\nstdio or HTTP]
        P[Pipeline\nscan + policy + taint]
        R[Router\npick upstream]
        D[Dashboard /_sluice/]
        DB[(audit.db)]
    end

    subgraph tools [MCP servers]
        F[filesystem]
        G[github]
        M[mock / others]
    end

    A -->|JSON-RPC| T
    T --> P
    P -->|allow| R
    P -->|block| T
    P --> DB
    R --> F
    R --> G
    R --> M
    F --> R
    G --> R
    M --> R
    R --> P
    P --> T
    T --> A
    D --> DB
Component Role
config.yaml Upstreams, policy rules, taint, audit path
Transport stdio for desktop clients; HTTP for servers and Docker
Pipeline Taint recall, detector scan, policy action, audit write
Router Forwards allowed messages to the right MCP upstream
Detectors Secrets, PII, tool poisoning, prompt injection
Taint store Remembers sensitive values per session; blocks reuse (taint_leak)
Audit / dashboard SQLite log + browser UI at /_sluice/

Desktop (stdio): Cursor → sluice stdio → MCP server child process · Server (HTTP): curl / agent → :4444 → pipeline → upstream MCP

What this is (and what it is not)

Most MCP security tools work like a mailbox scanner. They open one envelope, look for a credit card or API key, and decide whether to let that single message through.

Sluice works like a gate with memory. It still scans every message, but it also remembers what sensitive data already entered the session. The second question it asks is different: has this exact value already been inside the conversation, and is it now trying to leave through another tool?

That second question is the whole point. A secret inside a read_file response can be legitimate. The same secret inside a later send_email call is a leak. Sluice blocks the second moment, not the first.

This is not a hosted service, not an LLM judge, and not a replacement for identity or network access control. It is a local runtime that sits on the MCP wire, enforces YAML policy, writes an audit log to disk, and fronts multiple tool servers from one config.

A concrete case

Your agent calls read_file and the response includes AKIAIOSFODNN7EXAMPLE. That is fine. The file really contained it.

Two turns later it calls send_email and the body includes the same string. Sluice has no opinion about the first call. It stops the second one because the value already appeared inside this session and is trying to travel again.

Sluice blocking a cross-tool leak

Run it yourself: bash scripts/demo.sh

v0.3.0

New in this release:

  • Policy presets for filesystem, github, slack, postgres, brave-search (sluice presets)
  • Read-only HTML dashboard at /_sluice/
  • Docker image (Dockerfile) for HTTP deployments
  • Integration tests against a mock MCP server in CI
  • Taint v2: JSON-path provenance + propagation graph in audit log
  • Prompt-injection detector on tool responses
  • Optional OpenTelemetry exporter (pip install sluice-taint[otel])
  • Full streamable HTTP SSE reconnect with Last-Event-Id

Full setup and validation guide: docs/guide.md

Demo

bash scripts/demo.sh

Shows Part 1 (secret blocked on outbound) and Part 2 (read_file → send_email blocked by taint_leak). See the video above, or docs/demo-recording.md to record your own.

Docker (HTTP mode)

sluice init
# edit config.yaml — set audit.sqlite.path to /var/lib/sluice/audit.db for persistence

docker compose up --build
# Sluice on http://localhost:4444, dashboard at http://localhost:4444/_sluice/

Pre-built images (after tag push): ghcr.io/krishyaid-coder/sluice:latest

docker run --rm -p 4444:4444 \
  -v "$PWD/config.yaml:/etc/sluice/config.yaml:ro" \
  -v sluice-audit:/var/lib/sluice \
  ghcr.io/krishyaid-coder/sluice:latest serve --config /etc/sluice/config.yaml --host 0.0.0.0

Desktop clients (Cursor, Claude) use pip install sluice-taint and sluice stdio, not Docker.

Pseudonymize instead of block

For PII, a third policy action beyond block and redact: pseudonymize. The AI sees stable placeholders like EMAIL_A instead of the real values. When the AI later issues a tool call using those placeholders, Sluice substitutes real values back before the call reaches the tool server.

policy:
  rules:
    - detector: "pii.*"
      action: "pseudonymize"

pseudonym:
  enabled: true
  fail_closed_on_reverse: true

Round trip in one sentence: real values on your disk and on the wire, stable pseudonyms in the AI's context. See docs/design/pseudonymization.md for the full behavior including limitations.

v0.1.0

This release ships:

  • stdio and HTTP transports with streamable HTTP (MCP 2025-03-26) for remote upstreams
  • Carryover memory across tool calls (taint_leak)
  • Secret, PII, and tool-poisoning detectors
  • Per-upstream and per-tool YAML policy
  • SQLite audit log and sluice logs
  • CLI: init, serve, stdio, logs, doctor, version
  • Legacy config migration to config.yaml.upgraded
  • ${VAR} expansion in config values

Quick demo script:

bash scripts/demo.sh

Performance on a developer laptop (pipeline only, 1 KB messages):

clean  p50=0.02 ms   p95=0.02 ms
secret p50=0.61 ms   p95=0.85 ms

Reproduce with python -m sluice.bench.latency.

Install

pip install sluice-taint

PyPI package: sluice-taint. The CLI command is still sluice.

Get started

sluice init
sluice serve

The proxy listens on 127.0.0.1:4444 by default. Edit config.yaml before you point anything real at it.

From source

git clone https://github.com/krishyaid-coder/sluice
cd sluice
pip install -e ".[dev]"
sluice init
sluice serve

Hook up Claude Desktop or Cursor

Run Sluice as the MCP command instead of the tool server directly:

{
  "mcpServers": {
    "filesystem": {
      "command": "sluice",
      "args": ["stdio", "--config", "/absolute/path/to/config.yaml"]
    }
  }
}

Sluice spawns the upstream named in your config and speaks MCP on stdin/stdout.

Prove it works

With default policy, this should come back as a JSON-RPC error:

curl -s -X POST http://127.0.0.1:4444 \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "write_file",
      "arguments": {"content": "key=AKIAIOSFODNN7EXAMPLE"}
    }
  }' | jq

Running multiple tool servers

HTTP mode can front more than one upstream. Send traffic to POST /u/<upstream-name> or POST / for the default route in config.yaml.

Tooling

sluice init scaffolds config.

sluice serve starts the HTTP gate.

sluice stdio bridges one upstream over stdio for desktop clients.

sluice logs --since 1h prints recent decisions from the local SQLite log.

See what Sluice caught

For a summary of what happened in a session — sensitive values seen, where they went, what got blocked — use:

sluice report

Or for a specific session:

sluice report --session sess-a3f9b2c1

Add --format markdown to paste into a doc, or --format json to pipe.

sluice doctor loads your config and reports obvious problems.

sluice version prints the build.

sluice presets list|show|apply manages bundled policy presets.

Open the dashboard at http://127.0.0.1:4444/_sluice/ while sluice serve is running.

Rules and outcomes

You declare what to do when a pattern fires: refuse the message (block), strip the match and continue (redact), or allow it but remember the value for later (flag).

Built-in matchers know common API keys, tokens, PEM blocks, email and phone shapes, cards and government IDs, noisy high-entropy strings, and suspicious text hiding inside tools/list payloads.

When a remembered value shows up again in an outbound call, the refusal reason is taint_leak.

Everything runs on your machine. Audit rows land in ~/.sluice/audit.db unless you point them elsewhere.

Architecture

See Architecture.md for the full request path and module map.

License

Apache 2.0

Metadata

Release files for sluice-taint 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sluice-taint 0.4.0
File Size Uploaded Provenance Actions
sluice_taint-0.4.0.tar.gz 2.3 MB Without provenance Details for sluice_taint-0.4.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for sluice-taint 0.4.0
File Interpreter ABI Platform Actions
sluice_taint-0.4.0-py3-none-any.whl Python 3 none any Details for sluice_taint-0.4.0-py3-none-any.whl

Total release size: 2.4 MB

Release files / sluice_taint-0.4.0.tar.gz

Download URL sluice_taint-0.4.0.tar.gz
Size 2.3 MB
Tags Source
SHA-256 checksum
How to use checksums
5aea4c0528167347c8b30848d015f25f17358b5c9e4143d9fef8d115e3c2fdd5
BLAKE2b-256 checksum
How to use checksums
1f50171c2b016b5c71afb47bf029b57ef9f7ab930bf9793fd5a746e8d9eb6585
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.7

Release files / sluice_taint-0.4.0-py3-none-any.whl

Download URL sluice_taint-0.4.0-py3-none-any.whl
Size 60.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7aa8b6baf2affce5cb04383e46ae5dba054b79d3b3c16b00b49c0686f8a767cc
BLAKE2b-256 checksum
How to use checksums
360c8c51a8dc052294139953ba253c1acd483f9e8be716883f5a8fce94d74d85
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.7

Security for sluice-taint 0.4.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page