@futuretea/postgres-mcp-server 0.1.0 → 0.3.0
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 +90 -7
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -6,10 +6,15 @@ An MCP (Model Context Protocol) server for discovering one PostgreSQL 17+ instan
|
|
|
6
6
|
|
|
7
7
|
## Quick start
|
|
8
8
|
|
|
9
|
-
Set
|
|
9
|
+
Set discrete connection fields for a minimally privileged PostgreSQL role, then build and inspect the catalog. A valid UTF-8 password is a normal environment value here and does not need URL encoding. A password that is not valid UTF-8 is rejected before connect, and the error does not include the password.
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
export
|
|
12
|
+
export MCP_POSTGRES_HOST=127.0.0.1
|
|
13
|
+
export MCP_POSTGRES_PORT=5432
|
|
14
|
+
export MCP_POSTGRES_DATABASE=app
|
|
15
|
+
export MCP_POSTGRES_USERNAME=reader
|
|
16
|
+
export MCP_POSTGRES_PASSWORD='<runtime-secret>'
|
|
17
|
+
export MCP_POSTGRES_SSLMODE=disable
|
|
13
18
|
make build
|
|
14
19
|
./bin/postgres-mcp-server tools list
|
|
15
20
|
./bin/postgres-mcp-server mcp
|
|
@@ -59,7 +64,7 @@ docker run --rm -i \
|
|
|
59
64
|
ghcr.io/futuretea/postgres-mcp-server:v0.0.1
|
|
60
65
|
```
|
|
61
66
|
|
|
62
|
-
For an MCP client that launches Docker, pass the connection
|
|
67
|
+
For an MCP client that launches Docker, pass the connection settings through the Docker process:
|
|
63
68
|
|
|
64
69
|
```json
|
|
65
70
|
{
|
|
@@ -124,7 +129,38 @@ Every successful tool response is one JSON text envelope with `schema_version`,
|
|
|
124
129
|
|
|
125
130
|
## Database connection
|
|
126
131
|
|
|
127
|
-
|
|
132
|
+
Set discrete environment variables. Do not put the host, password, or a connection URL in YAML. A valid UTF-8 password does not need URL encoding.
|
|
133
|
+
|
|
134
|
+
| Variable | libpq alias | Required without `DATABASE_URL` or `PGSERVICE` | Default |
|
|
135
|
+
|---|---|---|---|
|
|
136
|
+
| `MCP_POSTGRES_HOST` | `PGHOST` | yes | none |
|
|
137
|
+
| `MCP_POSTGRES_PORT` | `PGPORT` | no | `5432` |
|
|
138
|
+
| `MCP_POSTGRES_DATABASE` | `PGDATABASE` | yes | none |
|
|
139
|
+
| `MCP_POSTGRES_USERNAME` | `PGUSER` | yes | none |
|
|
140
|
+
| `MCP_POSTGRES_PASSWORD` | `PGPASSWORD` | no | empty |
|
|
141
|
+
| `MCP_POSTGRES_SSLMODE` | `PGSSLMODE` | no | `prefer` |
|
|
142
|
+
|
|
143
|
+
`MCP_POSTGRES_*` matches this server's `MCP_` environment prefix and the ClickHouse server's `MCP_CLICKHOUSE_*` connection fields (`MCP_CLICKHOUSE_DATABASE`, `MCP_CLICKHOUSE_USERNAME`, `MCP_CLICKHOUSE_PASSWORD`). MySQL uses `MYSQL_MCP_*` because that repository prefixes every variable, including its HTTP port, with `MYSQL_MCP_`. `MCP_POSTGRES_PORT` is the database port. The HTTP port remains `MCP_PORT`.
|
|
144
|
+
|
|
145
|
+
Each `MCP_POSTGRES_*` variable is an alias of the `PG*` variable in the same row. If both names are set to different values, startup fails. The error names the two variables and does not print their values. `MCP_POSTGRES_*` is an explicit connection parameter, so it overrides a `PGSERVICE` entry for that field. When the `MCP_POSTGRES_*` alias is unset, a service file still overrides `PG*`, which is pgx's existing libpq behavior.
|
|
146
|
+
|
|
147
|
+
An unset `MCP_POSTGRES_PASSWORD` leaves `PGPASSWORD` and `.pgpass` in place. An empty `PGPASSWORD` is unset, matching libpq. Setting `MCP_POSTGRES_PASSWORD` to an empty string is an explicit empty password: it overrides `PGPASSWORD` and `.pgpass`. If that empty value and a non-empty `PGPASSWORD` are both set, startup fails and the error names the two variables without printing them.
|
|
148
|
+
|
|
149
|
+
The resolved password must be valid UTF-8. This applies to `MCP_POSTGRES_PASSWORD`, `PGPASSWORD`, a `DATABASE_URL` password, a service-file password, and a `.pgpass` password. Invalid UTF-8 fails configuration with `[postgres_invalid_config] password must be valid UTF-8` and the value is not printed. Keeping the original bytes in the connection config is not authentication: pgx's SCRAM preparation replaces those bytes with U+FFFD and would send a different password.
|
|
150
|
+
|
|
151
|
+
`PGSERVICE` can supply the required fields from a PostgreSQL service file. Without `DATABASE_URL` or `PGSERVICE`, host, database, and username are required. Libpq defaults for those three fields are not used, so a partial `PG*` setup that omits one of them fails. A missing required field produces an error that names the absent variables, for example `[postgres_invalid_config] missing MCP_POSTGRES_HOST or PGHOST, MCP_POSTGRES_DATABASE or PGDATABASE, MCP_POSTGRES_USERNAME or PGUSER`.
|
|
152
|
+
|
|
153
|
+
Configuration must resolve to one `host:port`. Distinct URL targets or comma-separated hosts are rejected before a connection is created.
|
|
154
|
+
|
|
155
|
+
### `DATABASE_URL` (optional)
|
|
156
|
+
|
|
157
|
+
`DATABASE_URL` remains an advanced alternative for a libpq URL you already have. When it is set, that URL is the connection string and every `MCP_POSTGRES_*` variable is ignored. Standard `PG*` variables still fill settings the URL omits, and a setting present in the URL overrides the same `PG*` variable. Leave `DATABASE_URL` unset to use the discrete fields above.
|
|
158
|
+
|
|
159
|
+
```sh
|
|
160
|
+
export DATABASE_URL='postgres://reader@127.0.0.1:5432/app?sslmode=disable'
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
A password in `DATABASE_URL` must be URL-encoded. Prefer `MCP_POSTGRES_PASSWORD` when the password contains reserved characters.
|
|
128
164
|
|
|
129
165
|
At startup the server requires PostgreSQL 17 or newer and rejects service roles that are superusers, bypass row-level security, have replication, or belong to server-file, server-program, or backend-signalling roles. Use a dedicated, least-privilege database role.
|
|
130
166
|
|
|
@@ -149,7 +185,7 @@ GRANT SELECT ON ALL TABLES IN SCHEMA public TO mcp_reader;
|
|
|
149
185
|
|
|
150
186
|
### Verify effective schema privileges
|
|
151
187
|
|
|
152
|
-
Before configuring
|
|
188
|
+
Before configuring the connection, verify that `mcp_reader` cannot create objects in the target schema:
|
|
153
189
|
|
|
154
190
|
```sql
|
|
155
191
|
SELECT has_schema_privilege('mcp_reader', 'public', 'CREATE') AS can_create_objects;
|
|
@@ -164,7 +200,7 @@ ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public
|
|
|
164
200
|
GRANT SELECT ON TABLES TO mcp_reader;
|
|
165
201
|
```
|
|
166
202
|
|
|
167
|
-
Before configuring
|
|
203
|
+
Before configuring the connection, connect as `mcp_reader` and verify that every boolean below is `false`:
|
|
168
204
|
|
|
169
205
|
```sql
|
|
170
206
|
SELECT current_user, rolsuper, rolbypassrls, rolreplication,
|
|
@@ -178,6 +214,46 @@ WHERE rolname = current_user;
|
|
|
178
214
|
|
|
179
215
|
If any value is `true`, use a new dedicated role rather than removing privileges from a shared administrator or application role.
|
|
180
216
|
|
|
217
|
+
## Output masking
|
|
218
|
+
|
|
219
|
+
Masking is off unless it is explicitly enabled. With no `masking` key, and with `enabled` omitted or false, tool output is the previous text. Set `enabled: true` and at least one rule to turn it on. `MCP_MASKING_JSON`, when the variable is present, replaces the whole YAML `masking` object. An empty value is invalid. Unset leaves the YAML object in place.
|
|
220
|
+
|
|
221
|
+
Empty documents, unknown fields, and `enabled: true` with no rules fail startup. The error is `invalid masking config` and does not include the document.
|
|
222
|
+
|
|
223
|
+
```yaml
|
|
224
|
+
masking:
|
|
225
|
+
enabled: true
|
|
226
|
+
placeholder: "[masked]"
|
|
227
|
+
names: ["synthetic_field"]
|
|
228
|
+
name_patterns: ["token_.+"]
|
|
229
|
+
values: ["synthetic-token-value"]
|
|
230
|
+
value_patterns: ["sk_live_[0-9]+"]
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
```sh
|
|
234
|
+
export MCP_MASKING_JSON='{"enabled":true,"names":["synthetic_field"],"values":["synthetic-token-value"]}'
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
`names` and `name_patterns` select a whole field name. They also select database, schema, table, column, index, owner, and role identifiers returned by these tools. Name patterns are anchored to the whole name. `a)|(b` is rejected. `(?:a)|(b)` matches only the whole names `a` and `b`. `values` and `value_patterns` are substring matches, including a one-character literal, and each match is replaced once. The placeholder is not searched again. A one-character literal can match inside ordinary words.
|
|
238
|
+
|
|
239
|
+
A matched string becomes the placeholder, `[masked]` unless you set another one. A matched non-string becomes JSON null. An existing SQL NULL stays null. A null in a result can be a stored NULL or a redacted value. Types and nullability in the JSON can change when a rule matches. `ordinal_position` stays a number and `nullable` stays a boolean unless that whole value matches a rule.
|
|
240
|
+
|
|
241
|
+
The column type decides which rule applies. `text` and other ordinary strings stay strings, even when every character is a digit. A match replaces that span with the placeholder. `smallint`, `integer`, `bigint`, `real`, `double precision`, `numeric`, and `boolean` use the exact text PostgreSQL sent. If a value rule hits that text, the whole value becomes null and the digits are not edited in place. A boolean cell whose text is `t` does not match a rule for `true`. An unmatched number keeps its original literal.
|
|
242
|
+
|
|
243
|
+
JSON and JSONB cells that are one JSON object or array are masked structurally, including escaped keys and JSON nested inside a string, for two JSON-in-string rounds. A deeper JSON value fails the result. Text that only looks like JSON stays text, apart from value matches. A JSON number or boolean that matches a value rule becomes null. An unmatched number keeps its original literal, including trailing zeros.
|
|
244
|
+
|
|
245
|
+
`bytea` is recognized by the column type. Known `\x` hex is decoded, masked, and written back as hex. A hex cell longer than 1 MiB fails before it is decoded. Value patterns also run on ASCII text inside invalid UTF-8. Escape-format `bytea`, invalid hex, and `bytea[]` fail the result when masking is on, and the original bytes are not returned. A text value that looks like hex is not decoded. A masked value past the size limit fails the result.
|
|
246
|
+
|
|
247
|
+
A value pattern that can match without consuming a character is rejected at startup. `\b|sk_live_4242` is one example. The error is `invalid masking config` and does not include the pattern. The shared masking package is copied from a pin that still awaits an independent review. This server does not claim that review is finished.
|
|
248
|
+
|
|
249
|
+
These tools do not return column or table comments. Index expressions are the `definition` string, and defaults are the `default` string. Name rules do not search inside those strings. Value rules do.
|
|
250
|
+
|
|
251
|
+
`schema_version` and the tool name are server constants and are not scanned. Fixed public errors and the placeholder are not scanned. A short literal that also occurs there can remain. Masking does not find every private value, compressed payload, or encoding.
|
|
252
|
+
|
|
253
|
+
If a result cannot be masked, including a name collision, an unsupported shape, or a value past the size limit, the call fails with `[postgres_masking_failed] result was not returned` and no partial JSON. Masking changes emitted text only. The SQL sent to PostgreSQL, and the stored rows, stay as they were.
|
|
254
|
+
|
|
255
|
+
HTTP debug logs record a route class (`mcp`, `sse`, `message`, or `other`), not the request path or query.
|
|
256
|
+
|
|
181
257
|
## Read-only boundary and deployment risk
|
|
182
258
|
|
|
183
259
|
Each database operation uses a PostgreSQL read-only transaction and pgx's extended execution protocol. `postgres_query` is parsed as exactly one SELECT, rejecting DML, DDL, row locks, SELECT INTO, modifying CTEs, and selected PostgreSQL built-ins with known side effects.
|
|
@@ -217,7 +293,14 @@ The HTTP health endpoint is `GET /healthz`. See `config.example.yaml` for transp
|
|
|
217
293
|
|
|
218
294
|
```sh
|
|
219
295
|
make docker
|
|
220
|
-
docker run --rm -i
|
|
296
|
+
docker run --rm -i \
|
|
297
|
+
--env MCP_POSTGRES_HOST \
|
|
298
|
+
--env MCP_POSTGRES_PORT \
|
|
299
|
+
--env MCP_POSTGRES_DATABASE \
|
|
300
|
+
--env MCP_POSTGRES_USERNAME \
|
|
301
|
+
--env MCP_POSTGRES_PASSWORD \
|
|
302
|
+
--env MCP_POSTGRES_SSLMODE \
|
|
303
|
+
postgres-mcp-server:dev
|
|
221
304
|
```
|
|
222
305
|
|
|
223
306
|
The image entrypoint is `postgres-mcp-server mcp`.
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@futuretea/postgres-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Read-only PostgreSQL Model Context Protocol server",
|
|
5
5
|
"main": "./bin/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"postgres-mcp-server": "bin/index.js"
|
|
8
8
|
},
|
|
9
9
|
"optionalDependencies": {
|
|
10
|
-
"@futuretea/postgres-mcp-server-darwin-amd64": "0.
|
|
11
|
-
"@futuretea/postgres-mcp-server-darwin-arm64": "0.
|
|
12
|
-
"@futuretea/postgres-mcp-server-linux-amd64": "0.
|
|
13
|
-
"@futuretea/postgres-mcp-server-linux-arm64": "0.
|
|
14
|
-
"@futuretea/postgres-mcp-server-windows-amd64": "0.
|
|
10
|
+
"@futuretea/postgres-mcp-server-darwin-amd64": "0.3.0",
|
|
11
|
+
"@futuretea/postgres-mcp-server-darwin-arm64": "0.3.0",
|
|
12
|
+
"@futuretea/postgres-mcp-server-linux-amd64": "0.3.0",
|
|
13
|
+
"@futuretea/postgres-mcp-server-linux-arm64": "0.3.0",
|
|
14
|
+
"@futuretea/postgres-mcp-server-windows-amd64": "0.3.0"
|
|
15
15
|
},
|
|
16
16
|
"repository": {
|
|
17
17
|
"type": "git",
|