mcp-sluice 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. mcp_sluice-0.1.0/.github/workflows/ci.yml +62 -0
  2. mcp_sluice-0.1.0/.github/workflows/release.yml +103 -0
  3. mcp_sluice-0.1.0/.gitignore +11 -0
  4. mcp_sluice-0.1.0/.hypothesis/.gitignore +9 -0
  5. mcp_sluice-0.1.0/.hypothesis/constants/03a92850ec05bf96 +4 -0
  6. mcp_sluice-0.1.0/.hypothesis/constants/28ed97e6b732c25b +4 -0
  7. mcp_sluice-0.1.0/.hypothesis/constants/2ce49e5fccb99c6e +4 -0
  8. mcp_sluice-0.1.0/.hypothesis/constants/2d25ca6f46e04685 +4 -0
  9. mcp_sluice-0.1.0/.hypothesis/constants/36d619bad1bddc4e +4 -0
  10. mcp_sluice-0.1.0/.hypothesis/constants/43910582fbcf1b5d +4 -0
  11. mcp_sluice-0.1.0/.hypothesis/constants/44302d81fc4ea7ad +4 -0
  12. mcp_sluice-0.1.0/.hypothesis/constants/5b63440f1497986b +4 -0
  13. mcp_sluice-0.1.0/.hypothesis/constants/7483b75bcf1c815e +4 -0
  14. mcp_sluice-0.1.0/.hypothesis/constants/75de9f9bfb03fafb +4 -0
  15. mcp_sluice-0.1.0/.hypothesis/constants/9ead00bae9fc6e82 +4 -0
  16. mcp_sluice-0.1.0/.hypothesis/constants/a5359a1d63075933 +4 -0
  17. mcp_sluice-0.1.0/.hypothesis/constants/a88b472d44e25d12 +4 -0
  18. mcp_sluice-0.1.0/.hypothesis/constants/aba7595b54e25bc8 +4 -0
  19. mcp_sluice-0.1.0/.hypothesis/constants/b930103efb1873de +4 -0
  20. mcp_sluice-0.1.0/.hypothesis/constants/cc1232942d7e2bcc +4 -0
  21. mcp_sluice-0.1.0/.hypothesis/constants/e55a5401d33ff5d1 +4 -0
  22. mcp_sluice-0.1.0/.hypothesis/constants/ec9ac0b980d68864 +4 -0
  23. mcp_sluice-0.1.0/.hypothesis/constants/f4184698165767bc +4 -0
  24. mcp_sluice-0.1.0/.hypothesis/constants/f6384783b6a5db4f +4 -0
  25. mcp_sluice-0.1.0/CHANGELOG.md +69 -0
  26. mcp_sluice-0.1.0/CLAUDE.md +128 -0
  27. mcp_sluice-0.1.0/LICENSE +202 -0
  28. mcp_sluice-0.1.0/PKG-INFO +352 -0
  29. mcp_sluice-0.1.0/README.md +334 -0
  30. mcp_sluice-0.1.0/benchmarks/memory_materialization.py +352 -0
  31. mcp_sluice-0.1.0/benchmarks/results/memory-2026-08-30.md +166 -0
  32. mcp_sluice-0.1.0/demo/README.md +118 -0
  33. mcp_sluice-0.1.0/demo/__init__.py +1 -0
  34. mcp_sluice-0.1.0/demo/median.py +525 -0
  35. mcp_sluice-0.1.0/demo/transcripts/20260831T040623Z/baseline.txt +252 -0
  36. mcp_sluice-0.1.0/demo/transcripts/20260831T040623Z/report.json +83 -0
  37. mcp_sluice-0.1.0/demo/transcripts/20260831T040623Z/treatment.txt +61 -0
  38. mcp_sluice-0.1.0/intent/001-scratch-db.md +188 -0
  39. mcp_sluice-0.1.0/plan/001-notes-m0.md +1195 -0
  40. mcp_sluice-0.1.0/plan/001-scratch-db.md +385 -0
  41. mcp_sluice-0.1.0/pyproject.toml +69 -0
  42. mcp_sluice-0.1.0/review/canary-v0.1.0.md +53 -0
  43. mcp_sluice-0.1.0/review/codex-prompt.md +152 -0
  44. mcp_sluice-0.1.0/sluice.example.toml +30 -0
  45. mcp_sluice-0.1.0/spec/001-scratch-db.md +894 -0
  46. mcp_sluice-0.1.0/src/sluice/__init__.py +5 -0
  47. mcp_sluice-0.1.0/src/sluice/__main__.py +86 -0
  48. mcp_sluice-0.1.0/src/sluice/config.py +232 -0
  49. mcp_sluice-0.1.0/src/sluice/errors.py +55 -0
  50. mcp_sluice-0.1.0/src/sluice/gate.py +258 -0
  51. mcp_sluice-0.1.0/src/sluice/handle.py +115 -0
  52. mcp_sluice-0.1.0/src/sluice/infer.py +128 -0
  53. mcp_sluice-0.1.0/src/sluice/intercept.py +326 -0
  54. mcp_sluice-0.1.0/src/sluice/models.py +112 -0
  55. mcp_sluice-0.1.0/src/sluice/naming.py +100 -0
  56. mcp_sluice-0.1.0/src/sluice/payload.py +228 -0
  57. mcp_sluice-0.1.0/src/sluice/proxy.py +254 -0
  58. mcp_sluice-0.1.0/src/sluice/py.typed +0 -0
  59. mcp_sluice-0.1.0/src/sluice/query.py +267 -0
  60. mcp_sluice-0.1.0/src/sluice/scope.py +95 -0
  61. mcp_sluice-0.1.0/src/sluice/server.py +162 -0
  62. mcp_sluice-0.1.0/src/sluice/shape.py +172 -0
  63. mcp_sluice-0.1.0/src/sluice/store.py +581 -0
  64. mcp_sluice-0.1.0/tests/__init__.py +0 -0
  65. mcp_sluice-0.1.0/tests/conftest.py +77 -0
  66. mcp_sluice-0.1.0/tests/fake_server/__init__.py +11 -0
  67. mcp_sluice-0.1.0/tests/fake_server/__main__.py +18 -0
  68. mcp_sluice-0.1.0/tests/fake_server/server.py +264 -0
  69. mcp_sluice-0.1.0/tests/test_cli.py +133 -0
  70. mcp_sluice-0.1.0/tests/test_concurrency.py +93 -0
  71. mcp_sluice-0.1.0/tests/test_config.py +183 -0
  72. mcp_sluice-0.1.0/tests/test_demo_median.py +25 -0
  73. mcp_sluice-0.1.0/tests/test_engine_contract.py +344 -0
  74. mcp_sluice-0.1.0/tests/test_infer.py +111 -0
  75. mcp_sluice-0.1.0/tests/test_intercept.py +503 -0
  76. mcp_sluice-0.1.0/tests/test_memory_bounds.py +444 -0
  77. mcp_sluice-0.1.0/tests/test_naming.py +96 -0
  78. mcp_sluice-0.1.0/tests/test_passthrough.py +175 -0
  79. mcp_sluice-0.1.0/tests/test_property_aggregates.py +363 -0
  80. mcp_sluice-0.1.0/tests/test_proxy.py +166 -0
  81. mcp_sluice-0.1.0/tests/test_query_limits.py +334 -0
  82. mcp_sluice-0.1.0/tests/test_query_safety.py +222 -0
  83. mcp_sluice-0.1.0/tests/test_query_tool.py +250 -0
  84. mcp_sluice-0.1.0/tests/test_scope.py +107 -0
  85. mcp_sluice-0.1.0/tests/test_shape.py +110 -0
  86. mcp_sluice-0.1.0/tests/test_store.py +242 -0
