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.
- {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/CHANGELOG.md +10 -0
- {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/PKG-INFO +8 -5
- {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/README.md +7 -4
- {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/pyproject.toml +1 -1
- {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/server.json +2 -2
- {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/__init__.py +1 -1
- {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/security/lineage.py +4 -1
- {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/security/parser.py +7 -0
- {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/.gitignore +0 -0
- {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/LICENSE +0 -0
- {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/sql-safe-mcp.example-multi.yaml +0 -0
- {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/sql-safe-mcp.example-shared.yaml +0 -0
- {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/sql-safe-mcp.example-simple.yaml +0 -0
- {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/__main__.py +0 -0
- {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/config.py +0 -0
- {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/db/__init__.py +0 -0
- {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/db/extras.py +0 -0
- {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/db/mysql.py +0 -0
- {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/db/reflection.py +0 -0
- {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/db/registry.py +0 -0
- {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/db/sqlserver.py +0 -0
- {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/diagnostics.py +0 -0
- {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/errors.py +0 -0
- {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/mcp_server.py +0 -0
- {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/models.py +0 -0
- {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/security/__init__.py +0 -0
- {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/security/dialect.py +0 -0
- {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/security/executor.py +0 -0
- {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/security/pipeline.py +0 -0
- {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/security/policy.py +0 -0
- {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/security/reasons.py +0 -0
- {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/security/schema.py +0 -0
- {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/security/tokens.py +0 -0
- {sql_safe_mcp-1.5.1 → sql_safe_mcp-1.5.2}/src/sql_safe_mcp/security/validated_query.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
269
|
-
|
|
270
|
-
|
|
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.
|
|
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
|
|
231
|
-
|
|
232
|
-
|
|
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
|
|
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"url": "https://github.com/proprock/sql-safe-mcp",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "1.5.
|
|
10
|
+
"version": "1.5.2",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "pypi",
|
|
14
14
|
"identifier": "sql-safe-mcp",
|
|
15
|
-
"version": "1.5.
|
|
15
|
+
"version": "1.5.2",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
}
|
|
@@ -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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|