@universal-mcp-toolkit/server-postgresql 0.1.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/.well-known/mcp-server.json +35 -0
- package/LICENSE +21 -0
- package/dist/index.d.ts +96 -0
- package/dist/index.js +556 -0
- package/package.json +54 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/json",
|
|
3
|
+
"name": "postgresql",
|
|
4
|
+
"title": "PostgreSQL MCP Server",
|
|
5
|
+
"description": "Schema inspection and safe query tools for PostgreSQL.",
|
|
6
|
+
"version": "0.1.0",
|
|
7
|
+
"transports": [
|
|
8
|
+
"stdio",
|
|
9
|
+
"http+sse"
|
|
10
|
+
],
|
|
11
|
+
"authentication": {
|
|
12
|
+
"mode": "environment-variables",
|
|
13
|
+
"required": [
|
|
14
|
+
"POSTGRESQL_URL"
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"capabilities": {
|
|
18
|
+
"tools": true,
|
|
19
|
+
"resources": true,
|
|
20
|
+
"prompts": true
|
|
21
|
+
},
|
|
22
|
+
"packageName": "@universal-mcp-toolkit/server-postgresql",
|
|
23
|
+
"homepage": "https://github.com/universal-mcp-toolkit/universal-mcp-toolkit#readme",
|
|
24
|
+
"tools": [
|
|
25
|
+
"list-tables",
|
|
26
|
+
"describe-table",
|
|
27
|
+
"run-query"
|
|
28
|
+
],
|
|
29
|
+
"resources": [
|
|
30
|
+
"schema-overview"
|
|
31
|
+
],
|
|
32
|
+
"prompts": [
|
|
33
|
+
"query-review"
|
|
34
|
+
]
|
|
35
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 universal-mcp-toolkit
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as _universal_mcp_toolkit_core from '@universal-mcp-toolkit/core';
|
|
2
|
+
import { ToolkitServer, ToolkitServerMetadata } from '@universal-mcp-toolkit/core';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
type JsonPrimitive = boolean | number | string | null;
|
|
6
|
+
interface JsonObject {
|
|
7
|
+
[key: string]: JsonValue;
|
|
8
|
+
}
|
|
9
|
+
type JsonValue = JsonPrimitive | JsonObject | JsonValue[];
|
|
10
|
+
declare const postgresqlEnvShape: {
|
|
11
|
+
POSTGRESQL_URL: z.ZodString;
|
|
12
|
+
POSTGRESQL_SCHEMA: z.ZodDefault<z.ZodString>;
|
|
13
|
+
POSTGRESQL_ALLOW_WRITES: z.ZodPipe<z.ZodDefault<z.ZodEnum<{
|
|
14
|
+
true: "true";
|
|
15
|
+
false: "false";
|
|
16
|
+
}>>, z.ZodTransform<boolean, "true" | "false">>;
|
|
17
|
+
POSTGRESQL_SSL: z.ZodPipe<z.ZodDefault<z.ZodEnum<{
|
|
18
|
+
true: "true";
|
|
19
|
+
false: "false";
|
|
20
|
+
}>>, z.ZodTransform<boolean, "true" | "false">>;
|
|
21
|
+
POSTGRESQL_MAX_RESULT_ROWS: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
22
|
+
POSTGRESQL_RESOURCE_TABLE_LIMIT: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
23
|
+
};
|
|
24
|
+
type PostgreSqlEnv = z.infer<z.ZodObject<typeof postgresqlEnvShape>>;
|
|
25
|
+
type SqlParameter = boolean | number | string | null;
|
|
26
|
+
interface PostgreSqlTableSummary {
|
|
27
|
+
schema: string;
|
|
28
|
+
name: string;
|
|
29
|
+
type: string;
|
|
30
|
+
}
|
|
31
|
+
interface PostgreSqlColumnSummary {
|
|
32
|
+
name: string;
|
|
33
|
+
ordinalPosition: number;
|
|
34
|
+
dataType: string;
|
|
35
|
+
isNullable: boolean;
|
|
36
|
+
defaultValue: string | null;
|
|
37
|
+
maxLength: number | null;
|
|
38
|
+
numericPrecision: number | null;
|
|
39
|
+
numericScale: number | null;
|
|
40
|
+
comment: string | null;
|
|
41
|
+
}
|
|
42
|
+
interface PostgreSqlConstraintSummary {
|
|
43
|
+
name: string;
|
|
44
|
+
type: string;
|
|
45
|
+
columns: string[];
|
|
46
|
+
}
|
|
47
|
+
interface PostgreSqlFieldSummary {
|
|
48
|
+
name: string;
|
|
49
|
+
dataType: string | null;
|
|
50
|
+
}
|
|
51
|
+
interface PostgreSqlQueryExecution {
|
|
52
|
+
rowCount: number | null;
|
|
53
|
+
fields: PostgreSqlFieldSummary[];
|
|
54
|
+
rows: JsonObject[];
|
|
55
|
+
}
|
|
56
|
+
interface PostgreSqlClient {
|
|
57
|
+
listTables(input: {
|
|
58
|
+
schema: string | null;
|
|
59
|
+
includeSystemSchemas: boolean;
|
|
60
|
+
limit: number;
|
|
61
|
+
}): Promise<{
|
|
62
|
+
database: string;
|
|
63
|
+
tables: PostgreSqlTableSummary[];
|
|
64
|
+
}>;
|
|
65
|
+
describeTable(input: {
|
|
66
|
+
schema: string;
|
|
67
|
+
table: string;
|
|
68
|
+
}): Promise<{
|
|
69
|
+
database: string;
|
|
70
|
+
schema: string;
|
|
71
|
+
table: string;
|
|
72
|
+
columns: PostgreSqlColumnSummary[];
|
|
73
|
+
constraints: PostgreSqlConstraintSummary[];
|
|
74
|
+
}>;
|
|
75
|
+
runQuery(input: {
|
|
76
|
+
sql: string;
|
|
77
|
+
params: readonly SqlParameter[];
|
|
78
|
+
}): Promise<PostgreSqlQueryExecution>;
|
|
79
|
+
close?(): Promise<void>;
|
|
80
|
+
}
|
|
81
|
+
declare const metadata: ToolkitServerMetadata;
|
|
82
|
+
declare const serverCard: _universal_mcp_toolkit_core.ToolkitServerCard;
|
|
83
|
+
declare class PostgreSqlServer extends ToolkitServer {
|
|
84
|
+
private readonly env;
|
|
85
|
+
private readonly client;
|
|
86
|
+
constructor(env: PostgreSqlEnv, client: PostgreSqlClient);
|
|
87
|
+
close(): Promise<void>;
|
|
88
|
+
}
|
|
89
|
+
interface CreatePostgreSqlServerOptions {
|
|
90
|
+
env?: PostgreSqlEnv;
|
|
91
|
+
client?: PostgreSqlClient;
|
|
92
|
+
}
|
|
93
|
+
declare function createServer(options?: CreatePostgreSqlServerOptions): PostgreSqlServer;
|
|
94
|
+
declare function main(argv?: readonly string[]): Promise<void>;
|
|
95
|
+
|
|
96
|
+
export { type CreatePostgreSqlServerOptions, type JsonObject, type JsonValue, type PostgreSqlClient, type PostgreSqlColumnSummary, type PostgreSqlConstraintSummary, type PostgreSqlEnv, type PostgreSqlFieldSummary, type PostgreSqlQueryExecution, PostgreSqlServer, type PostgreSqlTableSummary, createServer, main, metadata, serverCard };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { createRequire } from "module";
|
|
3
|
+
import { pathToFileURL } from "url";
|
|
4
|
+
import {
|
|
5
|
+
ExternalServiceError,
|
|
6
|
+
ToolkitServer,
|
|
7
|
+
ValidationError,
|
|
8
|
+
createServerCard,
|
|
9
|
+
defineTool,
|
|
10
|
+
loadEnv,
|
|
11
|
+
parseRuntimeOptions,
|
|
12
|
+
runToolkitServer
|
|
13
|
+
} from "@universal-mcp-toolkit/core";
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
var require2 = createRequire(import.meta.url);
|
|
16
|
+
var jsonValueSchema = z.lazy(
|
|
17
|
+
() => z.union([z.string(), z.number().finite(), z.boolean(), z.null(), z.array(jsonValueSchema), z.record(z.string(), jsonValueSchema)])
|
|
18
|
+
);
|
|
19
|
+
var jsonObjectSchema = z.record(z.string(), jsonValueSchema);
|
|
20
|
+
var booleanFlag = z.enum(["true", "false"]).default("false").transform((value) => value === "true");
|
|
21
|
+
var postgresqlEnvShape = {
|
|
22
|
+
POSTGRESQL_URL: z.string().min(1),
|
|
23
|
+
POSTGRESQL_SCHEMA: z.string().min(1).default("public"),
|
|
24
|
+
POSTGRESQL_ALLOW_WRITES: booleanFlag,
|
|
25
|
+
POSTGRESQL_SSL: booleanFlag,
|
|
26
|
+
POSTGRESQL_MAX_RESULT_ROWS: z.coerce.number().int().positive().max(1e3).default(200),
|
|
27
|
+
POSTGRESQL_RESOURCE_TABLE_LIMIT: z.coerce.number().int().positive().max(200).default(50)
|
|
28
|
+
};
|
|
29
|
+
var TOOL_NAMES = ["describe-table", "list-tables", "run-query"];
|
|
30
|
+
var RESOURCE_NAMES = ["schema-overview"];
|
|
31
|
+
var PROMPT_NAMES = ["query-review"];
|
|
32
|
+
var metadata = {
|
|
33
|
+
id: "postgresql",
|
|
34
|
+
title: "PostgreSQL MCP Server",
|
|
35
|
+
description: "Schema inspection and safe query tools for PostgreSQL.",
|
|
36
|
+
version: "0.1.0",
|
|
37
|
+
packageName: "@universal-mcp-toolkit/server-postgresql",
|
|
38
|
+
homepage: "https://github.com/universal-mcp-toolkit/universal-mcp-toolkit#readme",
|
|
39
|
+
repositoryUrl: "https://github.com/universal-mcp-toolkit/universal-mcp-toolkit",
|
|
40
|
+
envVarNames: ["POSTGRESQL_URL"],
|
|
41
|
+
transports: ["stdio", "sse"],
|
|
42
|
+
toolNames: TOOL_NAMES,
|
|
43
|
+
resourceNames: RESOURCE_NAMES,
|
|
44
|
+
promptNames: PROMPT_NAMES
|
|
45
|
+
};
|
|
46
|
+
var serverCard = createServerCard(metadata);
|
|
47
|
+
function extractErrorMessage(error) {
|
|
48
|
+
if (error instanceof Error) {
|
|
49
|
+
return error.message;
|
|
50
|
+
}
|
|
51
|
+
return "Unknown PostgreSQL error.";
|
|
52
|
+
}
|
|
53
|
+
function readString(value) {
|
|
54
|
+
return typeof value === "string" ? value : String(value ?? "");
|
|
55
|
+
}
|
|
56
|
+
function readNumberOrNull(value) {
|
|
57
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
if (typeof value === "string" && value.length > 0) {
|
|
61
|
+
const parsed = Number(value);
|
|
62
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
function readBooleanFlag(value) {
|
|
67
|
+
if (typeof value === "boolean") {
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
if (typeof value === "string") {
|
|
71
|
+
const normalized = value.toLowerCase();
|
|
72
|
+
return normalized === "true" || normalized === "yes";
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
function readStringArray(value) {
|
|
77
|
+
if (!Array.isArray(value)) {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
return value.map((entry) => String(entry));
|
|
81
|
+
}
|
|
82
|
+
function hasToHexString(value) {
|
|
83
|
+
return "toHexString" in value && typeof value.toHexString === "function";
|
|
84
|
+
}
|
|
85
|
+
function hasToJson(value) {
|
|
86
|
+
return "toJSON" in value && typeof value.toJSON === "function";
|
|
87
|
+
}
|
|
88
|
+
function sanitizeJsonValue(value) {
|
|
89
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") {
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
if (typeof value === "number") {
|
|
93
|
+
return Number.isFinite(value) ? value : String(value);
|
|
94
|
+
}
|
|
95
|
+
if (typeof value === "bigint") {
|
|
96
|
+
return value.toString();
|
|
97
|
+
}
|
|
98
|
+
if (value instanceof Date) {
|
|
99
|
+
return value.toISOString();
|
|
100
|
+
}
|
|
101
|
+
if (value instanceof Uint8Array) {
|
|
102
|
+
return Buffer.from(value).toString("base64");
|
|
103
|
+
}
|
|
104
|
+
if (Array.isArray(value)) {
|
|
105
|
+
return value.map((entry) => sanitizeJsonValue(entry));
|
|
106
|
+
}
|
|
107
|
+
if (typeof value === "object") {
|
|
108
|
+
if (hasToHexString(value)) {
|
|
109
|
+
return value.toHexString();
|
|
110
|
+
}
|
|
111
|
+
if (hasToJson(value)) {
|
|
112
|
+
const jsonValue = value.toJSON();
|
|
113
|
+
if (jsonValue !== value) {
|
|
114
|
+
return sanitizeJsonValue(jsonValue);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const result = {};
|
|
118
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
119
|
+
result[key] = sanitizeJsonValue(entry);
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
return String(value);
|
|
124
|
+
}
|
|
125
|
+
function normalizeSql(sql) {
|
|
126
|
+
return sql.replace(/^(\s|--[^\r\n]*[\r\n]+|\/\*[\s\S]*?\*\/)*/u, "").trim().replace(/\s+/gu, " ");
|
|
127
|
+
}
|
|
128
|
+
function inferStatementType(sql) {
|
|
129
|
+
const match = normalizeSql(sql).match(/^([a-z]+)/iu);
|
|
130
|
+
const statement = match?.[1];
|
|
131
|
+
return statement ? statement.toUpperCase() : "UNKNOWN";
|
|
132
|
+
}
|
|
133
|
+
function isPotentiallyMutatingSql(sql) {
|
|
134
|
+
const normalized = normalizeSql(sql);
|
|
135
|
+
if (normalized.length === 0) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
const readOnlyStart = /^(select|with|show|explain|values)\b/iu;
|
|
139
|
+
const writePattern = /\b(insert|update|delete|merge|create|alter|drop|truncate|grant|revoke|vacuum|call|copy|do|set|reset|refresh|reindex|cluster|comment|lock)\b/iu;
|
|
140
|
+
if (!readOnlyStart.test(normalized)) {
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
if (writePattern.test(normalized)) {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
return /^select\b[\s\S]*\binto\b/iu.test(normalized);
|
|
147
|
+
}
|
|
148
|
+
function getConnectionSummary(connectionString) {
|
|
149
|
+
try {
|
|
150
|
+
const url = new URL(connectionString);
|
|
151
|
+
const database = url.pathname.replace(/^\/+/u, "") || "postgres";
|
|
152
|
+
return {
|
|
153
|
+
database,
|
|
154
|
+
host: url.host || "localhost"
|
|
155
|
+
};
|
|
156
|
+
} catch {
|
|
157
|
+
return {
|
|
158
|
+
database: "unknown",
|
|
159
|
+
host: "unknown"
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
var NodePostgreSqlClient = class {
|
|
164
|
+
constructor(env) {
|
|
165
|
+
this.env = env;
|
|
166
|
+
const clientConfig = {
|
|
167
|
+
connectionString: env.POSTGRESQL_URL
|
|
168
|
+
};
|
|
169
|
+
if (env.POSTGRESQL_SSL) {
|
|
170
|
+
clientConfig.ssl = {
|
|
171
|
+
rejectUnauthorized: false
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const { Client } = require2("pg");
|
|
175
|
+
this.client = new Client(clientConfig);
|
|
176
|
+
this.database = getConnectionSummary(env.POSTGRESQL_URL).database;
|
|
177
|
+
}
|
|
178
|
+
database;
|
|
179
|
+
client;
|
|
180
|
+
connected = false;
|
|
181
|
+
async close() {
|
|
182
|
+
if (!this.connected) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
await this.client.end();
|
|
186
|
+
this.connected = false;
|
|
187
|
+
}
|
|
188
|
+
async listTables(input) {
|
|
189
|
+
const result = await this.execute(
|
|
190
|
+
`
|
|
191
|
+
select
|
|
192
|
+
table_schema,
|
|
193
|
+
table_name,
|
|
194
|
+
table_type
|
|
195
|
+
from information_schema.tables
|
|
196
|
+
where ($1::text is null or table_schema = $1)
|
|
197
|
+
and ($2::boolean or table_schema not in ('information_schema', 'pg_catalog'))
|
|
198
|
+
order by table_schema asc, table_name asc
|
|
199
|
+
limit $3
|
|
200
|
+
`,
|
|
201
|
+
[input.schema, input.includeSystemSchemas, input.limit]
|
|
202
|
+
);
|
|
203
|
+
return {
|
|
204
|
+
database: this.database,
|
|
205
|
+
tables: result.rows.map((row) => ({
|
|
206
|
+
schema: readString(row.table_schema),
|
|
207
|
+
name: readString(row.table_name),
|
|
208
|
+
type: readString(row.table_type)
|
|
209
|
+
}))
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
async describeTable(input) {
|
|
213
|
+
const columnsResult = await this.execute(
|
|
214
|
+
`
|
|
215
|
+
select
|
|
216
|
+
cols.column_name,
|
|
217
|
+
cols.ordinal_position,
|
|
218
|
+
cols.data_type,
|
|
219
|
+
cols.is_nullable,
|
|
220
|
+
cols.column_default,
|
|
221
|
+
cols.character_maximum_length,
|
|
222
|
+
cols.numeric_precision,
|
|
223
|
+
cols.numeric_scale,
|
|
224
|
+
pgd.description as comment
|
|
225
|
+
from information_schema.columns cols
|
|
226
|
+
left join pg_catalog.pg_statio_all_tables st
|
|
227
|
+
on st.schemaname = cols.table_schema
|
|
228
|
+
and st.relname = cols.table_name
|
|
229
|
+
left join pg_catalog.pg_description pgd
|
|
230
|
+
on pgd.objoid = st.relid
|
|
231
|
+
and pgd.objsubid = cols.ordinal_position
|
|
232
|
+
where cols.table_schema = $1
|
|
233
|
+
and cols.table_name = $2
|
|
234
|
+
order by cols.ordinal_position asc
|
|
235
|
+
`,
|
|
236
|
+
[input.schema, input.table]
|
|
237
|
+
);
|
|
238
|
+
if (columnsResult.rows.length === 0) {
|
|
239
|
+
throw new ValidationError(`Table '${input.schema}.${input.table}' was not found.`);
|
|
240
|
+
}
|
|
241
|
+
const constraintsResult = await this.execute(
|
|
242
|
+
`
|
|
243
|
+
select
|
|
244
|
+
tc.constraint_name,
|
|
245
|
+
tc.constraint_type,
|
|
246
|
+
array_remove(array_agg(kcu.column_name order by kcu.ordinal_position), null) as columns
|
|
247
|
+
from information_schema.table_constraints tc
|
|
248
|
+
left join information_schema.key_column_usage kcu
|
|
249
|
+
on tc.constraint_name = kcu.constraint_name
|
|
250
|
+
and tc.table_schema = kcu.table_schema
|
|
251
|
+
and tc.table_name = kcu.table_name
|
|
252
|
+
where tc.table_schema = $1
|
|
253
|
+
and tc.table_name = $2
|
|
254
|
+
group by tc.constraint_name, tc.constraint_type
|
|
255
|
+
order by tc.constraint_name asc
|
|
256
|
+
`,
|
|
257
|
+
[input.schema, input.table]
|
|
258
|
+
);
|
|
259
|
+
return {
|
|
260
|
+
database: this.database,
|
|
261
|
+
schema: input.schema,
|
|
262
|
+
table: input.table,
|
|
263
|
+
columns: columnsResult.rows.map((row) => ({
|
|
264
|
+
name: readString(row.column_name),
|
|
265
|
+
ordinalPosition: readNumberOrNull(row.ordinal_position) ?? 0,
|
|
266
|
+
dataType: readString(row.data_type),
|
|
267
|
+
isNullable: readBooleanFlag(row.is_nullable),
|
|
268
|
+
defaultValue: row.column_default === null ? null : readString(row.column_default),
|
|
269
|
+
maxLength: readNumberOrNull(row.character_maximum_length),
|
|
270
|
+
numericPrecision: readNumberOrNull(row.numeric_precision),
|
|
271
|
+
numericScale: readNumberOrNull(row.numeric_scale),
|
|
272
|
+
comment: row.comment === null ? null : readString(row.comment)
|
|
273
|
+
})),
|
|
274
|
+
constraints: constraintsResult.rows.map((row) => ({
|
|
275
|
+
name: readString(row.constraint_name),
|
|
276
|
+
type: readString(row.constraint_type),
|
|
277
|
+
columns: readStringArray(row.columns)
|
|
278
|
+
}))
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
async runQuery(input) {
|
|
282
|
+
const result = await this.execute(input.sql, input.params);
|
|
283
|
+
return {
|
|
284
|
+
rowCount: result.rowCount,
|
|
285
|
+
fields: result.fields.map((field) => ({
|
|
286
|
+
name: field.name,
|
|
287
|
+
dataType: null
|
|
288
|
+
})),
|
|
289
|
+
rows: result.rows.map((row) => sanitizeJsonValue(row))
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
async ensureConnected() {
|
|
293
|
+
if (this.connected) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
await this.client.connect();
|
|
298
|
+
this.connected = true;
|
|
299
|
+
} catch (error) {
|
|
300
|
+
throw new ExternalServiceError("Failed to connect to PostgreSQL.", {
|
|
301
|
+
details: extractErrorMessage(error)
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
async execute(text, values = []) {
|
|
306
|
+
await this.ensureConnected();
|
|
307
|
+
try {
|
|
308
|
+
return await this.client.query(text, values);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
throw new ExternalServiceError("PostgreSQL query failed.", {
|
|
311
|
+
details: extractErrorMessage(error)
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
var PostgreSqlServer = class extends ToolkitServer {
|
|
317
|
+
constructor(env, client) {
|
|
318
|
+
super(metadata);
|
|
319
|
+
this.env = env;
|
|
320
|
+
this.client = client;
|
|
321
|
+
this.registerTool(
|
|
322
|
+
defineTool({
|
|
323
|
+
name: "list-tables",
|
|
324
|
+
title: "List PostgreSQL tables",
|
|
325
|
+
description: "List tables and views visible to the configured PostgreSQL connection.",
|
|
326
|
+
inputSchema: {
|
|
327
|
+
schema: z.string().min(1).optional(),
|
|
328
|
+
includeSystemSchemas: z.boolean().default(false),
|
|
329
|
+
limit: z.number().int().positive().max(200).default(50)
|
|
330
|
+
},
|
|
331
|
+
outputSchema: {
|
|
332
|
+
database: z.string(),
|
|
333
|
+
schemaFilter: z.string().nullable(),
|
|
334
|
+
tableCount: z.number().int().nonnegative(),
|
|
335
|
+
tables: z.array(
|
|
336
|
+
z.object({
|
|
337
|
+
schema: z.string(),
|
|
338
|
+
name: z.string(),
|
|
339
|
+
type: z.string()
|
|
340
|
+
})
|
|
341
|
+
)
|
|
342
|
+
},
|
|
343
|
+
handler: async ({ includeSystemSchemas, limit, schema }, context) => {
|
|
344
|
+
await context.log("info", "Listing PostgreSQL tables.");
|
|
345
|
+
const result = await this.client.listTables({
|
|
346
|
+
schema: schema ?? null,
|
|
347
|
+
includeSystemSchemas,
|
|
348
|
+
limit
|
|
349
|
+
});
|
|
350
|
+
return {
|
|
351
|
+
database: result.database,
|
|
352
|
+
schemaFilter: schema ?? null,
|
|
353
|
+
tableCount: result.tables.length,
|
|
354
|
+
tables: result.tables
|
|
355
|
+
};
|
|
356
|
+
},
|
|
357
|
+
renderText: (output) => `${output.tableCount} tables found in ${output.database}.`
|
|
358
|
+
})
|
|
359
|
+
);
|
|
360
|
+
this.registerTool(
|
|
361
|
+
defineTool({
|
|
362
|
+
name: "describe-table",
|
|
363
|
+
title: "Describe PostgreSQL table",
|
|
364
|
+
description: "Inspect columns and constraints for a PostgreSQL table.",
|
|
365
|
+
inputSchema: {
|
|
366
|
+
schema: z.string().min(1).default(this.env.POSTGRESQL_SCHEMA),
|
|
367
|
+
table: z.string().min(1)
|
|
368
|
+
},
|
|
369
|
+
outputSchema: {
|
|
370
|
+
database: z.string(),
|
|
371
|
+
schema: z.string(),
|
|
372
|
+
table: z.string(),
|
|
373
|
+
columnCount: z.number().int().nonnegative(),
|
|
374
|
+
columns: z.array(
|
|
375
|
+
z.object({
|
|
376
|
+
name: z.string(),
|
|
377
|
+
ordinalPosition: z.number().int().nonnegative(),
|
|
378
|
+
dataType: z.string(),
|
|
379
|
+
isNullable: z.boolean(),
|
|
380
|
+
defaultValue: z.string().nullable(),
|
|
381
|
+
maxLength: z.number().int().nonnegative().nullable(),
|
|
382
|
+
numericPrecision: z.number().int().nonnegative().nullable(),
|
|
383
|
+
numericScale: z.number().int().nonnegative().nullable(),
|
|
384
|
+
comment: z.string().nullable()
|
|
385
|
+
})
|
|
386
|
+
),
|
|
387
|
+
constraints: z.array(
|
|
388
|
+
z.object({
|
|
389
|
+
name: z.string(),
|
|
390
|
+
type: z.string(),
|
|
391
|
+
columns: z.array(z.string())
|
|
392
|
+
})
|
|
393
|
+
)
|
|
394
|
+
},
|
|
395
|
+
handler: async ({ schema, table }, context) => {
|
|
396
|
+
await context.log("info", `Describing ${schema}.${table}.`);
|
|
397
|
+
const result = await this.client.describeTable({ schema, table });
|
|
398
|
+
return {
|
|
399
|
+
database: result.database,
|
|
400
|
+
schema: result.schema,
|
|
401
|
+
table: result.table,
|
|
402
|
+
columnCount: result.columns.length,
|
|
403
|
+
columns: result.columns,
|
|
404
|
+
constraints: result.constraints
|
|
405
|
+
};
|
|
406
|
+
},
|
|
407
|
+
renderText: (output) => `${output.schema}.${output.table} has ${output.columnCount} columns.`
|
|
408
|
+
})
|
|
409
|
+
);
|
|
410
|
+
this.registerTool(
|
|
411
|
+
defineTool({
|
|
412
|
+
name: "run-query",
|
|
413
|
+
title: "Run PostgreSQL query",
|
|
414
|
+
description: "Run a PostgreSQL query with a read-only-by-default safety guard.",
|
|
415
|
+
inputSchema: {
|
|
416
|
+
sql: z.string().min(1),
|
|
417
|
+
params: z.array(z.union([z.string(), z.number().finite(), z.boolean(), z.null()])).default([]),
|
|
418
|
+
allowWrite: z.boolean().default(false)
|
|
419
|
+
},
|
|
420
|
+
outputSchema: {
|
|
421
|
+
statementType: z.string(),
|
|
422
|
+
rowCount: z.number().int().nonnegative().nullable(),
|
|
423
|
+
fieldCount: z.number().int().nonnegative(),
|
|
424
|
+
fields: z.array(
|
|
425
|
+
z.object({
|
|
426
|
+
name: z.string(),
|
|
427
|
+
dataType: z.string().nullable()
|
|
428
|
+
})
|
|
429
|
+
),
|
|
430
|
+
returnedRows: z.number().int().nonnegative(),
|
|
431
|
+
truncated: z.boolean(),
|
|
432
|
+
rows: z.array(jsonObjectSchema)
|
|
433
|
+
},
|
|
434
|
+
handler: async ({ allowWrite, params, sql }, context) => {
|
|
435
|
+
const statementType = inferStatementType(sql);
|
|
436
|
+
const mutating = isPotentiallyMutatingSql(sql);
|
|
437
|
+
if (mutating && (!this.env.POSTGRESQL_ALLOW_WRITES || !allowWrite)) {
|
|
438
|
+
throw new ValidationError(
|
|
439
|
+
"Mutating SQL is blocked by default. Set POSTGRESQL_ALLOW_WRITES=true and pass allowWrite=true to run write statements."
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
if (mutating) {
|
|
443
|
+
await context.log("warning", "Executing an opt-in write query against PostgreSQL.");
|
|
444
|
+
} else {
|
|
445
|
+
await context.log("info", `Executing read-only ${statementType} query.`);
|
|
446
|
+
}
|
|
447
|
+
const execution = await this.client.runQuery({ sql, params });
|
|
448
|
+
const rows = execution.rows.slice(0, this.env.POSTGRESQL_MAX_RESULT_ROWS);
|
|
449
|
+
return {
|
|
450
|
+
statementType,
|
|
451
|
+
rowCount: execution.rowCount,
|
|
452
|
+
fieldCount: execution.fields.length,
|
|
453
|
+
fields: execution.fields,
|
|
454
|
+
returnedRows: rows.length,
|
|
455
|
+
truncated: execution.rows.length > rows.length,
|
|
456
|
+
rows
|
|
457
|
+
};
|
|
458
|
+
},
|
|
459
|
+
renderText: (output) => `${output.statementType} returned ${output.returnedRows} row(s).`
|
|
460
|
+
})
|
|
461
|
+
);
|
|
462
|
+
this.registerStaticResource(
|
|
463
|
+
"schema-overview",
|
|
464
|
+
"postgresql://schema-overview",
|
|
465
|
+
{
|
|
466
|
+
title: "PostgreSQL schema overview",
|
|
467
|
+
description: "Connection summary and a snapshot of tables from the default schema.",
|
|
468
|
+
mimeType: "application/json"
|
|
469
|
+
},
|
|
470
|
+
async () => {
|
|
471
|
+
const connection = getConnectionSummary(this.env.POSTGRESQL_URL);
|
|
472
|
+
const tables = await this.client.listTables({
|
|
473
|
+
schema: this.env.POSTGRESQL_SCHEMA,
|
|
474
|
+
includeSystemSchemas: false,
|
|
475
|
+
limit: this.env.POSTGRESQL_RESOURCE_TABLE_LIMIT
|
|
476
|
+
});
|
|
477
|
+
return this.createJsonResource("postgresql://schema-overview", {
|
|
478
|
+
connection,
|
|
479
|
+
defaultSchema: this.env.POSTGRESQL_SCHEMA,
|
|
480
|
+
writeQueriesEnabled: this.env.POSTGRESQL_ALLOW_WRITES,
|
|
481
|
+
tables: tables.tables
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
);
|
|
485
|
+
this.registerPrompt(
|
|
486
|
+
"query-review",
|
|
487
|
+
{
|
|
488
|
+
title: "PostgreSQL query review",
|
|
489
|
+
description: "Generate a review checklist for a PostgreSQL query before execution.",
|
|
490
|
+
argsSchema: {
|
|
491
|
+
objective: z.string().min(1),
|
|
492
|
+
sql: z.string().min(1),
|
|
493
|
+
allowWrite: z.boolean().default(false)
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
async ({ allowWrite, objective, sql }) => this.createTextPrompt(
|
|
497
|
+
[
|
|
498
|
+
"Review this PostgreSQL query before it is executed.",
|
|
499
|
+
`Objective: ${objective}`,
|
|
500
|
+
`Default schema: ${this.env.POSTGRESQL_SCHEMA}`,
|
|
501
|
+
`Write access enabled: ${this.env.POSTGRESQL_ALLOW_WRITES}`,
|
|
502
|
+
`Caller requested write execution: ${allowWrite}`,
|
|
503
|
+
"Check for:",
|
|
504
|
+
"- unexpected table scans or missing predicates",
|
|
505
|
+
"- whether LIMIT, ORDER BY, or EXPLAIN would improve safety",
|
|
506
|
+
"- lock or mutation risks",
|
|
507
|
+
"- whether the SQL matches the stated objective",
|
|
508
|
+
"",
|
|
509
|
+
"SQL:",
|
|
510
|
+
sql
|
|
511
|
+
].join("\n")
|
|
512
|
+
)
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
async close() {
|
|
516
|
+
await this.client.close?.();
|
|
517
|
+
await super.close();
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
function createServer(options = {}) {
|
|
521
|
+
const env = options.env ?? loadEnv(postgresqlEnvShape);
|
|
522
|
+
const client = options.client ?? new NodePostgreSqlClient(env);
|
|
523
|
+
return new PostgreSqlServer(env, client);
|
|
524
|
+
}
|
|
525
|
+
async function main(argv = process.argv.slice(2)) {
|
|
526
|
+
const env = loadEnv(postgresqlEnvShape);
|
|
527
|
+
const runtimeOptions = parseRuntimeOptions(argv);
|
|
528
|
+
await runToolkitServer(
|
|
529
|
+
{
|
|
530
|
+
createServer: () => createServer({ env }),
|
|
531
|
+
serverCard
|
|
532
|
+
},
|
|
533
|
+
runtimeOptions
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
function isMainModule() {
|
|
537
|
+
const entryPoint = process.argv[1];
|
|
538
|
+
if (!entryPoint) {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
return import.meta.url === pathToFileURL(entryPoint).href;
|
|
542
|
+
}
|
|
543
|
+
if (isMainModule()) {
|
|
544
|
+
void main().catch((error) => {
|
|
545
|
+
const message = error instanceof Error ? error.message : "Unknown startup error.";
|
|
546
|
+
console.error(`Failed to start PostgreSQL MCP server: ${message}`);
|
|
547
|
+
process.exitCode = 1;
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
export {
|
|
551
|
+
PostgreSqlServer,
|
|
552
|
+
createServer,
|
|
553
|
+
main,
|
|
554
|
+
metadata,
|
|
555
|
+
serverCard
|
|
556
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@universal-mcp-toolkit/server-postgresql",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Schema inspection and safe query tools for PostgreSQL.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"server-postgresql": "./dist/index.js",
|
|
9
|
+
"umt-postgresql": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/Markgatcha/universal-mcp-toolkit.git"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/Markgatcha/universal-mcp-toolkit#readme",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
".well-known"
|
|
26
|
+
],
|
|
27
|
+
"keywords": [
|
|
28
|
+
"mcp",
|
|
29
|
+
"model-context-protocol",
|
|
30
|
+
"ai",
|
|
31
|
+
"developer-tools",
|
|
32
|
+
"typescript",
|
|
33
|
+
"postgresql",
|
|
34
|
+
"database",
|
|
35
|
+
"sql",
|
|
36
|
+
"schema"
|
|
37
|
+
],
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@universal-mcp-toolkit/core": "0.1.0",
|
|
40
|
+
"pg": "^8.20.0",
|
|
41
|
+
"zod": "^4.3.6"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
48
|
+
"dev": "tsx watch src/index.ts",
|
|
49
|
+
"lint": "tsc --noEmit",
|
|
50
|
+
"typecheck": "tsc --noEmit",
|
|
51
|
+
"test": "vitest run --passWithNoTests",
|
|
52
|
+
"clean": "rimraf dist"
|
|
53
|
+
}
|
|
54
|
+
}
|