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 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 () => textJson(await handlers.listDatabases()));
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 }) => textJson(await handlers.createDatabase({ 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 }) => textJson(await handlers.describeDatabase(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 }) => textJson(await handlers.alterDatabaseOwner(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 }) => textJson(await handlers.dropDatabase(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 }) => textJson(await handlers.listRoles({ 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 }) => textJson(await handlers.createRole({
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 }) => textJson(await handlers.alterRolePassword(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 }) => textJson(await handlers.alterRoleAttributes(role, {
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 }) => textJson(await handlers.dropRole(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 }) => textJson(await handlers.grantRole({ 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 }) => textJson(await handlers.revokeRole({ 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 }) => textJson(await handlers.showRoleMemberships(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 }) => textJson(await handlers.grantPrivileges({ 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 }) => textJson(await handlers.revokePrivileges({ 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 }) => textJson(await handlers.showGrants({ 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 }) => textJson(await handlers.confirmTask(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');
@@ -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.0",
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.29.0",
49
- "pg": "^8.21.0",
50
- "zod": "^4.4.3"
48
+ "@modelcontextprotocol/sdk": "^1.30.0",
49
+ "pg": "^8.23.0",
50
+ "zod": "^4.5.4"
51
51
  },
52
52
  "devDependencies": {
53
- "@types/node": "^25.9.3",
54
- "@types/pg": "^8.20.0",
55
- "typescript": "^6.0.3"
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"