sql-safe-mcp 1.5.1__tar.gz → 1.5.2__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 (35) hide show
  1. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/CHANGELOG.md +10 -0
  2. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/PKG-INFO +8 -5
  3. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/README.md +7 -4
  4. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/pyproject.toml +1 -1
  5. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/server.json +2 -2
  6. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/__init__.py +1 -1
  7. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/security/lineage.py +4 -1
  8. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/security/parser.py +7 -0
  9. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/.gitignore +0 -0
  10. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/LICENSE +0 -0
  11. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/sql-safe-mcp.example-multi.yaml +0 -0
  12. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/sql-safe-mcp.example-shared.yaml +0 -0
  13. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/sql-safe-mcp.example-simple.yaml +0 -0
  14. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/__main__.py +0 -0
  15. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/config.py +0 -0
  16. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/db/__init__.py +0 -0
  17. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/db/extras.py +0 -0
  18. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/db/mysql.py +0 -0
  19. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/db/reflection.py +0 -0
  20. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/db/registry.py +0 -0
  21. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/db/sqlserver.py +0 -0
  22. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/diagnostics.py +0 -0
  23. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/errors.py +0 -0
  24. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/mcp_server.py +0 -0
  25. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/models.py +0 -0
  26. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/security/__init__.py +0 -0
  27. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/security/dialect.py +0 -0
  28. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/security/executor.py +0 -0
  29. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/security/pipeline.py +0 -0
  30. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/security/policy.py +0 -0
  31. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/security/reasons.py +0 -0
  32. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/security/schema.py +0 -0
  33. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/security/tokens.py +0 -0
  34. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/security/validated_query.py +0 -0
  35. {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/service.py +0 -0
@@ -7,6 +7,16 @@ categories `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, and `Security`.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.5.2] - 2026-10-05
11
+
12
+ ### Security
13
+
14
+ - `execute_sql` now rejects a projection whose alias names anything other than a direct column,
15
+ a literal, or `COUNT(*)`, such as `SELECT (Email) AS e FROM Users`. Before, the expression was
16
+ classified as a literal, so a protected column wrapped in parentheses was returned in clear,
17
+ and on MySQL/MariaDB a projected comparison such as `(Email > 'm') AS x` acted as an oracle on
18
+ the protected value. The lineage stage also fails closed on any untraceable projection.
19
+
10
20
  ## [1.5.1] - 2026-09-23
11
21
 
12
22
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sql-safe-mcp
3
- Version: 1.5.1
3
+ Version: 1.5.2
4
4
  Summary: Minimal, read-only, PII-safe MCP server for SQL databases.
5
5
  License: MIT License
6
6
 
@@ -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==1.5.2`.
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
 
@@ -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==1.5.2`.
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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sql-safe-mcp"
3
- version = "1.5.1"
3
+ version = "1.5.2"
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,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": "1.5.2",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "pypi",
14
14
  "identifier": "sql-safe-mcp",
15
- "version": "1.5.1",
15
+ "version": "1.5.2",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  }
@@ -1,3 +1,3 @@
1
1
  """Minimal, PII-safe MCP server for relational databases."""
2
2
 
3
- __version__ = "1.5.1"
3
+ __version__ = "1.5.2"
@@ -142,8 +142,11 @@ def _analyze_projections(
142
142
  outputs.append(OutputColumn(alias or canonical, "column", source, binding.name))
143
143
  elif isinstance(value, exp.Count):
144
144
  outputs.append(OutputColumn(alias or f"column_{index}", "count", None))
145
- else:
145
+ elif isinstance(value, exp.Literal | exp.Null | exp.Neg):
146
146
  outputs.append(OutputColumn(alias or f"column_{index}", "literal", None))
147
+ else:
148
+ # Fail closed: a projection whose source cannot be traced is never a "literal".
149
+ raise reject(Reason.PROJECTION_UNSUPPORTED)
147
150
  return outputs
148
151
 
149
152
 
@@ -80,6 +80,10 @@ _ALLOWED_ARGS: dict[type[exp.Expr], frozenset[str]] = {
80
80
  **{comparison: frozenset({"this", "expression"}) for comparison in _COMPARISONS},
81
81
  }
82
82
  _PLACEHOLDER_ARGS = frozenset({"this"})
83
+ # What an alias may name: a direct column, a literal, or COUNT(*). Anything else (a Paren, a
84
+ # comparison, ...) would be invisible to lineage and could project a protected column in clear.
85
+ # A Star under an alias is still rejected by the Star branch below with its own reason.
86
+ _ALIASED = (exp.Column, exp.Star, exp.Literal, exp.Null, exp.Neg, exp.Count)
83
87
  _PROJECTIONS = (exp.Column, exp.Star, exp.Literal, exp.Null, exp.Neg, exp.Alias, exp.Count)
84
88
 
85
89
 
@@ -110,6 +114,9 @@ def _check_node(node: exp.Expr, allow_placeholders: bool) -> None:
110
114
  elif isinstance(node, exp.Column):
111
115
  if not isinstance(node.this, (exp.Identifier, exp.Star)):
112
116
  raise reject(Reason.COLUMN_REFERENCE_UNSUPPORTED)
117
+ elif isinstance(node, exp.Alias):
118
+ if not isinstance(node.this, _ALIASED):
119
+ raise reject(Reason.PROJECTION_UNSUPPORTED)
113
120
  elif isinstance(node, exp.Count):
114
121
  if not isinstance(node.this, exp.Star):
115
122
  raise reject(Reason.COUNT_STAR_ONLY)
File without changes
File without changes