@@ -0,0 +1,62 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches:
7
+ - main
8
+ workflow_dispatch:
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ jobs:
14
+ check:
15
+ runs-on: ubuntu-latest
16
+ timeout-minutes: 15
17
+
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+
21
+ - name: Set up Python
22
+ uses: actions/setup-python@v5
23
+ with:
24
+ python-version: "3.14"
25
+
26
+ - name: Install uv
27
+ uses: astral-sh/setup-uv@v4
28
+
29
+ # Dependencies deliberately have lower bounds rather than a committed
30
+ # lock file. The engine contract tests make CI the forward-compatibility
31
+ # tripwire for new MCP and DuckDB releases.
32
+ - name: Install current dependencies
33
+ run: uv sync --upgrade
34
+
35
+ - name: Run tests
36
+ run: uv run pytest
37
+
38
+ - name: Run lint
39
+ run: uv run ruff check .
40
+
41
+ - name: Check formatting
42
+ run: uv run ruff format --check .
43
+
44
+ - name: Run type checks
45
+ run: uv run mypy src tests
46
+
47
+ - name: Build distributions
48
+ run: uv build
49
+
50
+ - name: Smoke-test wheel
51
+ run: |
52
+ uv venv --python 3.14 /tmp/sluice-wheel-smoke
53
+ uv pip install --python /tmp/sluice-wheel-smoke/bin/python dist/*.whl
54
+ /tmp/sluice-wheel-smoke/bin/python -c 'import importlib.metadata, importlib.resources, sluice; assert importlib.metadata.version("mcp-sluice") == sluice.__version__; assert importlib.resources.files("sluice").joinpath("py.typed").is_file()'
55
+ /tmp/sluice-wheel-smoke/bin/sluice --help
56
+
57
+ - name: Upload distributions
58
+ uses: actions/upload-artifact@v4
59
+ with:
60
+ name: mcp-sluice-dist
61
+ path: dist/
62
+ if-no-files-found: error
@@ -0,0 +1,103 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v[0-9]+.[0-9]+.[0-9]+"
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ concurrency:
12
+ group: release-${{ github.ref }}
13
+ cancel-in-progress: false
14
+
15
+ jobs:
16
+ build:
17
+ runs-on: ubuntu-latest
18
+ timeout-minutes: 15
19
+
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+
23
+ - name: Set up Python
24
+ uses: actions/setup-python@v5
25
+ with:
26
+ python-version: "3.14"
27
+
28
+ - name: Install uv
29
+ uses: astral-sh/setup-uv@v4
30
+
31
+ - name: Install current dependencies
32
+ run: uv sync --upgrade
33
+
34
+ - name: Verify tag matches package version
35
+ run: |
36
+ test "${GITHUB_REF_NAME#v}" = "$(uv run python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')"
37
+ test "${GITHUB_REF_NAME#v}" = "$(uv run python -c 'import sluice; print(sluice.__version__)')"
38
+
39
+ - name: Run release checks
40
+ run: |
41
+ uv run pytest
42
+ uv run ruff check .
43
+ uv run ruff format --check .
44
+ uv run mypy src tests
45
+
46
+ - name: Build distributions
47
+ run: uv build
48
+
49
+ - name: Verify distribution metadata
50
+ run: |
51
+ test -f "dist/mcp_sluice-${GITHUB_REF_NAME#v}.tar.gz"
52
+ test -f "dist/mcp_sluice-${GITHUB_REF_NAME#v}-py3-none-any.whl"
53
+ uvx twine check dist/*
54
+ unzip -p dist/*.whl 'mcp_sluice-*.dist-info/METADATA' | grep -Fx 'Name: mcp-sluice'
55
+ unzip -p dist/*.whl 'mcp_sluice-*.dist-info/METADATA' | grep -Fx 'License-Expression: Apache-2.0'
56
+ unzip -l dist/*.whl | grep -F 'licenses/LICENSE'
57
+
58
+ - name: Upload distributions
59
+ uses: actions/upload-artifact@v4
60
+ with:
61
+ name: mcp-sluice-dist
62
+ path: dist/
63
+ if-no-files-found: error
64
+
65
+ github-release:
66
+ needs:
67
+ - build
68
+ - pypi-publish
69
+ runs-on: ubuntu-latest
70
+ timeout-minutes: 10
71
+ permissions:
72
+ contents: write
73
+
74
+ steps:
75
+ - name: Download distributions
76
+ uses: actions/download-artifact@v4
77
+ with:
78
+ name: mcp-sluice-dist
79
+ path: dist
80
+
81
+ - name: Create GitHub release
82
+ env:
83
+ GH_TOKEN: ${{ github.token }}
84
+ GH_REPO: ${{ github.repository }}
85
+ run: gh release create "$GITHUB_REF_NAME" dist/* --verify-tag --generate-notes --title "$GITHUB_REF_NAME"
86
+
87
+ pypi-publish:
88
+ needs: build
89
+ runs-on: ubuntu-latest
90
+ timeout-minutes: 10
91
+ environment: pypi
92
+ permissions:
93
+ id-token: write
94
+
95
+ steps:
96
+ - name: Download distributions
97
+ uses: actions/download-artifact@v4
98
+ with:
99
+ name: mcp-sluice-dist
100
+ path: dist
101
+
102
+ - name: Publish package distributions to PyPI
103
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,11 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ .ruff_cache/
7
+ sluice.toml
8
+ uv.lock
9
+ .coverage
10
+ coverage.xml
11
+ htmlcov/
@@ -0,0 +1,9 @@
1
+ # This .gitignore file was automatically created by Hypothesis. Hypothesis gitignores
2
+ # .hypothesis by default, because we generally recommend that .hypothesis not be checked
3
+ # into version control.
4
+ #
5
+ # If you *would* like to check .hypothesis into version control, you should delete this
6
+ # file. Hypothesis will not re-create this .gitignore unless .hypothesis is deleted (and
7
+ # if it does, that's a bug - please report it!)
8
+
9
+ *
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/sluice/sluice/src/sluice/gate.py
2
+ # hypothesis_version: 6.167.1
3
+
4
+ [', ', '?', 'BASE_TABLE', 'FUNCTION', 'RECURSIVE_CTE_NODE', 'SELECT', 'SHOW_REF', 'TABLE_FUNCTION', '\\A[0-9a-f]{32}\\Z', 'array_resize', 'bitstring', 'cte_map', 'error', 'format', 'function', 'function_name', 'key', 'left', 'list_resize', 'lpad', 'map', 'nextval', 'no statement found', 'node', 'printf', 'query', 'right', 'rpad', 'schema_name', 'setseed', 'sleep_ms', 'table_name', 'type', 'value', 'write_log']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/sluice/sluice/src/sluice/__init__.py
2
+ # hypothesis_version: 6.167.1
3
+
4
+ ['0.1.0', '__version__']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/sluice/sluice/src/sluice/handle.py
2
+ # hypothesis_version: 6.167.1
3
+
4
+ [' (inexact)', '*', ', ', 'JSON', 'byte_size', 'call_id', 'channel_conflict', 'columns', 'columns: ', 'envelope_table', 'exact', 'flat_reason', 'name', 'preview', 'preview_complete', 'renamed: ', 'renamed_from', 'row_count', 'scope_id', 'source_channel', 'source_path', 'tables', 'text', 'type']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/sluice/sluice/src/sluice/models.py
2
+ # hypothesis_version: 6.167.1
3
+
4
+ [1000, 'is_error', 'non_text_content', 'none', 'oversize', 'selection_failed', 'structured', 'text']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/sluice/sluice/src/sluice/store.py
2
+ # hypothesis_version: 6.167.1
3
+
4
+ [512, ', ', ':memory:', '?', 'BEGIN TRANSACTION', 'COMMIT', 'ROLLBACK', 'retention_evicted', 'sluice_calls', 'utf-8']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/sluice/sluice/src/sluice/config.py
2
+ # hypothesis_version: 6.167.1
3
+
4
+ [10.0, 100, 256, 384, 512, 1000, 1024, 2048, 65536, 1048576, ', ', '1GB', 'SLUICE_CONFIG', 'args', 'command', 'cwd', 'env', 'limits', 'max_cell_bytes', 'max_columns', 'max_payload_bytes', 'max_session_bytes', 'max_session_calls', 'preview_bytes', 'preview_rows', 'query_max_bytes', 'query_max_rows', 'servers', 'sluice.toml', 'url', 'utf-8']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/sluice/sluice/src/sluice/payload.py
2
+ # hypothesis_version: 6.167.1
3
+
4
+ ['text', 'truncated', 'type', 'unknown', 'utf-8']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/sluice/sluice/src/sluice/scope.py
2
+ # hypothesis_version: 6.167.1
3
+
4
+ ['__dict__', 'conversationId', 'conversation_id', 'sessionId', 'session_id', 'threadId', 'thread_id', 'utf-8']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/sluice/sluice/src/sluice/infer.py
2
+ # hypothesis_version: 6.167.1
3
+
4
+ [127, 'BIGINT', 'BOOLEAN', 'DOUBLE', 'HUGEINT', 'JSON', 'VARCHAR', 'false', 'true']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/sluice/sluice/src/sluice/proxy.py
2
+ # hypothesis_version: 6.167.1
3
+
4
+ [1000, 'description', 'name', 'output schema', 'output_schema', 'structured content']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/sluice/sluice/.venv/bin/pytest
2
+ # hypothesis_version: 6.167.1
3
+
4
+ ['-script.pyw', '.exe', '__main__']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/sluice/sluice/src/sluice/query.py
2
+ # hypothesis_version: 6.167.1
3
+
4
+ [' |', ' | ', "''", '(no columns)', '---', 'NULL', '\\', '\\\\', '\\n', '\\|', 'additionalProperties', 'description', 'integer', 'max_rows', 'minimum', 'object', 'properties', 'query', 'required', 'sql', 'string', 'type', 'utf-8', '|', '| ', '…']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/sluice/sluice/demo/__init__.py
2
+ # hypothesis_version: 6.167.1
3
+
4
+ []
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/sluice/sluice/src/sluice/errors.py
2
+ # hypothesis_version: 6.167.1
3
+
4
+ ['output_schema', 'protocol', 'text', 'tool_error', 'transport']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/sluice/sluice/demo/median.py
2
+ # hypothesis_version: 6.167.1
3
+
4
+ [1e-06, 0.005, 180.0, 400, '\n--- stderr ---\n', '%Y%m%dT%H%M%SZ', ',', '--allowedTools', '--config', '--dry-run', '--max-turns', '--mcp-config', '--model', '--out-dir', '--output-format', '--permission-mode', '--rows', '--strict-mcp-config', '--timeout', '--tolerance-abs', '--tolerance-rel', '--tools', '--verbose', '-?\\d+(?:\\.\\d+)?', '-m', '-p', 'AKIA[0-9A-Z]{16}', 'ToolSearch', '[REDACTED]', '__main__', 'abs_tol', 'args', 'baseline', 'baseline.mcp.json', 'claude', 'command', 'conditions', 'cwd', 'demo', 'dontAsk', 'expected', 'expected_median', 'fake', 'generated_at_utc', 'max_turns', 'mcpServers', 'mcp__fake__rows', 'mcp__sluice__query', 'message', 'model', 'model_requested', 'non_determinism_note', 'ok', 'out_dir', 'pending', 'prompt', 'rel_tol', 'replace', 'report.json', 'result', 'rows', 'score', 'sk-[A-Za-z0-9]{20,}', 'sluice', 'sluice-demo-', 'sluice.toml', 'sonnet', 'status', 'store_true', 'stream-json', 'tests.fake_server', 'timeout', 'tolerance', 'tolerance_abs', 'tolerance_rel', 'transcripts', 'treatment', 'treatment.mcp.json', 'type', 'utf-8']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/sluice/sluice/src/sluice/server.py
2
+ # hypothesis_version: 6.167.1
3
+
4
+ ['max_rows', 'sluice', 'sql', 'text', 'unknown']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/sluice/sluice/src/sluice/naming.py
2
+ # hypothesis_version: 6.167.1
3
+
4
+ [128, '"', '""', '[^a-z0-9]+', '_', 'utf-8', 'x']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/sluice/sluice/src/sluice/intercept.py
2
+ # hypothesis_version: 6.167.1
3
+
4
+ ['content', 'text', 'tool_error']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/sluice/sluice/src/sluice/shape.py
2
+ # hypothesis_version: 6.167.1
3
+
4
+ [10000, '$', ', ', '_call_id', '_extra', '_row', 'value']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/sluice/sluice/src/sluice/__main__.py
2
+ # hypothesis_version: 6.167.1
3
+
4
+ [130, '--config', '--log-level', 'INFO', '__main__', 'path to sluice.toml', 'sluice']
@@ -0,0 +1,69 @@
1
+ # Changelog
2
+
3
+ This tracks what has landed against the milestones in
4
+ `plan/001-scratch-db.md`.
5
+
6
+ ## 0.1.0 — 2026-09-01
7
+
8
+ - **M1 — proxy.** Sluice starts, connects to one configured downstream MCP
9
+ server over stdio, mounts its tools under injective names, and forwards
10
+ calls unmodified. Paginated `tools/list`, whole-object tool cloning,
11
+ multi-round-trip relay, and the four-class failure taxonomy (§8).
12
+ - **M2 — envelope, scope, handle.** Every proxied call gets one `sluice_calls`
13
+ row in an in-memory DuckDB database, under the §6.1 engine lockdown. Scope
14
+ is derived from a client-supplied conversation id when present, minted
15
+ per call otherwise. Eligible results are replaced with a handle; channel
16
+ selection and conflict detection between `structuredContent` and text
17
+ content are implemented per §5.1.
18
+ - **M3 — flattening and inference.** Depth-1 projection extracts every
19
+ candidate row set from a result (§5.2), Sluice's own type inference assigns
20
+ a column type and an `exact` flag per §5.5 without going through DuckDB's
21
+ JSON inference, and tables are created file-free via explicit DDL plus
22
+ `executemany`.
23
+ - **M4 — the `query` tool.** The three-layer read-only gate (statement type,
24
+ engine lockdown, AST object allowlist), a per-query DuckDB connection with a
25
+ timer-based interrupt, and defined result shaping (row cap, byte cap,
26
+ per-cell truncation, markdown escaping) are implemented and covered by
27
+ `test_query_safety.py` and `test_query_limits.py`. Two review passes closed
28
+ a gate bypass and several other failures after initial implementation (see
29
+ commit history).
30
+ - Full test suite passes: proxy, passthrough, envelope, scope, shape,
31
+ inference, store, handle, config, CLI, engine-contract, query safety,
32
+ query limits, and concurrency.
33
+
34
+ ### M5 — correctness property
35
+
36
+ - The end-to-end Hypothesis suite covers normalized aggregate correctness,
37
+ 1–500-row boundaries, bounded floating-point regression evidence, and the
38
+ mandatory counterexamples. Adversarial review found cancellation inside the
39
+ former floating-point guarantee, so every `DOUBLE` column is now marked
40
+ inexact and per-aggregate exactness is left for future design.
41
+ - The reproducible live-model median demo is implemented outside CI. Its
42
+ committed sampled run recorded a wrong baseline answer (71.5) and the exact
43
+ Sluice-backed answer (72.5), with transcripts and a non-determinism warning.
44
+
45
+ ### Runtime bounds and adversarial review
46
+
47
+ - Whole-pipeline admission, deterministic logical session retention, and
48
+ metadata/catalog cardinality limits bound Sluice's own continued work.
49
+ - Post-fix calibration across flat, nested, wide, and mixed 1 MiB dual-channel
50
+ payloads at concurrency 2 supports the conservative 1 MiB v0 admission
51
+ default; a 15-call run records long-session behavior.
52
+ - Final review closed CTE binding-order isolation, scalar memory amplification,
53
+ queued-worker timeout, truncation-reporting, dual-channel sizing, failure
54
+ envelope, and dead-transport reuse defects.
55
+
56
+ ### M6 — documentation and packaging
57
+
58
+ - Installation, client configuration, architecture, usage, exactness,
59
+ isolation, resource bounds, limitations, troubleshooting, and release notes
60
+ are documented. The wheel includes `py.typed` and is validated by CI.
61
+
62
+ ### Release verification
63
+
64
+ - The end-to-end canary against a real configured GitHub MCP server passed; see
65
+ `review/canary-v0.1.0.md` for the observed handle, query, envelope, and
66
+ isolation checks.
67
+ - The distribution name is `mcp-sluice`; the import package and console command
68
+ remain `sluice`.
69
+ - Source and distributions are licensed under Apache-2.0.
@@ -0,0 +1,128 @@
1
+ # CLAUDE.md - Sluice
2
+
3
+ Passthrough MCP server. Proxies **exactly one** downstream MCP server in v0
4
+ (namespacing is built for fan-out, deferred), materializes every tool result into
5
+ a DuckDB scratch DB, and returns a preview plus a table handle instead of the
6
+ payload. Exposes one tool of its own, `query`, for read-only SQL over those
7
+ tables.
8
+
9
+ Read `spec/001-scratch-db.md` before changing behavior; `intent/` records why the
10
+ non-goals are non-goals; `plan/001-notes-m0.md` is the measured evidence behind
11
+ the rules below.
12
+
13
+ ## Commands
14
+
15
+ ```bash
16
+ uv sync # install
17
+ uv run sluice --config sluice.toml # run as an MCP stdio server
18
+ uv run pytest # full suite
19
+ uv run pytest -m "not slow" # skip timing-sensitive tests (concurrency, timeouts)
20
+ uv run ruff check . && uv run ruff format --check .
21
+ uv run mypy src tests
22
+ ```
23
+
24
+ The M5 Hypothesis correctness property is `tests/test_property_aggregates.py`.
25
+ The non-deterministic model-eval harness is `uv run python -m demo.median`; use
26
+ `--dry-run` to validate its setup without spending a model call. It is evidence,
27
+ never a CI gate.
28
+
29
+ ## Python version rules
30
+
31
+ - `requires-python = ">=3.14"`. **Do not write compatibility shims for older
32
+ versions.** No `sys.version_info` branches, no backport imports.
33
+ - PEP 604 unions: `X | None`, never `typing.Optional[X]`.
34
+ - PEP 585 builtin generics: `list[str]`, `dict[str, int]`, never `typing.List`.
35
+ - `type` statements for aliases, never `TypeAlias`.
36
+ - Type hints on every function signature. `mypy` is not optional.
37
+ - Python 3.15 releases 2026-10-01. Moving the floor to 3.15 is an **open
38
+ question, not a decision**. The gating factor is DuckDB cp315 wheel
39
+ availability, since DuckDB is a compiled extension and a source build is not an
40
+ acceptable install path.
41
+
42
+ ## Conventions
43
+
44
+ - Idiomatic modern Python of the FastAPI and Postgres kind. Dataclasses for value
45
+ objects, `pathlib`, structured logging **to stderr** (stdout is the MCP
46
+ transport and must never be written to).
47
+ - `shape.py`, `infer.py`, `naming.py` are pure: no DuckDB, no MCP, no IO. They
48
+ hold the logic most likely to be wrong and are cheapest to test. Keep them pure.
49
+ - DuckDB calls are blocking: dispatch through `anyio.to_thread.run_sync`,
50
+ serialize writes behind an `anyio.Lock`, one connection per in-flight query.
51
+ - pytest, function-style tests, and Hypothesis for the M5 correctness property.
52
+
53
+ ## Architecture
54
+
55
+ ```
56
+ client --stdio--> server.py tools/list = downstream union + query
57
+ proxy.py downstream session, paginated list, round-trip relay
58
+ gate.py the query tool's three-layer read-only gate
59
+ shape.py extract rows -> depth-1 projection (pure)
60
+ infer.py column types + the `exact` flag (pure)
61
+ naming.py injective names, quoting, collisions (pure)
62
+ scope.py scope ids; stale handles fail loudly
63
+ store.py envelope row + typed tables
64
+ handle.py preview + tables + columns -> the agent
65
+ query.py per-query connection, timeout, result shaping
66
+ ```
67
+
68
+ Data model: one `sluice_calls` envelope row per call. Plus one typed table per
69
+ candidate array, never appended to, named
70
+ `<server>__<tool>__<hash>__<scope>__<seq>`. There is no `__latest` view: with
71
+ scope minted per call it would name one table, and reaching a table whose name
72
+ you lost is discovery, which isolation blocks. Tables and the envelope row are
73
+ written in one transaction.
74
+
75
+ Verified baseline: MCP protocol `2026-07-28`, `mcp` 2.1.1, DuckDB 1.5.5. Every
76
+ normative claim in the spec is against those; dependencies have lower bounds
77
+ rather than exact pins, so do not generalize across revisions.
78
+
79
+ ## Rules that are load-bearing
80
+
81
+ Each of these was a bug before it was a rule. Spec section in parentheses.
82
+
83
+ - **Sluice never turns a working tool call into a failed one** (§8). Failures
84
+ degrade to an envelope-only handle. Two boundaries: OOM, and a connection-wide
85
+ interrupt.
86
+ - **The handle rides in `content`** (§4.1); `structuredContent` mirrors it.
87
+ **On the way in, `structuredContent` wins** (§5.1): a tool may put data there
88
+ and prose in `content`, and flattening the prose discards the data.
89
+ - **Exactness is domain-bounded** (§5.6). Integer columns within ±2^53 can claim
90
+ exact aggregate results; larger integers and every `DOUBLE` column are
91
+ marked inexact. `avg` and float `sum` have only bounded regression evidence.
92
+ Never state the claim without its domain.
93
+ - **Materialization is file-free** (§5.4). The lockdown is database-global and
94
+ blocks DuckDB's own readers. Do not "fix" a load failure by relaxing it.
95
+ - **Sluice owns type inference** (§5.5), because of the above. Never infer
96
+ `TIMESTAMP` from a string; mixed scalars become `VARCHAR`, never `JSON`
97
+ (`median()` on `JSON` returns a lexicographic answer).
98
+ - **Read-only means three layers** (§6.1): statement gate, engine lockdown, and
99
+ an **allowlist** over the parsed AST. Not a denylist: an allowlist is closed by
100
+ construction. The statement gate does NOT stop `SHOW`, `DESCRIBE`, or
101
+ `PRAGMA` — DuckDB types all of them as SELECT. CTE names are honoured by
102
+ lexical scope; collecting them globally is a complete bypass.
103
+ - **The physical `sluice_calls` is never queryable** (§3.3). It lists every
104
+ scope's tables. Each scope gets a filtered view instead.
105
+ - **No table discovery** (§12). Enumeration is what isolation blocks. Do not add
106
+ a `sluice_schema` view back.
107
+ - **Sluice never answers an elicitation** (§11). Round trips are relayed
108
+ untouched; `request_state` is opaque.
109
+ - **Clone the whole downstream tool object** (FR-3), mutating only name,
110
+ description, and `outputSchema`. Rebuilding it field by field drops
111
+ `annotations.destructiveHint`.
112
+ - **Every truncation is reported** (§6.3). Silent truncation is a correctness bug
113
+ in a tool that sells determinism.
114
+ - **Agent-sized allocation functions are blocked** (§6.1). Table functions and
115
+ scalar constructors such as `range`, `lpad`, `list_resize`, and `bitstring`
116
+ can allocate before output rendering applies its cap.
117
+ - **Admission covers the whole interception pipeline** (§7). Selection, parse,
118
+ projection, commit, and handle rendering all run under the same semaphore.
119
+ - **Session retention is bounded** (§7). `max_session_bytes` evicts oldest calls
120
+ deterministically, dropping their tables and payload columns while preserving
121
+ envelope metadata; handles never advertise a table after its call is evicted.
122
+ `max_session_calls` separately bounds metadata rows and scope views.
123
+
124
+ ## Out of scope for v0
125
+
126
+ No cross-session persistence, cross-server joins, entity resolution, auth,
127
+ policy, redaction, audit layer, hosted service, UI, or table discovery. Recorded
128
+ as choices in `intent/` §Non-goals, not as backlog.