sql-safe-mcp 1.5.1__tar.gz → 2.0.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 (37) hide show
  1. {sql_safe_mcp-1.5.1 → sql_safe_mcp-2.0.0}/CHANGELOG.md +71 -0
  2. {sql_safe_mcp-1.5.1 → sql_safe_mcp-2.0.0}/PKG-INFO +12 -7
  3. {sql_safe_mcp-1.5.1 → sql_safe_mcp-2.0.0}/README.md +9 -4
  4. {sql_safe_mcp-1.5.1 → sql_safe_mcp-2.0.0}/pyproject.toml +4 -4
  5. {sql_safe_mcp-1.5.1 → sql_safe_mcp-2.0.0}/server.json +2 -2
  6. {sql_safe_mcp-1.5.1 → sql_safe_mcp-2.0.0}/sql-safe-mcp.example-multi.yaml +6 -2
  7. {sql_safe_mcp-1.5.1 → sql_safe_mcp-2.0.0}/sql-safe-mcp.example-shared.yaml +18 -0
  8. {sql_safe_mcp-1.5.1 → sql_safe_mcp-2.0.0}/src/sql_safe_mcp/__init__.py +1 -1
  9. {sql_safe_mcp-1.5.1 → sql_safe_mcp-2.0.0}/src/sql_safe_mcp/__main__.py +21 -1
  10. {sql_safe_mcp-1.5.1 → sql_safe_mcp-2.0.0}/src/sql_safe_mcp/config.py +215 -31
  11. {sql_safe_mcp-1.5.1 → sql_safe_mcp-2.0.0}/src/sql_safe_mcp/diagnostics.py +18 -2
  12. {sql_safe_mcp-1.5.1 → sql_safe_mcp-2.0.0}/src/sql_safe_mcp/mcp_server.py +8 -1
  13. {sql_safe_mcp-1.5.1 → sql_safe_mcp-2.0.0}/src/sql_safe_mcp/models.py +3 -1
  14. sql_safe_mcp-2.0.0/src/sql_safe_mcp/pii_audit.py +200 -0
  15. {sql_safe_mcp-1.5.1 → sql_safe_mcp-2.0.0}/src/sql_safe_mcp/security/executor.py +15 -2
  16. {sql_safe_mcp-1.5.1 → sql_safe_mcp-2.0.0}/src/sql_safe_mcp/security/lineage.py +21 -13
  17. {sql_safe_mcp-1.5.1 → sql_safe_mcp-2.0.0}/src/sql_safe_mcp/security/parser.py +49 -1
  18. {sql_safe_mcp-1.5.1 → sql_safe_mcp-2.0.0}/src/sql_safe_mcp/security/pipeline.py +1 -0
  19. sql_safe_mcp-2.0.0/src/sql_safe_mcp/security/policy.py +147 -0
  20. {sql_safe_mcp-1.5.1 → sql_safe_mcp-2.0.0}/src/sql_safe_mcp/security/reasons.py +3 -0
  21. {sql_safe_mcp-1.5.1 → sql_safe_mcp-2.0.0}/src/sql_safe_mcp/security/schema.py +51 -5
  22. {sql_safe_mcp-1.5.1 → sql_safe_mcp-2.0.0}/src/sql_safe_mcp/security/tokens.py +102 -54
  23. {sql_safe_mcp-1.5.1 → sql_safe_mcp-2.0.0}/src/sql_safe_mcp/security/validated_query.py +43 -3
  24. {sql_safe_mcp-1.5.1 → sql_safe_mcp-2.0.0}/src/sql_safe_mcp/service.py +67 -3
  25. sql_safe_mcp-1.5.1/src/sql_safe_mcp/security/policy.py +0 -104
  26. {sql_safe_mcp-1.5.1 → sql_safe_mcp-2.0.0}/.gitignore +0 -0
  27. {sql_safe_mcp-1.5.1 → sql_safe_mcp-2.0.0}/LICENSE +0 -0
  28. {sql_safe_mcp-1.5.1 → sql_safe_mcp-2.0.0}/sql-safe-mcp.example-simple.yaml +0 -0
  29. {sql_safe_mcp-1.5.1 → sql_safe_mcp-2.0.0}/src/sql_safe_mcp/db/__init__.py +0 -0
  30. {sql_safe_mcp-1.5.1 → sql_safe_mcp-2.0.0}/src/sql_safe_mcp/db/extras.py +0 -0
  31. {sql_safe_mcp-1.5.1 → sql_safe_mcp-2.0.0}/src/sql_safe_mcp/db/mysql.py +0 -0
  32. {sql_safe_mcp-1.5.1 → sql_safe_mcp-2.0.0}/src/sql_safe_mcp/db/reflection.py +0 -0
  33. {sql_safe_mcp-1.5.1 → sql_safe_mcp-2.0.0}/src/sql_safe_mcp/db/registry.py +0 -0
  34. {sql_safe_mcp-1.5.1 → sql_safe_mcp-2.0.0}/src/sql_safe_mcp/db/sqlserver.py +0 -0
  35. {sql_safe_mcp-1.5.1 → sql_safe_mcp-2.0.0}/src/sql_safe_mcp/errors.py +0 -0
  36. {sql_safe_mcp-1.5.1 → sql_safe_mcp-2.0.0}/src/sql_safe_mcp/security/__init__.py +0 -0
  37. {sql_safe_mcp-1.5.1 → sql_safe_mcp-2.0.0}/src/sql_safe_mcp/security/dialect.py +0 -0
