# MCP-Postgresql **Repository Path**: framework-learning-notes/MCP-Postgresql ## Basic Information - **Project Name**: MCP-Postgresql - **Description**: 通用 PostgreSQL MCP Server,基于 Spring Boot 3 + Spring AI,通过 stdio 传输。 可被 CodeBuddy、Claude Desktop、Cursor、Cline 等任意 MCP 客户端拉起为子进程。 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-15 - **Last Updated**: 2026-09-15 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # MCP-Postgresql(2.0.0-jdk8)
简体中文 | English
通用 PostgreSQL MCP Server,stdio 传输,可被 CodeBuddy / Claude Desktop / 任何 MCP 客户端使用。 让 AI 安全地看 PostgreSQL、懂 PostgreSQL、有限度地操作 PostgreSQL。JDK 8 单 jar,stdio 通信,不开端口、不连外网,参数可通过环境变量或命令行参数覆盖。 **JDK 1.8 + Maven 3.3.x 兼容版**:**零 Spring 容器**——不使用 `@SpringBootApplication` / `@ConfigurationPropertiesScan` / `@Component`,无注解扫描、无依赖注入;全部组件在 `main()` 里按依赖顺序手工 `new`,启动路径完全确定、可断点跟踪。配置由 `PgProperties` 加载 `PG_*` 环境变量与 `--pg.*` 命令行参数(后者优先级更高);协议层为自实现的轻量 MCP JSON-RPC(5 个小类)。原功能与安全策略全部保留。 --- ## 一、核心特性 | 维度 | 设计 | |---|---| | **协议** | MCP 2024-11-05 over JSON-RPC 2.0,stdio 传输(stdin 请求 / stdout 响应 / stderr 日志) | | **架构** | 自实现 `McpProtocol` + `ToolRegistry` + `JsonRpcException`(不依赖 Spring AI / JDK 17) | | **容器** | **无 Spring**:纯手工装配,`main()` 里按依赖顺序 new(零注解扫描 / 零 DI / 零反射);配置由 `PG_*` 环境变量 + `--pg.*` 命令行参数覆盖 | | **驱动** | PostgreSQL JDBC 42.2.27(最后兼容 JDK 8 的版本,42.3+ 已要求 JDK 11+) | | **数据库** | PostgreSQL 12 / 14 / 15(驱动通用 PG 9.2+ 即可);catalog SQL 覆盖全 PG 主线版本 | | **安全** | 三层防护:① 高危关键字正则拦截 + 字符串/注释剥离防绕过;② 数据库级 `SET TRANSACTION READ ONLY` 强制只读;③ `pg.read-only: true` 配置级 fail-fast(只读模式下写工具根本不注册) | | **可移植** | 纯 JDK 8 语法,无 record/模式匹配/switch 表达式/var 等 Java 10+ 特性 | --- ## 二、环境要求 ``` JDK : 1.8 (1.8.0_202 已验证) Maven : 3.3.x (3.3.9 已验证;不要 ≥ 3.6,否则一些 plugin 不兼容) PostgreSQL : 12 / 14 / 15 (驱动通用,实际可连 PG 9.2+) 依赖 : 仅 postgresql 42.2.27 + jackson-databind 2.15.4 ``` ### Maven 3.3.9 兼容性 由于 Maven 3.3.9 太老,`pom.xml` 显式锁定 Maven 3.3.9 兼容版本: | Plugin | 锁定版本 | 说明 | |---|---|---| | `maven-compiler-plugin` | 3.8.0 | 3.10.1 要求 Maven 3.6.3+ | | `maven-jar-plugin` | 3.1.2 | 显式指定 `Main` 入口 | | `maven-shade-plugin` | 2.4.3 | 替代 spring-boot-maven-plugin(repackage),不引入过新 plugin | > 默认配置(`pg.host=127.0.0.1` 等)硬编码在 `PgProperties.load()`,**部署前请用环境变量覆盖**或传 `--pg.password=xxx`。 --- ## 三、构建与启动 ### 1. 编译打包 ```bash mvn -B -DskipTests clean package ``` 产出:`target/MCP-Postgresql-2.0.0-jdk8.jar`(fat jar,含 postgresql + jackson-databind 全部依赖)。 ### 2. 启动(MCP 客户端配置) ```json { "mcpServers": { "postgres": { "command": "java", "args": ["-jar", "D:/workspace/mcp/MCP-Postgresql/target/MCP-Postgresql-2.0.0-jdk8.jar"], "env": { "PG_HOST": "127.0.0.1", "PG_PORT": "5432", "PG_DATABASE": "postgres", "PG_USERNAME": "postgres", "PG_PASSWORD": "your_password" } } } } ``` 环境变量映射(命名保持简洁,便于 MCP 客户端 env 段直接设置): | 环境变量 | 配置项 | 默认值 | 说明 | |---|---|---|---| | `PG_HOST` | `pg.host` | `127.0.0.1` | — | | `PG_PORT` | `pg.port` | `5432` | — | | `PG_DATABASE` | `pg.database` | `postgres` | — | | `PG_USERNAME` | `pg.username` | `postgres` | — | | `PG_PASSWORD` | `pg.password` | 空 | — | | `PG_READ_ONLY` | `pg.read-only` | `false` | `true` 时写工具不注册 | | `PG_MAX_ROWS` | `pg.max-rows` | `200` | 单次查询返回行数上限 | | `PG_MAX_CELL_CHARS` | `pg.max-cell-chars` | `8192` | 单元格字符数上限,超出截断 | | `PG_SSLMODE` | `pg.sslmode` | `prefer` | — | | `PG_STATEMENT_TIMEOUT` | `pg.statement-timeout` | `30`(秒) | — | | `PG_CONNECT_TIMEOUT` | `pg.connect-timeout` | `10`(秒) | — | | `PG_MAX_CONNECTIONS` | `pg.max-connections` | `5` | 每库空闲连接上限 | | `PG_CONN_IDLE_TIMEOUT` | `pg.conn-idle-timeout` | `600`(秒) | 空闲连接超时;<=0 表示不清理 | | `PG_TRANSACTION_MAX_STATEMENTS` | `pg.transaction-max-statements` | `50` | 单次事务语句数上限 | | `LOG_LEVEL` | `log.level` | `INFO` | JUL 级别:SEVERE/WARNING/INFO/CONFIG/FINE/FINER/FINEST | ### 3. 命令行参数覆盖 ```bash # 命令行参数优先级最高(最高 → 低:命令行 > 环境变量 > 默认值) java -jar target/MCP-Postgresql-2.0.0-jdk8.jar \ --pg.host=127.0.0.1 \ --pg.password=your_password \ --pg.read-only=true \ --log.level=WARNING ``` > 注意:**没有 YAML / profile 切换**。配置来源仅两个:环境变量与命令行。如需 dev/prod 切换,由 MCP 客户端在 `env` 段写不同环境变量即可。 ### 4. stdio 测试(手工) ```bash # 把 JSON-RPC 请求写到文件,逐行一条 echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' > req.txt echo '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' >> req.txt # 喂给服务,看 stdout 返回 java -jar target/MCP-Postgresql-2.0.0-jdk8.jar < req.txt ``` --- ## 四、可用的 MCP 工具 | 工具 | 用途 | 写? | 必填参数 | |---|---|---|---| | `pg_health` | 连通性检查 / 排障第一步(版本/库/用户/地址/时间) | ❌ | — | | `pg_list_databases` | 列出所有数据库(名/所有者/大小/可连接) | ❌ | — | | `pg_list_schemas` | 列出 schema(排除系统内置) | ❌ | — | | `pg_list_tables` | 列出 schema 下的表/视图/物化视图/分区表 | ❌ | `schema`(可选,默认 public) | | `pg_describe_table` | 表完整结构(字段/约束/索引) | ❌ | `table` | | `pg_query` | 只读 SQL(SELECT/EXPLAIN/SHOW) | ❌ | `sql` | | `pg_explain` | EXPLAIN (FORMAT JSON, BUFFERS),可选 ANALYZE | ❌ | `sql` | | `pg_execute` | 单条写 SQL(增删改/DDL,支持 RETURNING) | ✅ | `sql` | | `pg_execute_transaction` | 多语句原子事务(支持参数化) | ✅ | `statements` | > 只读模式(`PG_READ_ONLY=true`)下 `pg_execute` / `pg_execute_transaction` 不注册——AI 侧物理不可见,调用即返回"工具不存在"。 ### 写工具示例 ```sql -- pg_execute UPDATE t_user SET status = 0 WHERE id = 3 ``` ```sql -- pg_execute_transaction(数组元素两种形式可混用) [ "UPDATE t_book SET stock = stock - 1 WHERE id = 1", {"sql": "INSERT INTO t_order(book_id, num) VALUES (?, ?)", "params": [1, 1]}, {"sql": "INSERT INTO t_log(at, msg) VALUES (?, ?)", "params": [ {"type":"timestamp","value":"2026-01-01 00:00:00"}, {"type":"jsonb","value":"{\"event\":\"order\"}"} ]} ] ``` 参数支持的类型化形式:`int/long/double/bool/date/time/timestamp/json/jsonb/string`(`string`/不传则走字面量自动识别)。 --- ## 五、安全模型(三层防护) ### 第 1 层:正则拦截(提示层) `SqlGuard` 拦截已知危险模式: - 多语句注入(含 `;` 拆分的攻击语句) - 写语句前缀:INSERT/UPDATE/DELETE/MERGE/CREATE/ALTER/DROP/TRUNCATE/GRANT/REVOKE/VACUUM/ANALYZE/REINDEX/CLUSTER/LOCK/REFRESH/COPY/CALL/DO/LISTEN/NOTIFY/SET - 事务控制:BEGIN/START/COMMIT/END/ROLLBACK/SAVEPOINT/RELEASE - 高危对象:`pg_read_server_files` / `pg_read_file` / `pg_read_binary_file` / `pg_ls_dir` / `lo_import` / `lo_export` / `dblink` / `pg_sleep` / `COPY ... FROM PROGRAM` - 删库级高危 DDL:`DROP DATABASE/SCHEMA/TABLESPACE/ROLE/USER`(仅写工具拦截)、`CREATE DATABASE/ROLE/USER`、`REINDEX DATABASE/SYSTEM` - CTE 写:`WITH x AS (DELETE ...) SELECT` `SqlStripper` 先剥离字符串字面量 / 行注释 / 块注释 / dollar-quoted 字符串 / 双引号标识符,再做关键字判断——避免 `WHERE note='delete'` 被误拦,也避免 `pg_/read_file` 拆分绕过。 ### 第 2 层:数据库强制只读(强制层) `QueryRunner` 在只读事务里加 `SET TRANSACTION READ ONLY`,**PostgreSQL 服务器**会拒绝任何写——即使绕过正则,DB 也会拒。 ``` BEGIN READ ONLY → 任意 INSERT/UPDATE → ERROR: cannot execute INSERT in a read-only transaction ``` ### 第 3 层:配置级 fail-fast `pg.read-only: true`(`PG_READ_ONLY=true`)让 `pg_execute` / `pg_execute_transaction` **根本不注册到工具列表**——AI 侧物理不可见。返回时是 `tools/call` 的"工具不存在",不是工具内部抛错,节省 DB 往返。 --- ## 六、stdio 协议实现细节 ### 输出双保险(防 stdout 污染) 1. **无 Spring**:不存在 Spring Boot banner,也没有容器启动日志,启动期不向 stdout 写任何内容 2. `Main.setupLogging()` 使用 JDK 自带 `java.util.logging`(JUL),`ConsoleHandler` 默认输出即 stderr;启动期用 `System.setProperty("java.util.logging.SimpleFormatter.format", ...)` 设单行格式,**不依赖任何日志配置文件** 3. `stdout` 写入走显式 UTF-8 的 `FileOutputStream(FileDescriptor.out)`,**独立于 JUL**,彻底规避日志库劫持 stdout 的可能 实测:服务运行时 stdout 仅含 JSON-RPC 响应,日志全在 stderr。 ### MCP 方法支持 | method | 用途 | |---|---| | `initialize` | 握手,返回 protocolVersion / capabilities / serverInfo | | `notifications/*` | 客户端通知(含 `notifications/initialized`),无响应 | | `tools/list` | 列出 9 个工具的完整描述 + JSON Schema | | `tools/call` | 调用工具;失败时 `result.isError=true` 而非 JSON-RPC error | | `ping` | 健康检查 | ### 自实现协议层(不依赖 Spring AI) ``` com.mcp.postgresql.protocol ├── McpProtocol — stdio 主循环:stdin.readLine → 路由 → println(JSON-RPC 2.0 编码、BOM 剥离) ├── JsonRpcException — JSON-RPC 2.0 标准错误码(-32700/-32600/-32601/-32602/-32603) ├── Tool — 工具的 name/description/inputSchema/handler ├── ToolHandler — 工具处理函数式接口(入参 arguments JsonNode,返回结果 JsonNode) └── ToolRegistry — 维护工具列表 + 响应 tools/list / tools/call(业务失败走 isError) ``` 仅 ~5 个类共 ~300 行,不引入任何 JDK 17 / Spring 依赖(既不用 Spring AI,也不用 Spring 容器)。 --- ## 七、目录结构 ``` MCP-Postgresql/ ├── pom.xml # 仅 postgresql + jackson-databind,无任何 Spring / YAML / Logback ├── README.md # 本文件 ├── README.en.md # English version ├── LICENSE # Apache-2.0 ├── src/ │ ├── main/ │ │ ├── java/com/mcp/postgresql/ │ │ │ ├── Main.java # 启动入口:手工装配 + 工具注册中心 │ │ │ ├── config/ │ │ │ │ └── PgProperties.java # 配置对象(环境变量 + 命令行参数;普通 POJO) │ │ │ ├── pool/ │ │ │ │ ├── ConnectionPool.java # 连接池(按 db 缓存 + 空闲超时后台清理) │ │ │ │ └── SqlWork.java # 函数式事务块 │ │ │ ├── runner/ │ │ │ │ ├── QueryRunner.java # 事务管理 + 只读强制 + 异常翻译 │ │ │ │ ├── JdbcExecutor.java # SQL 执行 + 参数绑定 + EXPLAIN 包装 │ │ │ │ └── StatementException.java # 定位 SQL 失败点 │ │ │ ├── security/ │ │ │ │ ├── SqlGuard.java # L3 正则黑名单 │ │ │ │ ├── SqlStripper.java # L2 字符串/注释剥离 │ │ │ │ └── IdentValidator.java # 共享数据库/schema/表名校验 │ │ │ ├── serialize/ │ │ │ │ └── ResultSerializer.java # JDBC → JSON(含行/单元格截断) │ │ │ ├── tools/ │ │ │ │ ├── MetaTools.java # 元数据 5 工具 │ │ │ │ ├── ReadTools.java # pg_query / pg_explain │ │ │ │ └── WriteTools.java # pg_execute / pg_execute_transaction │ │ │ └── protocol/ # 自实现 MCP 协议层 │ │ │ ├── McpProtocol.java │ │ │ ├── JsonRpcException.java │ │ │ ├── Tool.java │ │ │ ├── ToolHandler.java │ │ │ └── ToolRegistry.java │ │ └── resources/ # 仅源代码,无任何运行时配置(无 YAML / logback) │ └── test/ # 占位测试 └── target/ └── MCP-Postgresql-2.0.0-jdk8.jar # fat jar(依赖仅 postgresql + jackson-databind) ``` --- ## 八、与原版(Spring Boot 3 + Spring AI)对比 | 维度 | 原版(v2.0.0) | 本版(v2.0.0-jdk8) | |---|---|---| | JDK 最低 | 17 | **1.8** | | Maven 最低 | 3.6.3 | **3.3.9** | | Spring 容器 | Spring Boot 3.4.5 | **无**(纯手工装配,零 DI / 零注解扫描) | | 配置加载 | Spring `@ConfigurationProperties` | **环境变量 + 命令行参数**(无 YAML / 无 profile) | | Spring AI | ✅ 1.0.1 | ❌ 自实现 MCP 协议层(5 个小类) | | MCP 协议层 | SDK 托管 | **手写 JSON-RPC over stdio** | | PG 驱动 | 42.7.5(需 JDK 11+) | **42.2.27**(最后兼容 JDK 8) | | Java 17 语法 | ✅(record/模式匹配/var) | ❌(全部降级到 Java 8) | | Fat jar 插件 | spring-boot-maven-plugin | **maven-shade-plugin 2.4.3** | | 工具数量 | 9 | **9**(不变) | | 安全策略 | 三层 | **三层**(不变) | | stdio 防护 | ✅ | **✅**(stderr 实测通过) | | 依赖数量 | Spring 全家桶 | **2 个**(postgresql / jackson-databind) | --- ## 九、常见问题 **Q: 启动报 `Unknown lifecycle phase ".test.skip=true"`?** Maven 3.3.9 对 `-Dmaven.test.skip=true` 解析有问题。改用 `-DskipTests`,并保证测试代码里无外部依赖(占位类即可)。 **Q: `pg_health` 报 `数据库连接失败:Connection refused`?** 检查 `pg.host` / `pg.port` / `pg.username` / `pg.password`,本地启动 PG 可用 `pg_isready -h 127.0.0.1 -p 5432`。 **Q: 客户端报 `Parse error` / `Unexpected character (code 65279)`?** 请求带 UTF-8 BOM。`McpProtocol` 已自动剥离首行 BOM,但客户端发送前最好也剥一次(双重保险)。 **Q: 升级到 JDK 17 + Maven 3.9 后还能用吗?** 可以,直接在新 JDK 上跑即可——本服务不含 Spring 容器,没有版本绑定负担。若想换回 Spring AI 1.0.x + Spring Boot 3.4.x,需要回退协议层实现并在主类恢复容器装配。 **Q: 为什么主类不用 `@SpringBootApplication`?** 避免容器引入的不确定性(启动顺序、bean 冲突、反射扫描副作用)与额外体积。现在整条依赖链在 `main()` 里显式可见: ```java PgProperties props = PgProperties.load(args); setupLogging(props.logLevel); ConnectionPool pool = new ConnectionPool(props); Runtime.getRuntime().addShutdownHook(new Thread(pool::closeAll, "mcp-pg-shutdown")); QueryRunner runner = new QueryRunner(pool, props); SqlGuard guard = new SqlGuard(); JdbcExecutor executor = new JdbcExecutor(props); ToolRegistry registry = new ToolRegistry(); new MetaTools(runner, props).register(registry); new ReadTools(runner, guard, props, executor).register(registry); if (!props.readOnly) { new WriteTools(runner, guard, props, executor).register(registry); } probeDatabase(runner); Writer stdout = new BufferedWriter(new OutputStreamWriter( new FileOutputStream(FileDescriptor.out), StandardCharsets.UTF_8)); Reader stdin = new BufferedReader( new InputStreamReader(System.in, StandardCharsets.UTF_8)); new McpProtocol(registry, "MCP-Postgresql", "2.0.0-jdk8").serve(stdin, stdout); ``` **Q: 不想要 YAML / profile 怎么办?** 本版**就没有** YAML 配置。所有参数通过 `PG_*` 环境变量或 `--pg.*` 命令行参数传入。多环境切换由 MCP 客户端在 `env` 段写不同环境变量即可——这比 YAML profile 更透明、更可审计。 **Q: 连接会被服务端踢掉吗?** `PG_CONN_IDLE_TIMEOUT`(默认 600 秒)控制空闲连接的最大寿命。后台调度器每 `max(5, 超时/3)` 秒扫描一次,超时连接会被主动关闭——下次 `acquire` 再新建,避免长生命周期服务持有被服务端踢掉的死连接。设为 `0` 或负数关闭该清理。 **Q: 大查询怎么避免撑爆上下文?** 两个维度保护: - `PG_MAX_ROWS`(默认 200)单次查询返回行数上限;超限截断并返回 `truncated: true` + `hint`。 - `PG_MAX_CELL_CHARS`(默认 8192)单格字符数上限;超出截断并附"已截断,原文 N 字符"提示。 --- ## 十、版本 - **2.0.0-jdk8** (当前):JDK 1.8 + Maven 3.3.x 兼容版,零 Spring 容器(纯手工装配)+ 自实现 MCP 协议层 - **2.0.0**:JDK 17 + Spring Boot 3.4.5 + Spring AI 1.0.1(保留在 git 历史) License: Apache-2.0