@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.
Files changed (2) hide show
  1. package/README.md +90 -7
  2. 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 a connection target for a minimally privileged PostgreSQL role, then build and inspect the catalog:
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 DATABASE_URL='postgres://reader@127.0.0.1:5432/app?sslmode=disable'
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 setting through the Docker process:
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
- Use a bare `DATABASE_URL`, or standard PostgreSQL environment variables such as `PGHOST`, `PGPORT`, `PGDATABASE`, `PGUSER`, `PGPASSWORD`, and `PGSSLMODE`. When `DATABASE_URL` is set it takes precedence. Configuration must resolve to one `host:port`; distinct URL or comma-separated `PGHOST` targets are rejected before a connection is created.
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 `DATABASE_URL`, verify that `mcp_reader` cannot create objects in the target schema:
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 `DATABASE_URL`, connect as `mcp_reader` and verify that every boolean below is `false`:
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 --env DATABASE_URL postgres-mcp-server:dev
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.1.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.1.0",
11
- "@futuretea/postgres-mcp-server-darwin-arm64": "0.1.0",
12
- "@futuretea/postgres-mcp-server-linux-amd64": "0.1.0",
13
- "@futuretea/postgres-mcp-server-linux-arm64": "0.1.0",
14
- "@futuretea/postgres-mcp-server-windows-amd64": "0.1.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",