@@ -7,6 +7,77 @@ categories `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, and `Security`.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.0.0] - 2026-11-07
11
+
12
+ ### Added
13
+
14
+ - `sql-safe-mcp --verify-pii-rules [--strict]` connects to each `all_pii_safe` alias, reports how
15
+ many tables and columns every PII rule matches, and warns about rules that match nothing
16
+ (exit 2 with `--strict`) and about unprotected columns whose names look personal.
17
+ - `logging.audit: true` writes one `sql_safe_mcp.audit` line per `execute_sql` call with the server,
18
+ database, tables and columns read, protected column count, row count, truncation, and elapsed
19
+ time (or the error `code`). It never contains SQL, bind values, rows, or tokens. Off by default;
20
+ the tool response is unchanged.
21
+ - `--verify-pii-rules` also checks `pii.exposed_columns` and `pii.deny` entries (unmatched ones
22
+ warn and fail `--strict`) and, in `deny-by-default`, advises only on exposed columns.
23
+ - `pii.mode: deny-by-default` protects every column not listed in `pii.exposed_columns`, and
24
+ `pii.deny` rejects any reference to a column with `QUERY_REJECTED` ("a denied column cannot be
25
+ referenced"). Existing configurations are unchanged. See `docs/configuration.md`.
26
+ - `runtime.token_ttl_seconds` (off by default) rejects tokens older than the limit with
27
+ `INVALID_PII_TOKEN`. Enabling it also rejects tokens issued before it was set. The token format
28
+ stays `pii:v1:`.
29
+ - `runtime.min_count_cell` (off by default) makes `execute_sql` return a `COUNT(*)` value from 1
30
+ to the threshold minus one as `null`, with the new column encoding `suppressed`. `0` is not
31
+ suppressed.
32
+ - `pii_key_file` reads an alias key from a file instead of `pii_key_env`; a relative path is
33
+ resolved against the configuration file's directory. On POSIX the file must be owned by the
34
+ server user or root, with no access for others and no group write.
35
+
36
+ ### Security
37
+
38
+ - With `logging.level: DEBUG`, the MCP SDK no longer writes a malformed protocol message to
39
+ stderr; its logger is held at `INFO` or above, because that message could carry SQL, tokens,
40
+ or rows.
41
+ - `all_pii_safe` aliases now require verified TLS in `connection_url`: SQL Server needs
42
+ `Encrypt=yes` and rejects `TrustServerCertificate=yes` and `odbc_connect`; MySQL and MariaDB
43
+ need `ssl_ca` or `ssl_capath` and reject options that turn certificate checks off. A
44
+ configuration without them fails at startup with `CONFIG_ERROR`. `require_tls: false` on the
45
+ alias turns the check off and logs a warning at every start.
46
+ - After loading the configuration, the server removes `pii_key_env` variables from its environment,
47
+ so child processes such as ODBC drivers no longer inherit PII keys.
48
+ - `pii.rules` entries accept an optional `domain`. A token returned for a column of one domain is
49
+ rejected with `INVALID_PII_TOKEN` when compared with a column of another domain, so protected
50
+ values cannot be replayed across unrelated columns. Without `domain` the domain is the server
51
+ alias. Rules of different domains matching one column reject queries touching it with
52
+ `QUERY_REJECTED`.
53
+ - PII tokens now pad their payload to a multiple of 32 bytes, so token length reveals only a
54
+ 32-byte bucket of the plaintext length.
55
+ - Tokens issued before padding and domains are rejected with `INVALID_PII_TOKEN`; agents must
56
+ request new tokens.
57
+ - `execute_sql` now allows each SQL construct only in the positions the supported subset uses.
58
+ A bare column or literal used as a condition (`WHERE a`, `ON 1`, `WHERE a AND b = 1`) and any
59
+ other node in an unlisted position is rejected with `QUERY_REJECTED` before the database runs
60
+ the query.
61
+ - `execute_sql` now checks a PII token against the type of the protected column it is compared
62
+ with. A string token compared with an integer or decimal column, or any token compared with a
63
+ column of an unrecognised type, is rejected with `QUERY_REJECTED` before the database runs the
64
+ query. Before, SQL Server could echo the decrypted value in a conversion error (for example
65
+ error 245).
66
+ - Driver error messages in logs no longer carry values. Quoted literals (`'…'`, `N'…'`, `"…"`)
67
+ become `'***'` and digit runs of six or more become `***`, since SQL Server and MySQL echo the
68
+ offending value in conversion and duplicate-key errors. A failed `execute_sql` logs only the
69
+ driver error class and its SQLSTATE or numeric code, with no message text.
70
+
71
+ ## [1.5.2] - 2026-10-05
72
+
73
+ ### Security
74
+
75
+ - `execute_sql` now rejects a projection whose alias names anything other than a direct column,
76
+ a literal, or `COUNT(*)`, such as `SELECT (Email) AS e FROM Users`. Before, the expression was
77
+ classified as a literal, so a protected column wrapped in parentheses was returned in clear,
78
+ and on MySQL/MariaDB a projected comparison such as `(Email > 'm') AS x` acted as an oracle on
79
+ the protected value. The lineage stage also fails closed on any untraceable projection.
80
+
10
81
  ## [1.5.1] - 2026-09-23
11
82
 
12
83
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sql-safe-mcp
3
- Version: 1.5.1
3
+ Version: 2.0.0
4
4
  Summary: Minimal, read-only, PII-safe MCP server for SQL databases.
5
5
  License: MIT License
6
6
 
@@ -26,10 +26,10 @@ License: MIT License
26
26
  License-File: LICENSE
27
27
  Requires-Python: <3.15,>=3.12
28
28
  Requires-Dist: anyio<5,>=4.8
29
- Requires-Dist: cryptography<47,>=46.0.7
29
+ Requires-Dist: cryptography<51,>=48.0.1
30
30
  Requires-Dist: mcp<3,>=2
31
31
  Requires-Dist: pydantic<3,>=2.10
32
- Requires-Dist: pymysql>=1.2.3
32
+ Requires-Dist: pymysql<2,>=1.2.3
33
33
  Requires-Dist: pyodbc<6,>=5.2
34
34
  Requires-Dist: pyyaml<7,>=6
35
35
  Requires-Dist: sqlalchemy<3,>=2.0
@@ -133,7 +133,7 @@ or
133
133
  pip install sql-safe-mcp
134
134
  ```
