ainative-postgres-mcp 1.0.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.
@@ -0,0 +1,53 @@
1
+ # AINative Postgres MCP — Usage Guide
2
+
3
+ This MCP server is an enhanced fork of @modelcontextprotocol/server-postgres that auto-provisions a managed PostgreSQL instance with pgvector. Zero-config database for AI agents.
4
+
5
+ ## Available Tools (5)
6
+
7
+ | Tool | Description |
8
+ |------|-------------|
9
+ | `query` | Execute read-only SQL queries (SELECT, EXPLAIN, etc.) |
10
+ | `list_tables` | List all tables in the database |
11
+ | `describe_table` | Get table schema: columns, types, constraints, indexes |
12
+ | `create_table` | Create a new table with SQL DDL |
13
+ | `insert` | Insert rows and return the inserted data |
14
+
15
+ ## Behavior Rules
16
+
17
+ 1. **Use `query` for all read operations** — it runs in a READ ONLY transaction for safety.
18
+ 2. **Use `list_tables` first** — before querying, check what tables exist.
19
+ 3. **Use `describe_table` before inserting** — verify column names and types.
20
+ 4. **Use `create_table` for DDL** — only CREATE TABLE statements are allowed (no DROP, ALTER).
21
+ 5. **Use `insert` for writes** — pass structured row objects, not raw SQL INSERT.
22
+ 6. **pgvector is available** — create `vector(N)` columns for embeddings and use `<=>` for similarity search.
23
+
24
+ ## Auto-Provisioning
25
+
26
+ No DATABASE_URL? The server auto-provisions a free managed PostgreSQL instance and prints a **claim URL**. Surface this URL to the user so they can take ownership of their database.
27
+
28
+ ## MCP Config
29
+
30
+ ```json
31
+ {
32
+ "mcpServers": {
33
+ "postgres": {
34
+ "command": "npx",
35
+ "args": ["-y", "ainative-postgres-mcp"]
36
+ }
37
+ }
38
+ }
39
+ ```
40
+
41
+ With existing database:
42
+
43
+ ```json
44
+ {
45
+ "mcpServers": {
46
+ "postgres": {
47
+ "command": "npx",
48
+ "args": ["-y", "ainative-postgres-mcp"],
49
+ "env": { "DATABASE_URL": "postgresql://user:pass@host:5432/db" }
50
+ }
51
+ }
52
+ }
53
+ ```
package/.cody/CODY.md ADDED
@@ -0,0 +1,53 @@
1
+ # AINative Postgres MCP — Cody Usage Guide
2
+
3
+ This MCP server is an enhanced fork of @modelcontextprotocol/server-postgres that auto-provisions a managed PostgreSQL instance with pgvector. Zero-config database for AI agents.
4
+
5
+ ## Available Tools (5)
6
+
7
+ | Tool | Description |
8
+ |------|-------------|
9
+ | `query` | Execute read-only SQL queries (SELECT, EXPLAIN, etc.) |
10
+ | `list_tables` | List all tables in the database |
11
+ | `describe_table` | Get table schema: columns, types, constraints, indexes |
12
+ | `create_table` | Create a new table with SQL DDL |
13
+ | `insert` | Insert rows and return the inserted data |
14
+
15
+ ## Behavior Rules
16
+
17
+ 1. **Use `query` for all read operations** — it runs in a READ ONLY transaction for safety.
18
+ 2. **Use `list_tables` first** — before querying, check what tables exist.
19
+ 3. **Use `describe_table` before inserting** — verify column names and types.
20
+ 4. **Use `create_table` for DDL** — only CREATE TABLE statements are allowed (no DROP, ALTER).
21
+ 5. **Use `insert` for writes** — pass structured row objects, not raw SQL INSERT.
22
+ 6. **pgvector is available** — create `vector(N)` columns for embeddings and use `<=>` for similarity search.
23
+
24
+ ## Auto-Provisioning
25
+
26
+ No DATABASE_URL? The server auto-provisions a free managed PostgreSQL instance and prints a **claim URL**. Surface this URL to the user so they can take ownership of their database.
27
+
28
+ ## MCP Config
29
+
30
+ ```json
31
+ {
32
+ "mcpServers": {
33
+ "postgres": {
34
+ "command": "npx",
35
+ "args": ["-y", "ainative-postgres-mcp"]
36
+ }
37
+ }
38
+ }
39
+ ```
40
+
41
+ With existing database:
42
+
43
+ ```json
44
+ {
45
+ "mcpServers": {
46
+ "postgres": {
47
+ "command": "npx",
48
+ "args": ["-y", "ainative-postgres-mcp"],
49
+ "env": { "DATABASE_URL": "postgresql://user:pass@host:5432/db" }
50
+ }
51
+ }
52
+ }
53
+ ```
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AINative Studio
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/README.md ADDED
@@ -0,0 +1,177 @@
1
+ # ainative-postgres-mcp
2
+
3
+ Zero-config PostgreSQL MCP server with auto-provisioning. Drop-in replacement for `@modelcontextprotocol/server-postgres` — no `DATABASE_URL` needed.
4
+
5
+ The official `@modelcontextprotocol/server-postgres` requires you to bring your own Postgres database and pass a connection string. This fork **auto-provisions a managed PostgreSQL instance** with pgvector on first run. Agents get a database instantly with zero configuration.
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ npx ainative-postgres-mcp
11
+ ```
12
+
13
+ That's it. On first run:
14
+ 1. A free managed PostgreSQL instance is provisioned
15
+ 2. pgvector extension is enabled automatically
16
+ 3. Connection details are saved to `~/.ainative/postgres-config.json`, `.env`, and `.mcp.json`
17
+ 4. A claim URL is printed so you can take ownership of the database
18
+
19
+ ## MCP Configuration
20
+
21
+ ### Claude Code / Claude Desktop
22
+
23
+ Add to your `claude_desktop_config.json` or `.mcp.json`:
24
+
25
+ ```json
26
+ {
27
+ "mcpServers": {
28
+ "postgres": {
29
+ "command": "npx",
30
+ "args": ["-y", "ainative-postgres-mcp"]
31
+ }
32
+ }
33
+ }
34
+ ```
35
+
36
+ ### With existing database
37
+
38
+ ```json
39
+ {
40
+ "mcpServers": {
41
+ "postgres": {
42
+ "command": "npx",
43
+ "args": ["-y", "ainative-postgres-mcp"],
44
+ "env": {
45
+ "DATABASE_URL": "postgresql://user:pass@host:5432/mydb"
46
+ }
47
+ }
48
+ }
49
+ }
50
+ ```
51
+
52
+ ### Cursor
53
+
54
+ Add to `.cursor/mcp.json`:
55
+
56
+ ```json
57
+ {
58
+ "mcpServers": {
59
+ "postgres": {
60
+ "command": "npx",
61
+ "args": ["-y", "ainative-postgres-mcp"]
62
+ }
63
+ }
64
+ }
65
+ ```
66
+
67
+ ### Windsurf
68
+
69
+ Add to `~/.windsurf/mcp.json`:
70
+
71
+ ```json
72
+ {
73
+ "mcpServers": {
74
+ "postgres": {
75
+ "command": "npx",
76
+ "args": ["-y", "ainative-postgres-mcp"]
77
+ }
78
+ }
79
+ }
80
+ ```
81
+
82
+ ## Tools
83
+
84
+ | Tool | Description |
85
+ |------|-------------|
86
+ | `query` | Execute read-only SQL queries (runs in READ ONLY transaction) |
87
+ | `list_tables` | List all tables with schema, name, and type |
88
+ | `describe_table` | Get full table schema: columns, types, constraints, indexes |
89
+ | `create_table` | Create a new table with SQL DDL |
90
+ | `insert` | Insert rows into a table (returns inserted rows) |
91
+
92
+ ### Comparison with official server-postgres
93
+
94
+ | Feature | `@modelcontextprotocol/server-postgres` | `ainative-postgres-mcp` |
95
+ |---------|----------------------------------------|------------------------|
96
+ | Read queries | Yes | Yes |
97
+ | Write queries | No | Yes (`create_table`, `insert`) |
98
+ | Schema inspection | Via resources | Via `describe_table` tool |
99
+ | Table listing | No | Yes (`list_tables`) |
100
+ | Auto-provisioning | No | Yes |
101
+ | pgvector | Manual setup | Auto-enabled |
102
+ | Zero config | No (requires DATABASE_URL) | Yes |
103
+ | Free tier | N/A | Yes |
104
+
105
+ ## How Auto-Provisioning Works
106
+
107
+ ```
108
+ Start
109
+ |
110
+ v
111
+ DATABASE_URL set? ----yes----> Connect directly
112
+ |
113
+ no
114
+ v
115
+ ~/.ainative/postgres-config.json exists? ----yes----> Load and connect
116
+ |
117
+ no
118
+ v
119
+ .mcp.json has DATABASE_URL? ----yes----> Load and connect
120
+ |
121
+ no
122
+ v
123
+ POST /api/v1/public/instant-db (get API key)
124
+ |
125
+ v
126
+ POST /api/v1/zerodb/postgres/provision (get Postgres)
127
+ |
128
+ v
129
+ Save to ~/.ainative/postgres-config.json + .env + .mcp.json
130
+ |
131
+ v
132
+ Connect and enable pgvector
133
+ ```
134
+
135
+ ## pgvector Support
136
+
137
+ pgvector is automatically enabled on provisioned instances. You can use vector columns in your tables:
138
+
139
+ ```sql
140
+ CREATE TABLE documents (
141
+ id SERIAL PRIMARY KEY,
142
+ content TEXT,
143
+ embedding vector(1536)
144
+ );
145
+ ```
146
+
147
+ And run similarity searches:
148
+
149
+ ```sql
150
+ SELECT content, embedding <=> '[0.1, 0.2, ...]'::vector AS distance
151
+ FROM documents
152
+ ORDER BY distance
153
+ LIMIT 10;
154
+ ```
155
+
156
+ ## Environment Variables
157
+
158
+ | Variable | Description | Required |
159
+ |----------|-------------|----------|
160
+ | `DATABASE_URL` | PostgreSQL connection string | No (auto-provisioned if missing) |
161
+ | `ZERODB_API_KEY` | ZeroDB API key (for provisioning) | No (auto-created) |
162
+ | `ZERODB_PROJECT_ID` | ZeroDB project ID | No (auto-created) |
163
+ | `ZERODB_API_URL` | ZeroDB API base URL | No (defaults to `https://api.ainative.studio`) |
164
+
165
+ ## Free Tier
166
+
167
+ Auto-provisioned instances include:
168
+ - Managed PostgreSQL with pgvector
169
+ - Shared compute (suitable for development and light production)
170
+ - Automatic backups
171
+ - SSL encryption
172
+
173
+ Sign up at [ainative.studio](https://ainative.studio) to claim your instance and unlock higher limits.
174
+
175
+ ## License
176
+
177
+ MIT
package/index.js ADDED
@@ -0,0 +1,696 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * AINative Postgres MCP Server
5
+ *
6
+ * Zero-config PostgreSQL MCP server with auto-provisioning.
7
+ * Drop-in replacement for @modelcontextprotocol/server-postgres that
8
+ * auto-provisions a managed Postgres instance via ZeroDB on first run.
9
+ *
10
+ * Tools:
11
+ * query — Execute read-only SQL queries
12
+ * describe_table — Get table schema (columns, types, constraints)
13
+ * list_tables — List all tables in the database
14
+ * create_table — Create a new table with SQL DDL
15
+ * insert — Insert rows into a table
16
+ *
17
+ * On startup:
18
+ * 1. Check for DATABASE_URL env var
19
+ * 2. If missing, auto-provision via ZeroDB Postgres API
20
+ * 3. Connect using pg (node-postgres)
21
+ * 4. Enable pgvector extension automatically
22
+ *
23
+ * Usage:
24
+ * npx ainative-postgres-mcp # Auto-provisions on first run
25
+ * DATABASE_URL=postgres://... npx ainative-postgres-mcp # Use existing database
26
+ *
27
+ * Refs #3947
28
+ */
29
+
30
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
31
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
32
+ import {
33
+ ListToolsRequestSchema,
34
+ CallToolRequestSchema
35
+ } from '@modelcontextprotocol/sdk/types.js';
36
+ import dotenv from 'dotenv';
37
+ import pg from 'pg';
38
+ import { createRequire } from 'module';
39
+
40
+ const require = createRequire(import.meta.url);
41
+ const { version: PKG_VERSION } = require('./package.json');
42
+
43
+ dotenv.config();
44
+
45
+ const SERVER_NAME = 'ainative-postgres-mcp';
46
+ const ZERODB_API_URL = process.env.ZERODB_API_URL || 'https://api.ainative.studio';
47
+
48
+ // ─────────────────────────────────────────────────────────────────
49
+ // Auto-provisioning: get or create a managed Postgres instance
50
+ // ─────────────────────────────────────────────────────────────────
51
+
52
+ async function httpPost(url, body, headers = {}) {
53
+ const https = await import('https');
54
+ const urlObj = new URL(url);
55
+ return new Promise((resolve, reject) => {
56
+ const data = JSON.stringify(body);
57
+ const req = https.default.request({
58
+ hostname: urlObj.hostname,
59
+ port: urlObj.port || 443,
60
+ path: urlObj.pathname,
61
+ method: 'POST',
62
+ headers: {
63
+ 'Content-Type': 'application/json',
64
+ 'Content-Length': Buffer.byteLength(data),
65
+ ...headers
66
+ }
67
+ }, (res) => {
68
+ let responseData = '';
69
+ res.on('data', chunk => responseData += chunk);
70
+ res.on('end', () => {
71
+ if (res.statusCode >= 200 && res.statusCode < 300) {
72
+ try { resolve(JSON.parse(responseData)); }
73
+ catch { resolve(responseData); }
74
+ } else {
75
+ reject(new Error(`HTTP ${res.statusCode}: ${responseData}`));
76
+ }
77
+ });
78
+ });
79
+ req.on('error', reject);
80
+ req.write(data);
81
+ req.end();
82
+ });
83
+ }
84
+
85
+ async function httpGet(url, headers = {}) {
86
+ const https = await import('https');
87
+ const urlObj = new URL(url);
88
+ return new Promise((resolve, reject) => {
89
+ const req = https.default.request({
90
+ hostname: urlObj.hostname,
91
+ port: urlObj.port || 443,
92
+ path: urlObj.pathname,
93
+ method: 'GET',
94
+ headers: { 'Content-Type': 'application/json', ...headers }
95
+ }, (res) => {
96
+ let data = '';
97
+ res.on('data', chunk => data += chunk);
98
+ res.on('end', () => {
99
+ if (res.statusCode >= 200 && res.statusCode < 300) {
100
+ try { resolve(JSON.parse(data)); }
101
+ catch { resolve(data); }
102
+ } else {
103
+ reject(new Error(`HTTP ${res.statusCode}: ${data}`));
104
+ }
105
+ });
106
+ });
107
+ req.on('error', reject);
108
+ req.end();
109
+ });
110
+ }
111
+
112
+ async function loadSavedConfig() {
113
+ const { existsSync, readFileSync } = await import('fs');
114
+ const { join } = await import('path');
115
+ const os = await import('os');
116
+
117
+ const configPath = join(os.default.homedir(), '.ainative', 'postgres-config.json');
118
+ if (existsSync(configPath)) {
119
+ try {
120
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'));
121
+ if (config.database_url) return config;
122
+ } catch (_) {}
123
+ }
124
+ return null;
125
+ }
126
+
127
+ async function saveConfig(config) {
128
+ const { existsSync, mkdirSync, writeFileSync, readFileSync, appendFileSync } = await import('fs');
129
+ const { join } = await import('path');
130
+ const os = await import('os');
131
+
132
+ // Save to ~/.ainative/postgres-config.json
133
+ const ainativeDir = join(os.default.homedir(), '.ainative');
134
+ if (!existsSync(ainativeDir)) mkdirSync(ainativeDir, { recursive: true });
135
+ const configPath = join(ainativeDir, 'postgres-config.json');
136
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
137
+
138
+ // Append to .env in cwd
139
+ const envPath = join(process.cwd(), '.env');
140
+ const envBlock = `\n# PostgreSQL (auto-provisioned by ainative-postgres-mcp)\nDATABASE_URL=${config.database_url}\nZERODB_API_KEY=${config.api_key}\nZERODB_PROJECT_ID=${config.project_id}\n`;
141
+ if (existsSync(envPath)) {
142
+ if (!readFileSync(envPath, 'utf-8').includes('DATABASE_URL')) {
143
+ appendFileSync(envPath, envBlock);
144
+ }
145
+ } else {
146
+ writeFileSync(envPath, envBlock.trimStart());
147
+ }
148
+
149
+ // Write .mcp.json
150
+ const mcpPath = join(process.cwd(), '.mcp.json');
151
+ const mcpEntry = {
152
+ 'ainative-postgres': {
153
+ command: 'npx',
154
+ args: ['-y', 'ainative-postgres-mcp'],
155
+ env: {
156
+ DATABASE_URL: config.database_url,
157
+ ZERODB_API_KEY: config.api_key,
158
+ ZERODB_PROJECT_ID: config.project_id
159
+ }
160
+ }
161
+ };
162
+ let existing = {};
163
+ if (existsSync(mcpPath)) { try { existing = JSON.parse(readFileSync(mcpPath, 'utf-8')); } catch (_) {} }
164
+ writeFileSync(mcpPath, JSON.stringify({
165
+ ...existing,
166
+ mcpServers: { ...(existing.mcpServers || {}), ...mcpEntry }
167
+ }, null, 2) + '\n');
168
+ }
169
+
170
+ async function autoProvision() {
171
+ console.error('\n No DATABASE_URL found — auto-provisioning a managed PostgreSQL instance...\n');
172
+
173
+ // Step 1: Get API key via instant-db
174
+ let apiKey, projectId;
175
+ const existingKey = process.env.ZERODB_API_KEY;
176
+ const existingProject = process.env.ZERODB_PROJECT_ID;
177
+
178
+ if (existingKey && existingProject) {
179
+ apiKey = existingKey;
180
+ projectId = existingProject;
181
+ console.error(' Using existing ZeroDB credentials');
182
+ } else {
183
+ console.error(' Step 1/2: Creating free ZeroDB account...');
184
+ const creds = await httpPost(`${ZERODB_API_URL}/api/v1/public/instant-db`, {
185
+ agree_terms: true
186
+ });
187
+ apiKey = creds.api_key;
188
+ projectId = creds.project_id;
189
+ console.error(` Account created: ${projectId}`);
190
+ if (creds.claim_url) {
191
+ console.error(`\n *** Claim your account: ${creds.claim_url} ***\n`);
192
+ }
193
+ }
194
+
195
+ // Step 2: Provision Postgres instance
196
+ console.error(' Step 2/2: Provisioning PostgreSQL instance...');
197
+ const authHeaders = {
198
+ 'X-API-Key': apiKey,
199
+ 'X-Project-ID': projectId
200
+ };
201
+
202
+ const pgResult = await httpPost(
203
+ `${ZERODB_API_URL}/api/v1/zerodb/postgres/provision`,
204
+ { enable_pgvector: true },
205
+ authHeaders
206
+ );
207
+
208
+ const databaseUrl = pgResult.connection_string || pgResult.database_url || pgResult.dsn;
209
+ if (!databaseUrl) {
210
+ throw new Error('Provisioning succeeded but no connection string returned');
211
+ }
212
+
213
+ const config = {
214
+ database_url: databaseUrl,
215
+ api_key: apiKey,
216
+ project_id: projectId,
217
+ provisioned_at: new Date().toISOString()
218
+ };
219
+
220
+ await saveConfig(config);
221
+
222
+ console.error(' PostgreSQL provisioned successfully!');
223
+ console.error(` Connection saved to ~/.ainative/postgres-config.json`);
224
+ console.error(` Also saved to .env and .mcp.json\n`);
225
+
226
+ return databaseUrl;
227
+ }
228
+
229
+ async function resolveDatabaseUrl() {
230
+ // 1. Check env var
231
+ if (process.env.DATABASE_URL) {
232
+ console.error(' Using DATABASE_URL from environment');
233
+ return process.env.DATABASE_URL;
234
+ }
235
+
236
+ // 2. Check saved config
237
+ const saved = await loadSavedConfig();
238
+ if (saved?.database_url) {
239
+ console.error(' Using saved config from ~/.ainative/postgres-config.json');
240
+ return saved.database_url;
241
+ }
242
+
243
+ // 3. Scan .mcp.json for credentials
244
+ const { existsSync, readFileSync } = await import('fs');
245
+ const { join, dirname } = await import('path');
246
+
247
+ let dir = process.cwd();
248
+ for (let i = 0; i < 6; i++) {
249
+ const mcpPath = join(dir, '.mcp.json');
250
+ if (existsSync(mcpPath)) {
251
+ try {
252
+ const mcp = JSON.parse(readFileSync(mcpPath, 'utf-8'));
253
+ const servers = mcp.mcpServers || {};
254
+ const pgServer = servers['ainative-postgres']
255
+ || servers['postgres']
256
+ || servers['postgres-mcp']
257
+ || Object.values(servers).find(s => (s.args || []).join(' ').includes('postgres'));
258
+ const env = pgServer?.env;
259
+ if (env?.DATABASE_URL) {
260
+ console.error(` Loaded DATABASE_URL from ${mcpPath}`);
261
+ return env.DATABASE_URL;
262
+ }
263
+ } catch (_) {}
264
+ }
265
+ const parent = dirname(dir);
266
+ if (parent === dir) break;
267
+ dir = parent;
268
+ }
269
+
270
+ // 4. Auto-provision
271
+ return await autoProvision();
272
+ }
273
+
274
+ // ─────────────────────────────────────────────────────────────────
275
+ // Postgres Client Manager
276
+ // ─────────────────────────────────────────────────────────────────
277
+
278
+ class PostgresManager {
279
+ constructor(databaseUrl) {
280
+ this.databaseUrl = databaseUrl;
281
+ this.pool = null;
282
+ }
283
+
284
+ async connect() {
285
+ this.pool = new pg.default.Pool({
286
+ connectionString: this.databaseUrl,
287
+ max: 5,
288
+ idleTimeoutMillis: 30000,
289
+ connectionTimeoutMillis: 10000,
290
+ ssl: this.databaseUrl.includes('sslmode=require') || this.databaseUrl.includes('ssl=true')
291
+ ? { rejectUnauthorized: false }
292
+ : undefined
293
+ });
294
+
295
+ // Test connection
296
+ const client = await this.pool.connect();
297
+ try {
298
+ await client.query('SELECT 1');
299
+ } finally {
300
+ client.release();
301
+ }
302
+ }
303
+
304
+ async enablePgvector() {
305
+ try {
306
+ await this.pool.query('CREATE EXTENSION IF NOT EXISTS vector');
307
+ return true;
308
+ } catch (err) {
309
+ // pgvector may not be available — non-fatal
310
+ console.error(` pgvector extension: ${err.message.includes('could not') ? 'not available' : err.message}`);
311
+ return false;
312
+ }
313
+ }
314
+
315
+ async query(sql) {
316
+ const client = await this.pool.connect();
317
+ try {
318
+ // Execute in a read-only transaction for safety
319
+ await client.query('BEGIN TRANSACTION READ ONLY');
320
+ const result = await client.query(sql);
321
+ await client.query('COMMIT');
322
+ return {
323
+ rows: result.rows,
324
+ rowCount: result.rowCount,
325
+ fields: result.fields?.map(f => ({ name: f.name, dataTypeID: f.dataTypeID }))
326
+ };
327
+ } catch (err) {
328
+ await client.query('ROLLBACK').catch(() => {});
329
+ throw err;
330
+ } finally {
331
+ client.release();
332
+ }
333
+ }
334
+
335
+ async execute(sql, params = []) {
336
+ const client = await this.pool.connect();
337
+ try {
338
+ const result = await client.query(sql, params);
339
+ return {
340
+ rows: result.rows,
341
+ rowCount: result.rowCount,
342
+ command: result.command
343
+ };
344
+ } catch (err) {
345
+ throw err;
346
+ } finally {
347
+ client.release();
348
+ }
349
+ }
350
+
351
+ async listTables() {
352
+ const result = await this.pool.query(`
353
+ SELECT table_schema, table_name, table_type
354
+ FROM information_schema.tables
355
+ WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
356
+ ORDER BY table_schema, table_name
357
+ `);
358
+ return result.rows;
359
+ }
360
+
361
+ async describeTable(tableName) {
362
+ // Parse schema.table if provided
363
+ let schema = 'public';
364
+ let table = tableName;
365
+ if (tableName.includes('.')) {
366
+ [schema, table] = tableName.split('.');
367
+ }
368
+
369
+ const columnsResult = await this.pool.query(`
370
+ SELECT
371
+ c.column_name,
372
+ c.data_type,
373
+ c.character_maximum_length,
374
+ c.is_nullable,
375
+ c.column_default,
376
+ c.udt_name
377
+ FROM information_schema.columns c
378
+ WHERE c.table_schema = $1 AND c.table_name = $2
379
+ ORDER BY c.ordinal_position
380
+ `, [schema, table]);
381
+
382
+ const constraintsResult = await this.pool.query(`
383
+ SELECT
384
+ tc.constraint_name,
385
+ tc.constraint_type,
386
+ kcu.column_name
387
+ FROM information_schema.table_constraints tc
388
+ JOIN information_schema.key_column_usage kcu
389
+ ON tc.constraint_name = kcu.constraint_name
390
+ AND tc.table_schema = kcu.table_schema
391
+ WHERE tc.table_schema = $1 AND tc.table_name = $2
392
+ ORDER BY tc.constraint_type, kcu.column_name
393
+ `, [schema, table]);
394
+
395
+ const indexesResult = await this.pool.query(`
396
+ SELECT
397
+ indexname,
398
+ indexdef
399
+ FROM pg_indexes
400
+ WHERE schemaname = $1 AND tablename = $2
401
+ ORDER BY indexname
402
+ `, [schema, table]);
403
+
404
+ return {
405
+ table: tableName,
406
+ schema: schema,
407
+ columns: columnsResult.rows,
408
+ constraints: constraintsResult.rows,
409
+ indexes: indexesResult.rows
410
+ };
411
+ }
412
+
413
+ async createTable(sql) {
414
+ // Validate it's a CREATE TABLE statement
415
+ const normalized = sql.trim().toUpperCase();
416
+ if (!normalized.startsWith('CREATE TABLE') && !normalized.startsWith('CREATE UNLOGGED TABLE')) {
417
+ throw new Error('SQL must be a CREATE TABLE statement');
418
+ }
419
+ return await this.execute(sql);
420
+ }
421
+
422
+ async insert(table, rows) {
423
+ if (!rows || rows.length === 0) {
424
+ throw new Error('No rows provided');
425
+ }
426
+
427
+ const columns = Object.keys(rows[0]);
428
+ const values = [];
429
+ const placeholders = [];
430
+ let paramIndex = 1;
431
+
432
+ for (const row of rows) {
433
+ const rowPlaceholders = [];
434
+ for (const col of columns) {
435
+ values.push(row[col] !== undefined ? row[col] : null);
436
+ rowPlaceholders.push(`$${paramIndex++}`);
437
+ }
438
+ placeholders.push(`(${rowPlaceholders.join(', ')})`);
439
+ }
440
+
441
+ // Sanitize table name — allow schema.table format
442
+ const safeTable = table.split('.').map(part => `"${part.replace(/"/g, '""')}"`).join('.');
443
+ const safeColumns = columns.map(c => `"${c.replace(/"/g, '""')}"`).join(', ');
444
+
445
+ const sql = `INSERT INTO ${safeTable} (${safeColumns}) VALUES ${placeholders.join(', ')} RETURNING *`;
446
+ return await this.execute(sql, values);
447
+ }
448
+
449
+ async close() {
450
+ if (this.pool) {
451
+ await this.pool.end();
452
+ }
453
+ }
454
+ }
455
+
456
+ // ─────────────────────────────────────────────────────────────────
457
+ // Tool definitions
458
+ // ─────────────────────────────────────────────────────────────────
459
+
460
+ const TOOLS = [
461
+ {
462
+ name: 'query',
463
+ description: 'Execute a read-only SQL query against the connected PostgreSQL database. All queries run inside a READ ONLY transaction for safety. Use this for SELECT, EXPLAIN, and other read operations.',
464
+ inputSchema: {
465
+ type: 'object',
466
+ properties: {
467
+ sql: {
468
+ type: 'string',
469
+ description: 'The SQL query to execute (read-only)'
470
+ }
471
+ },
472
+ required: ['sql']
473
+ }
474
+ },
475
+ {
476
+ name: 'list_tables',
477
+ description: 'List all tables in the connected PostgreSQL database, including schema, table name, and type (BASE TABLE or VIEW).',
478
+ inputSchema: {
479
+ type: 'object',
480
+ properties: {},
481
+ required: []
482
+ }
483
+ },
484
+ {
485
+ name: 'describe_table',
486
+ description: 'Get the full schema of a table including columns (name, type, nullable, default), constraints (primary key, foreign key, unique), and indexes. Supports schema.table format.',
487
+ inputSchema: {
488
+ type: 'object',
489
+ properties: {
490
+ table_name: {
491
+ type: 'string',
492
+ description: 'The table name to describe. Use "schema.table" for non-public schemas.'
493
+ }
494
+ },
495
+ required: ['table_name']
496
+ }
497
+ },
498
+ {
499
+ name: 'create_table',
500
+ description: 'Create a new table in the database using a SQL CREATE TABLE statement. Supports all PostgreSQL column types including pgvector vector columns.',
501
+ inputSchema: {
502
+ type: 'object',
503
+ properties: {
504
+ sql: {
505
+ type: 'string',
506
+ description: 'The CREATE TABLE SQL statement'
507
+ }
508
+ },
509
+ required: ['sql']
510
+ }
511
+ },
512
+ {
513
+ name: 'insert',
514
+ description: 'Insert one or more rows into a table. Rows are provided as an array of objects where keys are column names. Returns the inserted rows.',
515
+ inputSchema: {
516
+ type: 'object',
517
+ properties: {
518
+ table: {
519
+ type: 'string',
520
+ description: 'The table name to insert into. Use "schema.table" for non-public schemas.'
521
+ },
522
+ rows: {
523
+ type: 'array',
524
+ items: {
525
+ type: 'object',
526
+ description: 'A row object where keys are column names and values are the data to insert'
527
+ },
528
+ description: 'Array of row objects to insert'
529
+ }
530
+ },
531
+ required: ['table', 'rows']
532
+ }
533
+ }
534
+ ];
535
+
536
+ // ─────────────────────────────────────────────────────────────────
537
+ // Tool execution router
538
+ // ─────────────────────────────────────────────────────────────────
539
+
540
+ async function executeTool(name, args, manager) {
541
+ switch (name) {
542
+ case 'query':
543
+ return await manager.query(args.sql);
544
+ case 'list_tables':
545
+ return await manager.listTables();
546
+ case 'describe_table':
547
+ return await manager.describeTable(args.table_name);
548
+ case 'create_table':
549
+ return await manager.createTable(args.sql);
550
+ case 'insert':
551
+ return await manager.insert(args.table, args.rows);
552
+ default:
553
+ return null;
554
+ }
555
+ }
556
+
557
+ // ─────────────────────────────────────────────────────────────────
558
+ // Main server
559
+ // ─────────────────────────────────────────────────────────────────
560
+
561
+ async function main() {
562
+ console.error('\n');
563
+ console.error(' ██████╗ ██████╗ ███████╗████████╗ ██████╗ ██████╗ ███████╗███████╗');
564
+ console.error(' ██╔══██╗██╔═══██╗██╔════╝╚══██╔══╝██╔════╝ ██╔══██╗██╔════╝██╔════╝');
565
+ console.error(' ██████╔╝██║ ██║███████╗ ██║ ██║ ███╗██████╔╝█████╗ ███████╗');
566
+ console.error(' ██╔═══╝ ██║ ██║╚════██║ ██║ ██║ ██║██╔══██╗██╔══╝ ╚════██║');
567
+ console.error(' ██║ ╚██████╔╝███████║ ██║ ╚██████╔╝██║ ██║███████╗███████║');
568
+ console.error(' ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚══════╝');
569
+ console.error('\n AINative Postgres — Zero-Config Database for AI Agents');
570
+ console.error('\n==========================================================');
571
+ console.error(` Postgres MCP Server v${PKG_VERSION}`);
572
+ console.error(' Drop-in replacement for @modelcontextprotocol/server-postgres');
573
+ console.error(' Managed PostgreSQL with pgvector + auto-provisioning');
574
+ console.error('==========================================================\n');
575
+
576
+ // Resolve database URL (env > saved config > .mcp.json > auto-provision)
577
+ let databaseUrl;
578
+ try {
579
+ databaseUrl = await resolveDatabaseUrl();
580
+ } catch (err) {
581
+ console.error(` Failed to resolve database: ${err.message}`);
582
+ console.error(' Set DATABASE_URL or run: npx zerodb-cli init');
583
+ console.error(' Or sign up: https://ainative.studio\n');
584
+ process.exit(1);
585
+ }
586
+
587
+ // Connect to Postgres
588
+ const manager = new PostgresManager(databaseUrl);
589
+ try {
590
+ await manager.connect();
591
+ console.error(' Connected to PostgreSQL');
592
+ } catch (err) {
593
+ console.error(` Connection failed: ${err.message}`);
594
+ console.error(' Check your DATABASE_URL and try again.\n');
595
+ process.exit(1);
596
+ }
597
+
598
+ // Enable pgvector
599
+ const hasVector = await manager.enablePgvector();
600
+ if (hasVector) {
601
+ console.error(' pgvector extension: enabled');
602
+ }
603
+
604
+ // Show table count
605
+ try {
606
+ const tables = await manager.listTables();
607
+ console.error(` Tables: ${tables.length} found`);
608
+ } catch (_) {}
609
+
610
+ console.error(` Tools: ${TOOLS.length} available (query, list_tables, describe_table, create_table, insert)\n`);
611
+
612
+ // Create MCP server
613
+ const server = new Server(
614
+ { name: SERVER_NAME, version: PKG_VERSION },
615
+ { capabilities: { tools: {} } }
616
+ );
617
+
618
+ // List tools handler
619
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
620
+ tools: TOOLS.map(tool => ({
621
+ name: tool.name,
622
+ description: tool.description,
623
+ inputSchema: tool.inputSchema
624
+ }))
625
+ }));
626
+
627
+ // Call tool handler
628
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
629
+ const { name, arguments: args } = request.params;
630
+ const tool = TOOLS.find(t => t.name === name);
631
+
632
+ if (!tool) {
633
+ return {
634
+ content: [{ type: 'text', text: JSON.stringify({ error: `Unknown tool: ${name}` }) }],
635
+ isError: true
636
+ };
637
+ }
638
+
639
+ try {
640
+ const result = await executeTool(name, args || {}, manager);
641
+
642
+ if (result === null) {
643
+ return {
644
+ content: [{ type: 'text', text: JSON.stringify({ error: `Tool ${name} not implemented` }) }],
645
+ isError: true
646
+ };
647
+ }
648
+
649
+ return {
650
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
651
+ };
652
+ } catch (err) {
653
+ console.error(` [${SERVER_NAME}] Tool ${name} error:`, err.message);
654
+ return {
655
+ content: [{
656
+ type: 'text',
657
+ text: JSON.stringify({
658
+ error: err.message,
659
+ tool: name,
660
+ hint: err.message.includes('ECONNREFUSED') || err.message.includes('timeout')
661
+ ? 'Database connection failed. Check DATABASE_URL or re-provision with: npx ainative-postgres-mcp'
662
+ : undefined
663
+ })
664
+ }],
665
+ isError: true
666
+ };
667
+ }
668
+ });
669
+
670
+ // Connect via stdio transport
671
+ const transport = new StdioServerTransport();
672
+ await server.connect(transport);
673
+ console.error(` MCP Server connected and ready (${TOOLS.length} tools)\n`);
674
+ }
675
+
676
+ // Only start server when run directly (not imported for testing)
677
+ const isMainModule = process.argv[1] && (
678
+ process.argv[1].endsWith('ainative-postgres-mcp/index.js') ||
679
+ process.argv[1].includes('ainative-postgres-mcp')
680
+ ) && !process.argv[1].includes('test');
681
+
682
+ if (isMainModule) {
683
+ // Graceful shutdown
684
+ process.on('SIGINT', () => { console.error('\n Shutting down...'); process.exit(0); });
685
+ process.on('SIGTERM', () => { console.error('\n Shutting down...'); process.exit(0); });
686
+
687
+ main().catch(err => {
688
+ console.error(`[${SERVER_NAME}] Fatal error:`, err.message);
689
+ console.error('\n Set DATABASE_URL or run: npx zerodb-cli init');
690
+ console.error(' Or sign up: https://ainative.studio\n');
691
+ process.exit(1);
692
+ });
693
+ }
694
+
695
+ // Export for testing
696
+ export { PostgresManager, TOOLS, resolveDatabaseUrl, autoProvision };
package/package.json ADDED
@@ -0,0 +1,94 @@
1
+ {
2
+ "name": "ainative-postgres-mcp",
3
+ "version": "1.0.0",
4
+ "description": "Zero-config Postgres MCP server with auto-provisioning. Drop-in replacement for @modelcontextprotocol/server-postgres — no DATABASE_URL needed. Auto-provisions a managed PostgreSQL instance with pgvector on first run.",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "ainative-postgres-mcp": "./index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node index.js",
12
+ "test": "node --test tests/*.test.js"
13
+ },
14
+ "keywords": [
15
+ "mcp",
16
+ "mcp-server",
17
+ "mcp-server-hosting",
18
+ "model-context-protocol",
19
+ "model-context-protocol-server",
20
+ "modelcontextprotocol",
21
+ "postgres",
22
+ "postgresql",
23
+ "database",
24
+ "auto-provisioning",
25
+ "zerodb",
26
+ "ainative",
27
+ "managed-postgres",
28
+ "serverless-postgres",
29
+ "free-database",
30
+ "pgvector",
31
+ "vector-search",
32
+ "sql",
33
+ "query",
34
+ "schema",
35
+ "claude",
36
+ "claude-code",
37
+ "claude-desktop",
38
+ "claude-ai-agents",
39
+ "cursor",
40
+ "windsurf",
41
+ "codex",
42
+ "n8n-ai",
43
+ "server-postgres",
44
+ "server-postgres-alternative",
45
+ "neon-alternative",
46
+ "supabase-alternative",
47
+ "planetscale-alternative",
48
+ "zero-config",
49
+ "instant-database",
50
+ "ai-database",
51
+ "agent-database",
52
+ "agentic-ai",
53
+ "ai-agents",
54
+ "ai-tool-calling",
55
+ "production-ready",
56
+ "drop-in-replacement",
57
+ "free-tier",
58
+ "cloud-database"
59
+ ],
60
+ "author": {
61
+ "name": "AINative Studio",
62
+ "email": "toby@rely.ventures",
63
+ "url": "https://ainative.studio"
64
+ },
65
+ "license": "MIT",
66
+ "repository": {
67
+ "type": "git",
68
+ "url": "https://github.com/AINative-Studio/ainative-postgres-mcp.git"
69
+ },
70
+ "bugs": {
71
+ "url": "https://github.com/AINative-Studio/ainative-postgres-mcp/issues"
72
+ },
73
+ "homepage": "https://github.com/AINative-Studio/ainative-postgres-mcp#readme",
74
+ "dependencies": {
75
+ "@modelcontextprotocol/sdk": "^1.24.0",
76
+ "dotenv": "^16.4.0",
77
+ "pg": "^8.13.0"
78
+ },
79
+ "files": [
80
+ "index.js",
81
+ "README.md",
82
+ "LICENSE",
83
+ ".claude/",
84
+ ".cody/"
85
+ ],
86
+ "publishConfig": {
87
+ "access": "public",
88
+ "registry": "https://registry.npmjs.org/"
89
+ },
90
+ "engines": {
91
+ "node": ">=18.0.0",
92
+ "npm": ">=9.0.0"
93
+ }
94
+ }