Enhanced PostgreSQL MCP Server
by GarethCott
Verified
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListResourcesRequestSchema,
ListToolsRequestSchema,
ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import pg from "pg";
const server = new Server(
{
name: "example-servers/postgres",
version: "0.1.0",
},
{
capabilities: {
resources: {},
tools: {},
},
},
);
const args = process.argv.slice(2);
if (args.length === 0) {
console.error("Please provide a database URL as a command-line argument");
process.exit(1);
}
const databaseUrl = args[0];
const resourceBaseUrl = new URL(databaseUrl);
resourceBaseUrl.protocol = "postgres:";
resourceBaseUrl.password = "";
const pool = new pg.Pool({
connectionString: databaseUrl,
});
const SCHEMA_PATH = "schema";
server.setRequestHandler(ListResourcesRequestSchema, async () => {
const client = await pool.connect();
try {
const result = await client.query(
"SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'",
);
return {
resources: result.rows.map((row) => ({
uri: new URL(`${row.table_name}/${SCHEMA_PATH}`, resourceBaseUrl).href,
mimeType: "application/json",
name: `"${row.table_name}" database schema`,
})),
};
} finally {
client.release();
}
});
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const resourceUrl = new URL(request.params.uri);
const pathComponents = resourceUrl.pathname.split("/");
const schema = pathComponents.pop();
const tableName = pathComponents.pop();
if (schema !== SCHEMA_PATH) {
throw new Error("Invalid resource URI");
}
const client = await pool.connect();
try {
const result = await client.query(
"SELECT column_name, data_type FROM information_schema.columns WHERE table_name = $1",
[tableName],
);
return {
contents: [
{
uri: request.params.uri,
mimeType: "application/json",
text: JSON.stringify(result.rows, null, 2),
},
],
};
} finally {
client.release();
}
});
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "query",
description: "Run a read-only SQL query",
inputSchema: {
type: "object",
properties: {
sql: { type: "string" },
},
},
},
{
name: "execute",
description: "Execute a SQL statement that modifies data (INSERT, UPDATE, DELETE)",
inputSchema: {
type: "object",
properties: {
sql: { type: "string" },
},
},
},
{
name: "insert",
description: "Insert a new record into a table",
inputSchema: {
type: "object",
properties: {
table: { type: "string" },
data: {
type: "object",
additionalProperties: true
},
},
required: ["table", "data"],
},
},
{
name: "update",
description: "Update records in a table",
inputSchema: {
type: "object",
properties: {
table: { type: "string" },
data: {
type: "object",
additionalProperties: true
},
where: { type: "string" },
},
required: ["table", "data", "where"],
},
},
{
name: "delete",
description: "Delete records from a table",
inputSchema: {
type: "object",
properties: {
table: { type: "string" },
where: { type: "string" },
},
required: ["table", "where"],
},
},
{
name: "createTable",
description: "Create a new table with specified columns and constraints",
inputSchema: {
type: "object",
properties: {
tableName: { type: "string" },
columns: {
type: "array",
items: {
type: "object",
properties: {
name: { type: "string" },
type: { type: "string" },
constraints: { type: "string", description: "Optional constraints like NOT NULL, UNIQUE, etc." }
},
required: ["name", "type"]
}
},
constraints: {
type: "array",
items: {
type: "string",
description: "Table-level constraints like PRIMARY KEY, FOREIGN KEY, etc."
}
}
},
required: ["tableName", "columns"]
},
},
{
name: "createFunction",
description: "Create a PostgreSQL function/procedure",
inputSchema: {
type: "object",
properties: {
name: { type: "string" },
parameters: { type: "string" },
returnType: { type: "string" },
language: { type: "string", description: "plpgsql, sql, etc." },
body: { type: "string" },
options: { type: "string", description: "Additional function options" }
},
required: ["name", "parameters", "returnType", "language", "body"]
},
},
{
name: "createTrigger",
description: "Create a trigger on a table",
inputSchema: {
type: "object",
properties: {
name: { type: "string" },
tableName: { type: "string" },
functionName: { type: "string" },
when: { type: "string", description: "BEFORE, AFTER, or INSTEAD OF" },
events: {
type: "array",
items: { type: "string", description: "INSERT, UPDATE, DELETE" }
},
forEach: { type: "string", description: "ROW or STATEMENT" },
condition: { type: "string", description: "Optional WHEN condition" }
},
required: ["name", "tableName", "functionName", "when", "events", "forEach"]
},
},
{
name: "createIndex",
description: "Create an index on a table",
inputSchema: {
type: "object",
properties: {
tableName: { type: "string" },
indexName: { type: "string" },
columns: {
type: "array",
items: { type: "string" }
},
unique: { type: "boolean" },
type: { type: "string", description: "BTREE, HASH, GIN, GIST, etc." },
where: { type: "string", description: "Optional condition" }
},
required: ["tableName", "indexName", "columns"]
},
},
{
name: "alterTable",
description: "Alter a table structure",
inputSchema: {
type: "object",
properties: {
tableName: { type: "string" },
operation: {
type: "string",
description: "ADD COLUMN, DROP COLUMN, ALTER COLUMN, etc."
},
details: { type: "string", description: "Specific details for the operation" }
},
required: ["tableName", "operation", "details"]
},
}
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "query") {
const sql = request.params.arguments?.sql as string;
const client = await pool.connect();
try {
await client.query("BEGIN TRANSACTION READ ONLY");
const result = await client.query(sql);
return {
content: [{ type: "text", text: JSON.stringify(result.rows, null, 2) }],
isError: false,
};
} catch (error) {
throw error;
} finally {
client
.query("ROLLBACK")
.catch((error) =>
console.warn("Could not roll back transaction:", error),
);
client.release();
}
} else if (request.params.name === "execute") {
const sql = request.params.arguments?.sql as string;
const client = await pool.connect();
try {
await client.query("BEGIN");
const result = await client.query(sql);
await client.query("COMMIT");
return {
content: [{
type: "text",
text: JSON.stringify({
command: result.command,
rowCount: result.rowCount,
rows: result.rows
}, null, 2)
}],
isError: false,
};
} catch (error) {
await client.query("ROLLBACK").catch(err =>
console.warn("Could not roll back transaction:", err)
);
throw error;
} finally {
client.release();
}
} else if (request.params.name === "insert") {
const table = request.params.arguments?.table as string;
const data = request.params.arguments?.data as Record<string, any>;
const client = await pool.connect();
try {
const columns = Object.keys(data);
const values = Object.values(data);
const placeholders = values.map((_, i) => `$${i + 1}`).join(', ');
await client.query("BEGIN");
const result = await client.query(
`INSERT INTO ${table} (${columns.join(', ')}) VALUES (${placeholders}) RETURNING *`,
values
);
await client.query("COMMIT");
return {
content: [{
type: "text",
text: JSON.stringify(result.rows, null, 2)
}],
isError: false,
};
} catch (error) {
await client.query("ROLLBACK").catch(err =>
console.warn("Could not roll back transaction:", err)
);
throw error;
} finally {
client.release();
}
} else if (request.params.name === "update") {
const table = request.params.arguments?.table as string;
const data = request.params.arguments?.data as Record<string, any>;
const where = request.params.arguments?.where as string;
const client = await pool.connect();
try {
const setClause = Object.entries(data)
.map(([col, _], i) => `${col} = $${i + 1}`)
.join(', ');
await client.query("BEGIN");
const result = await client.query(
`UPDATE ${table} SET ${setClause} WHERE ${where} RETURNING *`,
Object.values(data)
);
await client.query("COMMIT");
return {
content: [{
type: "text",
text: JSON.stringify(result.rows, null, 2)
}],
isError: false,
};
} catch (error) {
await client.query("ROLLBACK").catch(err =>
console.warn("Could not roll back transaction:", err)
);
throw error;
} finally {
client.release();
}
} else if (request.params.name === "delete") {
const table = request.params.arguments?.table as string;
const where = request.params.arguments?.where as string;
const client = await pool.connect();
try {
await client.query("BEGIN");
const result = await client.query(
`DELETE FROM ${table} WHERE ${where} RETURNING *`
);
await client.query("COMMIT");
return {
content: [{
type: "text",
text: JSON.stringify(result.rows, null, 2)
}],
isError: false,
};
} catch (error) {
await client.query("ROLLBACK").catch(err =>
console.warn("Could not roll back transaction:", err)
);
throw error;
} finally {
client.release();
}
} else if (request.params.name === "createTable") {
const tableName = request.params.arguments?.tableName as string;
const columns = request.params.arguments?.columns as Array<{
name: string;
type: string;
constraints?: string;
}>;
const constraints = request.params.arguments?.constraints as Array<string>;
const client = await pool.connect();
try {
const columnDefinitions = columns.map(col => {
return `${col.name} ${col.type}${col.constraints ? ' ' + col.constraints : ''}`;
}).join(', ');
const tableConstraints = constraints ? ', ' + constraints.join(', ') : '';
const createTableSQL = `CREATE TABLE ${tableName} (${columnDefinitions}${tableConstraints})`;
await client.query('BEGIN');
await client.query(createTableSQL);
await client.query('COMMIT');
return {
content: [{
type: "text",
text: JSON.stringify({
message: `Table ${tableName} created successfully`,
sql: createTableSQL
}, null, 2)
}],
isError: false,
};
} catch (error) {
await client.query("ROLLBACK").catch(err =>
console.warn("Could not roll back transaction:", err)
);
throw error;
} finally {
client.release();
}
} else if (request.params.name === "createFunction") {
const name = request.params.arguments?.name as string;
const parameters = request.params.arguments?.parameters as string;
const returnType = request.params.arguments?.returnType as string;
const language = request.params.arguments?.language as string;
const body = request.params.arguments?.body as string;
const options = request.params.arguments?.options as string;
const client = await pool.connect();
try {
const createFunctionSQL = `
CREATE OR REPLACE FUNCTION ${name}(${parameters})
RETURNS ${returnType}
LANGUAGE ${language}
${options || ''}
AS $$
${body}
$$;
`;
await client.query('BEGIN');
await client.query(createFunctionSQL);
await client.query('COMMIT');
return {
content: [{
type: "text",
text: JSON.stringify({
message: `Function ${name} created successfully`,
sql: createFunctionSQL
}, null, 2)
}],
isError: false,
};
} catch (error) {
await client.query("ROLLBACK").catch(err =>
console.warn("Could not roll back transaction:", err)
);
throw error;
} finally {
client.release();
}
} else if (request.params.name === "createTrigger") {
const name = request.params.arguments?.name as string;
const tableName = request.params.arguments?.tableName as string;
const functionName = request.params.arguments?.functionName as string;
const when = request.params.arguments?.when as string;
const events = request.params.arguments?.events as string[];
const forEach = request.params.arguments?.forEach as string;
const condition = request.params.arguments?.condition as string;
const client = await pool.connect();
try {
const eventStr = events.join(' OR ');
const whenClause = condition ? `WHEN (${condition})` : '';
const createTriggerSQL = `
CREATE TRIGGER ${name}
${when} ${eventStr}
ON ${tableName}
FOR EACH ${forEach}
${whenClause}
EXECUTE FUNCTION ${functionName}();
`;
await client.query('BEGIN');
await client.query(createTriggerSQL);
await client.query('COMMIT');
return {
content: [{
type: "text",
text: JSON.stringify({
message: `Trigger ${name} created successfully on table ${tableName}`,
sql: createTriggerSQL
}, null, 2)
}],
isError: false,
};
} catch (error) {
await client.query("ROLLBACK").catch(err =>
console.warn("Could not roll back transaction:", err)
);
throw error;
} finally {
client.release();
}
} else if (request.params.name === "createIndex") {
const tableName = request.params.arguments?.tableName as string;
const indexName = request.params.arguments?.indexName as string;
const columns = request.params.arguments?.columns as string[];
const unique = request.params.arguments?.unique as boolean;
const type = request.params.arguments?.type as string;
const where = request.params.arguments?.where as string;
const client = await pool.connect();
try {
const uniqueStr = unique ? 'UNIQUE' : '';
const typeStr = type ? `USING ${type}` : '';
const whereClause = where ? `WHERE ${where}` : '';
const createIndexSQL = `
CREATE ${uniqueStr} INDEX ${indexName}
ON ${tableName} ${typeStr} (${columns.join(', ')})
${whereClause}
`;
await client.query('BEGIN');
await client.query(createIndexSQL);
await client.query('COMMIT');
return {
content: [{
type: "text",
text: JSON.stringify({
message: `Index ${indexName} created successfully on table ${tableName}`,
sql: createIndexSQL
}, null, 2)
}],
isError: false,
};
} catch (error) {
await client.query("ROLLBACK").catch(err =>
console.warn("Could not roll back transaction:", err)
);
throw error;
} finally {
client.release();
}
} else if (request.params.name === "alterTable") {
const tableName = request.params.arguments?.tableName as string;
const operation = request.params.arguments?.operation as string;
const details = request.params.arguments?.details as string;
const client = await pool.connect();
try {
const alterTableSQL = `ALTER TABLE ${tableName} ${operation} ${details}`;
await client.query('BEGIN');
await client.query(alterTableSQL);
await client.query('COMMIT');
return {
content: [{
type: "text",
text: JSON.stringify({
message: `Table ${tableName} altered successfully`,
sql: alterTableSQL
}, null, 2)
}],
isError: false,
};
} catch (error) {
await client.query("ROLLBACK").catch(err =>
console.warn("Could not roll back transaction:", err)
);
throw error;
} finally {
client.release();
}
}
throw new Error(`Unknown tool: ${request.params.name}`);
});
async function runServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
runServer().catch(console.error);