135
135
 
136
- Pin a version when you want a fixed surface: `uvx sql-safe-mcp==1.5.1`.
136
+ Pin a version when you want a fixed surface: `uvx sql-safe-mcp==2.0.0`.
137
137
 
138
138
  Requires Python 3.12–3.14, [uv](https://docs.astral.sh/uv/) (or `pip`), and
139
139
  [Microsoft ODBC Driver 18 for SQL Server](https://learn.microsoft.com/sql/connect/odbc/download-odbc-driver-for-sql-server)
@@ -265,9 +265,12 @@ only to stderr.
265
265
  ```
266
266
 
267
267
  can return a row such as `[42, "pii:v1:..."]`: the agent can follow the customer without reading
268
- the email address. A token is accepted only in `=` and `IN` predicates on the same protected column
269
- and server alias. Protection covers the columns you list, so list every column that holds personal
270
- data.
268
+ the email address. A token is accepted only in `=` and `IN` predicates on the same server alias.
269
+ If `FirstName` is protected, `SELECT FirstName FROM Users WHERE FirstName = 'Alice'` is rejected
270
+ before it reaches the database. Filter protected columns only with authentic `pii:v1:...` tokens
271
+ returned by `execute_sql` for the same alias; a matching prefix alone is not enough. Each token is
272
+ authenticated and alias-bound, then validated and converted to a bound database value.
273
+ Protection covers the columns you list, so list every column that holds personal data.
271
274
 
272
275
  ## Documentation
273
276
 
@@ -275,6 +278,8 @@ data.
275
278
  limits, and logging.
276
279
  - [Tool reference](docs/tools.md) - arguments, responses, errors, and the accepted SQL subset.
277
280
  - [Security model](docs/security-model.md) - trust boundaries, guarantees, limitations, and verification.
281
+ - [Health data of minors](docs/health-data.md) - requirements, mechanisms, and an operator
282
+ checklist for medical deployments.
278
283
  - [Checks](docs/checks.md) - local, integration, and security verification commands.
279
284
 
280
285
  ## Security
@@ -95,7 +95,7 @@ or
95
95
  pip install sql-safe-mcp
96
96
  ```
97
97
 
98
- Pin a version when you want a fixed surface: `uvx sql-safe-mcp==1.5.1`.
98
+ Pin a version when you want a fixed surface: `uvx sql-safe-mcp==2.0.0`.
99
99
 
100
100
  Requires Python 3.12–3.14, [uv](https://docs.astral.sh/uv/) (or `pip`), and
101
101
  [Microsoft ODBC Driver 18 for SQL Server](https://learn.microsoft.com/sql/connect/odbc/download-odbc-driver-for-sql-server)
@@ -227,9 +227,12 @@ only to stderr.
227
227
  ```
228
228
 
229
229
  can return a row such as `[42, "pii:v1:..."]`: the agent can follow the customer without reading
230
- the email address. A token is accepted only in `=` and `IN` predicates on the same protected column
231
- and server alias. Protection covers the columns you list, so list every column that holds personal
232
- data.
230
+ the email address. A token is accepted only in `=` and `IN` predicates on the same server alias.
231
+ If `FirstName` is protected, `SELECT FirstName FROM Users WHERE FirstName = 'Alice'` is rejected
232
+ before it reaches the database. Filter protected columns only with authentic `pii:v1:...` tokens
233
+ returned by `execute_sql` for the same alias; a matching prefix alone is not enough. Each token is
234
+ authenticated and alias-bound, then validated and converted to a bound database value.
235
+ Protection covers the columns you list, so list every column that holds personal data.
233
236
 
234
237
  ## Documentation
235
238
 
@@ -237,6 +240,8 @@ data.
237
240
  limits, and logging.
238
241
  - [Tool reference](docs/tools.md) - arguments, responses, errors, and the accepted SQL subset.
239
242
  - [Security model](docs/security-model.md) - trust boundaries, guarantees, limitations, and verification.
243
+ - [Health data of minors](docs/health-data.md) - requirements, mechanisms, and an operator
244
+ checklist for medical deployments.
240
245
  - [Checks](docs/checks.md) - local, integration, and security verification commands.
241
246
 
242
247
  ## Security
@@ -1,16 +1,16 @@
1
1
  [project]
2
2
  name = "sql-safe-mcp"
3
- version = "1.5.1"
3
+ version = "2.0.0"
4
4
  description = "Minimal, read-only, PII-safe MCP server for SQL databases."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12,<3.15"
7
7
  license = { file = "LICENSE" }
8
8
  dependencies = [
9
9
  "anyio>=4.8,<5",
10
- "cryptography>=46.0.7,<47",
10
+ "cryptography>=48.0.1,<51",
11
11
  "mcp>=2,<3",
12
12
  "pydantic>=2.10,<3",
13
- "pymysql>=1.2.3",
13
+ "pymysql>=1.2.3,<2",
14
14
  "pyodbc>=5.2,<6",
15
15
  "pyyaml>=6,<7",
16
16
  "sqlalchemy>=2.0,<3",
@@ -63,7 +63,7 @@ markers = [
63
63
  [tool.mutmut]
64
64
  source_paths = ["src/sql_safe_mcp"]
65
65
  also_copy = ["server.json", "README.md", "tools", "scripts", "compose.sqlserver.yml", "compose.mysql.yml"]
66
- only_mutate = ["*security/*.py", "*config.py"]
66
+ only_mutate = ["*security/*.py", "*config.py", "*diagnostics.py", "*service.py"]
67
67
  mutate_only_covered_lines = true
68
68
  pytest_add_cli_args = ["-q", "-m", "not integration and not deep", "-p", "no:cacheprovider"]
69
69
  pytest_add_cli_args_test_selection = ["tests/unit", "tests/security", "tests/contract"]
@@ -7,12 +7,12 @@
7
7
  "url": "https://github.com/proprock/sql-safe-mcp",
8
8
  "source": "github"
9
9
  },
10
- "version": "1.5.1",
10
+ "version": "2.0.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "pypi",
14
14
  "identifier": "sql-safe-mcp",
15
- "version": "1.5.1",
15
+ "version": "2.0.0",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  }
@@ -19,11 +19,14 @@ servers:
19
19
  billing:
20
20
  # `sqlserver` uses an mssql+pyodbc URL; the URL dialect must match this engine.
21
21
  engine: sqlserver
22
- # all_pii_safe requires both `pii_key_env` and at least one effective local PII rule.
22
+ # all_pii_safe requires a key (`pii_key_env` or `pii_key_file`) and at least one effective PII rule.
23
23
  access_level: all_pii_safe
24
+ # all_pii_safe requires verified TLS: the URL must set Encrypt=yes and must not set
25
+ # TrustServerCertificate=yes. `require_tls: false` turns the check off for a dev server.
24
26
  connection_url: "${BILLING_SQL_URL}"
25
27
  # Environment-variable name for one base64-encoded 32-byte key. Names must match
26
- # ^[A-Z_][A-Z0-9_]*$; each all_pii_safe alias needs a different key.
28
+ # ^[A-Z_][A-Z0-9_]*$; each all_pii_safe alias needs a different key. Instead of
29
+ # pii_key_env you may set `pii_key_file: secrets/billing.key` (relative to this file).
27
30
  pii_key_env: BILLING_PII_KEY
28
31
  pii:
29
32
  # Only local rules are used in this file; `include` is intentionally absent.
@@ -42,6 +45,7 @@ servers:
42
45
  # mysql and mariadb both use mysql+pymysql URLs. Select the engine matching the server.
43
46
  engine: mysql
44
47
  access_level: all_pii_safe
48
+ # Verified TLS for MySQL/MariaDB: the URL must set ssl_ca (or ssl_capath).
45
49
  connection_url: "${SHOP_MYSQL_URL}"
46
50
  pii_key_env: SHOP_PII_KEY
47
51
  pii:
@@ -38,6 +38,9 @@ pii_rules:
38
38
  schema: dbo # Optional for SQL Server; omit it to match every schema.
39
39
  table: "Users[0-9]?"
40
40
  columns: [Email, FirstName, LastName]
41
+ # Optional token domain (alias character set). Tokens of one domain are accepted only on
42
+ # columns of that domain; without it the domain is the server alias.
43
+ domain: users_identity
41
44
 
42
45
  servers:
43
46
  legacy-prod:
@@ -65,6 +68,21 @@ servers:
65
68
  table: Cards
66
69
  columns: [HolderName]
67
70
 
71
+ peds-deny-by-default:
72
+ engine: sqlserver
73
+ access_level: all_pii_safe
74
+ connection_url: "${PEDS_SQL_URL}"
75
+ pii_key_env: PEDS_PII_KEY
76
+ pii:
77
+ # `deny-by-default` protects every column that exposed_columns does not list.
78
+ mode: deny-by-default
79
+ exposed_columns: # `database` is optional here and defaults to "*".
80
+ - table: Visits
81
+ columns: [VisitId, DepartmentCode, VisitYear]
82
+ deny: # Unreachable even as a token; columns: ["*"] denies a whole table.
83
+ - table: "*"
84
+ columns: [Notes, Anamnesis, Attachment]
85
+
68
86
  reporting-metadata:
69
87
  engine: sqlserver
70
88
  access_level: metadata
@@ -1,3 +1,3 @@
1
1
  """Minimal, PII-safe MCP server for relational databases."""
2
2
 
3
- __version__ = "1.5.1"
3
+ __version__ = "2.0.0"
@@ -10,6 +10,7 @@ from pathlib import Path
10
10
  from sql_safe_mcp.config import load_config
11
11
  from sql_safe_mcp.errors import DomainError
12
12
  from sql_safe_mcp.mcp_server import create_server
13
+ from sql_safe_mcp.pii_audit import collect_catalog, verify_pii_rules
13
14
 
14
15
 
15
16
  def _positive_int(value: str) -> int:
@@ -41,11 +42,24 @@ def _parser() -> argparse.ArgumentParser:
41
42
  metavar="N",
42
43
  help="Print N base64-encoded 32-byte PII keys (default: 1) and exit.",
43
44
  )
45
+ actions.add_argument(
46
+ "--verify-pii-rules",
47
+ action="store_true",
48
+ help="Connect to each all_pii_safe alias and report PII rules that match nothing.",
49
+ )
50
+ parser.add_argument(
51
+ "--strict",
52
+ action="store_true",
53
+ help="With --verify-pii-rules, exit 2 when any rule matches nothing.",
54
+ )
44
55
  return parser
45
56
 
46
57
 
47
58
  def main() -> None:
48
- args = _parser().parse_args()
59
+ parser = _parser()
60
+ args = parser.parse_args()
61
+ if args.strict and not args.verify_pii_rules:
62
+ parser.error("--strict requires --verify-pii-rules")
49
63
  if args.gen_pii_key is not None:
50
64
  for _ in range(args.gen_pii_key):
51
65
  print(base64.b64encode(secrets.token_bytes(32)).decode("ascii"))
@@ -58,6 +72,12 @@ def main() -> None:
58
72
  if args.check_config:
59
73
  print(f"Configuration valid: {len(config.servers)} server alias(es).")
60
74
  return
75
+ if args.verify_pii_rules:
76
+ report = verify_pii_rules(config, collect_catalog)
77
+ print("\n".join(report.lines), file=sys.stderr)
78
+ if report.errors or (args.strict and report.unmatched_rules):
79
+ raise SystemExit(2)
80
+ return
61
81
  create_server(config).run("stdio")
62
82
 
63
83