@yanzhang123/readonly-db-mcp 0.9.5 → 0.9.7

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.
package/README.md CHANGED
@@ -256,6 +256,8 @@ MYSQL_USE_PURE=false
256
256
  MYSQL_RAISE_ON_WARNINGS=false
257
257
  ```
258
258
 
259
+ `MYSQL_SQL_MODE` 与命名配置的 `sql_mode` 会在每次查询的会话中显式设置(空值即设为空模式)。会改变引号或反斜杠语义的模式会被拒绝:`ANSI_QUOTES`、`NO_BACKSLASH_ESCAPES`,以及包含它们的组合模式 `ANSI`、`DB2`、`MAXDB`、`MSSQL`、`ORACLE`、`POSTGRESQL`,因为只读 SQL 闸门按 MySQL 默认规则识别字符串边界。
260
+
259
261
  ### Streamable HTTP 传输(推荐)
260
262
 
261
263
  DeepSeek Harness 等新客户端可直接连接标准 MCP Streamable HTTP 端点:
@@ -599,7 +601,7 @@ npm test
599
601
  - **企业审计**:UTC JSONL 事件包含事件 ID、MCP 请求 ID、操作、调用方归因、策略决定、目标库、无字面量查询指纹、耗时、结果大小和结果状态;不记录 SQL 原文和结果数据
600
602
  - **两阶段审计**:失败关闭模式下,建立连接前先 fsync 写入 `started` 事件,随后记录终态事件
601
603
  - **默认加密**:数据库 TLS 默认 `REQUIRED`;生产配置建议使用 `VERIFY_CA` 或 `VERIFY_IDENTITY`
602
- - **解析器加固**:拒绝 MySQL `/*!...*/` 和 MariaDB `/*M!...*/` 可执行注释;未限定到允许数据库的全局 `SHOW` 被拦截
604
+ - **解析器加固**:拒绝 MySQL `/*!...*/` 和 MariaDB `/*M!...*/` 可执行注释;未限定到允许数据库的全局 `SHOW` 被拦截;每次查询显式设置已校验的 `sql_mode`,不接受改变字符串边界的 `ANSI_QUOTES`、`NO_BACKSLASH_ESCAPES` 等模式
603
605
  - **诊断隐私**:连接诊断只公开不可逆账号指纹和权限摘要,不返回用户名、授权原文、配置路径或数据库错误原文
604
606
  - **SSE 安全边界**:默认仅绑定回环地址;非回环部署必须配置 Bearer 认证或由唯一入口的认证反向代理保护
605
607
 
package/SECURITY.md CHANGED
@@ -144,6 +144,11 @@ The MCP server includes built-in protection against SQL injection for administra
144
144
  built-in or their reviewed name is explicitly listed in `allowed_functions`.
145
145
  Sequence advancement, lock functions and other known side-effecting functions
146
146
  remain blocked even inside `SELECT`.
147
+ - The guard tokenizes strings with MySQL's default quote and backslash rules.
148
+ Profiles therefore reject `sql_mode` values that change them (`ANSI_QUOTES`,
149
+ `NO_BACKSLASH_ESCAPES`, and the combination modes `ANSI`, `DB2`, `MAXDB`,
150
+ `MSSQL`, `ORACLE`, `POSTGRESQL`), and every query session explicitly applies
151
+ the validated `sql_mode` instead of inheriting the server's global value.
147
152
  - All exposed SQL then runs inside a MySQL read-only transaction. Normal completion rolls back; cancellation or result truncation closes the socket, forcing rollback and stopping unread-row production.
148
153
  - Every MCP tool is advertised as read-only and non-destructive.
149
154
  - Named profiles can define `allowed_databases`; SQL table/function qualifiers and resource URIs are checked against it with a MySQL AST parser.
@@ -147,8 +147,11 @@ function sleep(milliseconds) {
147
147
  );
148
148
  }
149
149
 
150
- function acquireInstallLock(lockPath, markerPath) {
151
- const deadline = Date.now() + 180_000;
150
+ // Returns null when another process finished an installation that matches
151
+ // this package. A stale marker alone is not enough: the lock holder may be
152
+ // replacing that runtime right now.
153
+ function acquireInstallLock(lockPath, isReady, { timeoutMs = 180_000 } = {}) {
154
+ const deadline = Date.now() + timeoutMs;
152
155
  while (true) {
153
156
  try {
154
157
  return fs.openSync(lockPath, "wx");
@@ -156,7 +159,7 @@ function acquireInstallLock(lockPath, markerPath) {
156
159
  if (!error || error.code !== "EEXIST") {
157
160
  throw error;
158
161
  }
159
- if (fs.existsSync(markerPath)) {
162
+ if (isReady()) {
160
163
  return null;
161
164
  }
162
165
  const age = Date.now() - fs.statSync(lockPath).mtimeMs;
@@ -187,7 +190,9 @@ function ensureRuntime() {
187
190
  }
188
191
 
189
192
  fs.mkdirSync(runtimeRoot, { recursive: true });
190
- const lockHandle = acquireInstallLock(lock, marker);
193
+ const lockHandle = acquireInstallLock(lock, () =>
194
+ runtimeIsReady(marker, runtimePython, fingerprint),
195
+ );
191
196
  if (lockHandle === null) {
192
197
  return runtimePython;
193
198
  }
@@ -284,6 +289,7 @@ async function main(argv = process.argv.slice(2)) {
284
289
  }
285
290
 
286
291
  module.exports = {
292
+ acquireInstallLock,
287
293
  bundledWheel,
288
294
  cacheBase,
289
295
  dependencyLock,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yanzhang123/readonly-db-mcp",
3
- "version": "0.9.5",
3
+ "version": "0.9.7",
4
4
  "description": "Enterprise-auditable, strictly read-only MySQL MCP server with multi-environment profiles.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",