easy-pg-admin-mcp 0.1.0 → 0.1.2
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 +1 -0
- package/README.zh-TW.md +1 -0
- package/build/config.js +1 -0
- package/build/db.js +4 -0
- package/build/index.js +30 -20
- package/build/toolHandlers.js +4 -1
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -56,6 +56,7 @@ Use environment variables, matching the rest of the `easy-*-mcp` family.
|
|
|
56
56
|
| `PG_PASSWORD` | No | - | PostgreSQL password |
|
|
57
57
|
| `PG_DATABASE` | Conditional | - | Default database used for the admin connection |
|
|
58
58
|
| `PG_CONNECTION_LIMIT` | No | `10` | Maximum number of active pool connections |
|
|
59
|
+
| `PG_CONNECTION_TIMEOUT` | No | `10000` | Connection establishment timeout in milliseconds |
|
|
59
60
|
| `PG_IDLE_TIMEOUT` | No | `30000` | Idle connection timeout in milliseconds |
|
|
60
61
|
| `PG_ENABLE_KEEP_ALIVE` | No | `true` | Whether TCP keep-alive is enabled |
|
|
61
62
|
| `PG_KEEP_ALIVE_INITIAL_DELAY` | No | `0` | Initial TCP keep-alive delay in milliseconds |
|
package/README.zh-TW.md
CHANGED
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
| `PG_PASSWORD` | 否 | - | PostgreSQL 密碼 |
|
|
57
57
|
| `PG_DATABASE` | 條件必填 | - | 管理連線使用的預設 database |
|
|
58
58
|
| `PG_CONNECTION_LIMIT` | 否 | `10` | pool 最大 active connections |
|
|
59
|
+
| `PG_CONNECTION_TIMEOUT` | 否 | `10000` | 建立 PostgreSQL 連線的逾時時間,單位毫秒 |
|
|
59
60
|
| `PG_IDLE_TIMEOUT` | 否 | `30000` | idle connection timeout,單位毫秒 |
|
|
60
61
|
| `PG_ENABLE_KEEP_ALIVE` | 否 | `true` | 是否啟用 TCP keep-alive |
|
|
61
62
|
| `PG_KEEP_ALIVE_INITIAL_DELAY` | 否 | `0` | TCP keep-alive 初始延遲,單位毫秒 |
|
package/build/config.js
CHANGED
|
@@ -29,6 +29,7 @@ export function parseAdminConfig(env) {
|
|
|
29
29
|
password: env.PG_PASSWORD,
|
|
30
30
|
database: env.PG_DATABASE ?? '',
|
|
31
31
|
connectionLimit: parsePositiveInt(env.PG_CONNECTION_LIMIT, 10),
|
|
32
|
+
connectionTimeout: parsePositiveInt(env.PG_CONNECTION_TIMEOUT, 10000),
|
|
32
33
|
idleTimeout: parsePositiveInt(env.PG_IDLE_TIMEOUT, 30000),
|
|
33
34
|
enableKeepAlive: parseBoolean(env.PG_ENABLE_KEEP_ALIVE, true),
|
|
34
35
|
keepAliveInitialDelay: env.PG_KEEP_ALIVE_INITIAL_DELAY ? Number.parseInt(env.PG_KEEP_ALIVE_INITIAL_DELAY, 10) : 0,
|
package/build/db.js
CHANGED
|
@@ -17,6 +17,7 @@ export function getPool() {
|
|
|
17
17
|
password: config.pg.connectionString ? undefined : config.pg.password,
|
|
18
18
|
database: config.pg.connectionString ? undefined : config.pg.database,
|
|
19
19
|
max: config.pg.connectionLimit,
|
|
20
|
+
connectionTimeoutMillis: config.pg.connectionTimeout,
|
|
20
21
|
idleTimeoutMillis: config.pg.idleTimeout,
|
|
21
22
|
keepAlive: config.pg.enableKeepAlive,
|
|
22
23
|
keepAliveInitialDelayMillis: config.pg.keepAliveInitialDelay,
|
|
@@ -24,6 +25,9 @@ export function getPool() {
|
|
|
24
25
|
});
|
|
25
26
|
return pool;
|
|
26
27
|
}
|
|
28
|
+
export async function verifyConnection() {
|
|
29
|
+
await getPool().query('SELECT 1');
|
|
30
|
+
}
|
|
27
31
|
export function escapeIdentifier(identifier) {
|
|
28
32
|
return `"${identifier.replace(/"/g, '""')}"`;
|
|
29
33
|
}
|
package/build/index.js
CHANGED
|
@@ -4,8 +4,8 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { parseAdminConfig } from './config.js';
|
|
6
6
|
import { createConfirmationStore } from './confirmationStore.js';
|
|
7
|
-
import { getPool } from './db.js';
|
|
8
|
-
import { createAdminHandlers } from './toolHandlers.js';
|
|
7
|
+
import { getPool, verifyConnection } from './db.js';
|
|
8
|
+
import { createAdminHandlers, formatError } from './toolHandlers.js';
|
|
9
9
|
const config = parseAdminConfig(process.env);
|
|
10
10
|
const store = createConfirmationStore({ defaultTtlMs: config.tokenTtlSeconds * 1000 });
|
|
11
11
|
getPool();
|
|
@@ -25,15 +25,24 @@ const roleAttributesSchema = {
|
|
|
25
25
|
connectionLimit: z.number().int().min(-1).optional(),
|
|
26
26
|
validUntil: z.string().min(1).nullable().optional(),
|
|
27
27
|
};
|
|
28
|
-
function textJson(value) {
|
|
28
|
+
function textJson(value, isError = false) {
|
|
29
29
|
return {
|
|
30
30
|
content: [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
31
|
+
...(isError ? { isError: true } : {}),
|
|
31
32
|
};
|
|
32
33
|
}
|
|
34
|
+
async function safeTextJson(operation) {
|
|
35
|
+
try {
|
|
36
|
+
return textJson(await operation());
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
return textJson(formatError(error), true);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
33
42
|
server.registerTool('pg_list_databases', {
|
|
34
43
|
description: 'List databases in the current PostgreSQL instance.',
|
|
35
44
|
inputSchema: z.object({}),
|
|
36
|
-
}, async () =>
|
|
45
|
+
}, async () => safeTextJson(() => handlers.listDatabases()));
|
|
37
46
|
server.registerTool('pg_create_database', {
|
|
38
47
|
description: 'Create a PostgreSQL database.',
|
|
39
48
|
inputSchema: z.object({
|
|
@@ -43,33 +52,33 @@ server.registerTool('pg_create_database', {
|
|
|
43
52
|
template: z.string().min(1).optional(),
|
|
44
53
|
locale: z.string().min(1).optional(),
|
|
45
54
|
}),
|
|
46
|
-
}, async ({ database, owner, encoding, template, locale }) =>
|
|
55
|
+
}, async ({ database, owner, encoding, template, locale }) => safeTextJson(() => handlers.createDatabase({ database, owner, encoding, template, locale })));
|
|
47
56
|
server.registerTool('pg_describe_database', {
|
|
48
57
|
description: 'Inspect a PostgreSQL database definition.',
|
|
49
58
|
inputSchema: z.object({
|
|
50
59
|
database: z.string().min(1),
|
|
51
60
|
}),
|
|
52
|
-
}, async ({ database }) =>
|
|
61
|
+
}, async ({ database }) => safeTextJson(() => handlers.describeDatabase(database)));
|
|
53
62
|
server.registerTool('pg_alter_database_owner', {
|
|
54
63
|
description: 'Change the owner of a PostgreSQL database.',
|
|
55
64
|
inputSchema: z.object({
|
|
56
65
|
database: z.string().min(1),
|
|
57
66
|
owner: z.string().min(1),
|
|
58
67
|
}),
|
|
59
|
-
}, async ({ database, owner }) =>
|
|
68
|
+
}, async ({ database, owner }) => safeTextJson(() => handlers.alterDatabaseOwner(database, owner)));
|
|
60
69
|
server.registerTool('pg_drop_database', {
|
|
61
70
|
description: 'Request database deletion and return a short-lived confirmation token.',
|
|
62
71
|
inputSchema: z.object({
|
|
63
72
|
database: z.string().min(1),
|
|
64
73
|
force: z.boolean().optional(),
|
|
65
74
|
}),
|
|
66
|
-
}, async ({ database, force }) =>
|
|
75
|
+
}, async ({ database, force }) => safeTextJson(() => handlers.dropDatabase(database, { force })));
|
|
67
76
|
server.registerTool('pg_list_roles', {
|
|
68
77
|
description: 'List PostgreSQL roles.',
|
|
69
78
|
inputSchema: z.object({
|
|
70
79
|
includeSystem: z.boolean().optional(),
|
|
71
80
|
}),
|
|
72
|
-
}, async ({ includeSystem }) =>
|
|
81
|
+
}, async ({ includeSystem }) => safeTextJson(() => handlers.listRoles({ includeSystem })));
|
|
73
82
|
server.registerTool('pg_create_role', {
|
|
74
83
|
description: 'Create a PostgreSQL role. SUPERUSER is intentionally not supported.',
|
|
75
84
|
inputSchema: z.object({
|
|
@@ -77,7 +86,7 @@ server.registerTool('pg_create_role', {
|
|
|
77
86
|
password: z.string().min(1).optional(),
|
|
78
87
|
...roleAttributesSchema,
|
|
79
88
|
}),
|
|
80
|
-
}, async ({ role, password, login, createdb, createrole, inherit, replication, bypassrls, connectionLimit, validUntil }) =>
|
|
89
|
+
}, async ({ role, password, login, createdb, createrole, inherit, replication, bypassrls, connectionLimit, validUntil }) => safeTextJson(() => handlers.createRole({
|
|
81
90
|
role,
|
|
82
91
|
password,
|
|
83
92
|
login,
|
|
@@ -95,14 +104,14 @@ server.registerTool('pg_alter_role_password', {
|
|
|
95
104
|
role: z.string().min(1),
|
|
96
105
|
password: z.string().min(1),
|
|
97
106
|
}),
|
|
98
|
-
}, async ({ role, password }) =>
|
|
107
|
+
}, async ({ role, password }) => safeTextJson(() => handlers.alterRolePassword(role, password)));
|
|
99
108
|
server.registerTool('pg_alter_role_attributes', {
|
|
100
109
|
description: 'Change PostgreSQL role attributes. SUPERUSER is intentionally not supported.',
|
|
101
110
|
inputSchema: z.object({
|
|
102
111
|
role: z.string().min(1),
|
|
103
112
|
...roleAttributesSchema,
|
|
104
113
|
}),
|
|
105
|
-
}, async ({ role, login, createdb, createrole, inherit, replication, bypassrls, connectionLimit, validUntil }) =>
|
|
114
|
+
}, async ({ role, login, createdb, createrole, inherit, replication, bypassrls, connectionLimit, validUntil }) => safeTextJson(() => handlers.alterRoleAttributes(role, {
|
|
106
115
|
login,
|
|
107
116
|
createdb,
|
|
108
117
|
createrole,
|
|
@@ -117,7 +126,7 @@ server.registerTool('pg_drop_role', {
|
|
|
117
126
|
inputSchema: z.object({
|
|
118
127
|
role: z.string().min(1),
|
|
119
128
|
}),
|
|
120
|
-
}, async ({ role }) =>
|
|
129
|
+
}, async ({ role }) => safeTextJson(() => handlers.dropRole(role)));
|
|
121
130
|
server.registerTool('pg_grant_role', {
|
|
122
131
|
description: 'Grant a PostgreSQL role to another role.',
|
|
123
132
|
inputSchema: z.object({
|
|
@@ -125,7 +134,7 @@ server.registerTool('pg_grant_role', {
|
|
|
125
134
|
member: z.string().min(1),
|
|
126
135
|
adminOption: z.boolean().optional(),
|
|
127
136
|
}),
|
|
128
|
-
}, async ({ role, member, adminOption }) =>
|
|
137
|
+
}, async ({ role, member, adminOption }) => safeTextJson(() => handlers.grantRole({ role, member, adminOption })));
|
|
129
138
|
server.registerTool('pg_revoke_role', {
|
|
130
139
|
description: 'Revoke a PostgreSQL role from another role.',
|
|
131
140
|
inputSchema: z.object({
|
|
@@ -133,13 +142,13 @@ server.registerTool('pg_revoke_role', {
|
|
|
133
142
|
member: z.string().min(1),
|
|
134
143
|
adminOption: z.boolean().optional(),
|
|
135
144
|
}),
|
|
136
|
-
}, async ({ role, member, adminOption }) =>
|
|
145
|
+
}, async ({ role, member, adminOption }) => safeTextJson(() => handlers.revokeRole({ role, member, adminOption })));
|
|
137
146
|
server.registerTool('pg_show_role_memberships', {
|
|
138
147
|
description: 'Show memberships for a PostgreSQL role.',
|
|
139
148
|
inputSchema: z.object({
|
|
140
149
|
role: z.string().min(1),
|
|
141
150
|
}),
|
|
142
|
-
}, async ({ role }) =>
|
|
151
|
+
}, async ({ role }) => safeTextJson(() => handlers.showRoleMemberships(role)));
|
|
143
152
|
server.registerTool('pg_grant_privileges', {
|
|
144
153
|
description: 'Grant database-level PostgreSQL privileges to a role.',
|
|
145
154
|
inputSchema: z.object({
|
|
@@ -148,7 +157,7 @@ server.registerTool('pg_grant_privileges', {
|
|
|
148
157
|
privileges: z.array(z.string().min(1)).min(1),
|
|
149
158
|
withGrantOption: z.boolean().optional(),
|
|
150
159
|
}),
|
|
151
|
-
}, async ({ role, database, privileges, withGrantOption }) =>
|
|
160
|
+
}, async ({ role, database, privileges, withGrantOption }) => safeTextJson(() => handlers.grantPrivileges({ role, database, privileges, withGrantOption })));
|
|
152
161
|
server.registerTool('pg_revoke_privileges', {
|
|
153
162
|
description: 'Revoke database-level PostgreSQL privileges from a role.',
|
|
154
163
|
inputSchema: z.object({
|
|
@@ -157,21 +166,22 @@ server.registerTool('pg_revoke_privileges', {
|
|
|
157
166
|
privileges: z.array(z.string().min(1)).min(1),
|
|
158
167
|
grantOptionFor: z.boolean().optional(),
|
|
159
168
|
}),
|
|
160
|
-
}, async ({ role, database, privileges, grantOptionFor }) =>
|
|
169
|
+
}, async ({ role, database, privileges, grantOptionFor }) => safeTextJson(() => handlers.revokePrivileges({ role, database, privileges, grantOptionFor })));
|
|
161
170
|
server.registerTool('pg_show_grants', {
|
|
162
171
|
description: 'Show database-level PostgreSQL grants for a role.',
|
|
163
172
|
inputSchema: z.object({
|
|
164
173
|
role: z.string().min(1),
|
|
165
174
|
database: z.string().min(1).optional(),
|
|
166
175
|
}),
|
|
167
|
-
}, async ({ role, database }) =>
|
|
176
|
+
}, async ({ role, database }) => safeTextJson(() => handlers.showGrants({ role, database })));
|
|
168
177
|
server.registerTool('pg_confirm_task', {
|
|
169
178
|
description: 'Confirm and execute a previously issued destructive action token.',
|
|
170
179
|
inputSchema: z.object({
|
|
171
180
|
token: z.string().min(1),
|
|
172
181
|
}),
|
|
173
|
-
}, async ({ token }) =>
|
|
182
|
+
}, async ({ token }) => safeTextJson(() => handlers.confirmTask(token)));
|
|
174
183
|
async function main() {
|
|
184
|
+
await verifyConnection();
|
|
175
185
|
const transport = new StdioServerTransport();
|
|
176
186
|
await server.connect(transport);
|
|
177
187
|
console.error('easy-pg-admin-mcp running on stdio');
|
package/build/toolHandlers.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { escapeIdentifier, getPool } from './db.js';
|
|
2
2
|
import { assertNoSuperuser, formatPrivilegeList, parseRoleAttributes, validateDatabaseName, validateExistingRoleName, validateRoleName, } from './validation.js';
|
|
3
3
|
export function formatError(error, code = 'ADMIN_OPERATION_FAILED') {
|
|
4
|
+
const postgresCode = error && typeof error === 'object' && 'code' in error && typeof error.code === 'string'
|
|
5
|
+
? error.code
|
|
6
|
+
: undefined;
|
|
4
7
|
return {
|
|
5
8
|
error: error instanceof Error ? error.message : String(error),
|
|
6
|
-
code,
|
|
9
|
+
code: postgresCode ?? code,
|
|
7
10
|
};
|
|
8
11
|
}
|
|
9
12
|
export function createAdminHandlers(store, tokenTtlSeconds) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "easy-pg-admin-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "High privilege PostgreSQL admin MCP server for database and role management",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -45,14 +45,14 @@
|
|
|
45
45
|
"prepack": "npm run build"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
49
|
-
"pg": "^8.
|
|
50
|
-
"zod": "^4.4
|
|
48
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
49
|
+
"pg": "^8.23.0",
|
|
50
|
+
"zod": "^4.5.4"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@types/node": "^
|
|
54
|
-
"@types/pg": "^8.
|
|
55
|
-
"typescript": "^
|
|
53
|
+
"@types/node": "^26.5.0",
|
|
54
|
+
"@types/pg": "^8.23.1",
|
|
55
|
+
"typescript": "^7.0.2"
|
|
56
56
|
},
|
|
57
57
|
"engines": {
|
|
58
58
|
"node": ">=20"
|