Skip to main content
Glama
Shreyans481

PostgreSQL MCP Server

by Shreyans481
index_sentry.ts9.26 kB
import * as Sentry from "@sentry/cloudflare"; import OAuthProvider from "@cloudflare/workers-oauth-provider"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { McpAgent } from "agents/mcp"; import { z } from "zod"; import { GitHubHandler } from "./github-handler"; import { validateSqlQuery, isWriteOperation, closeDb, withDatabase } from "./database"; // Context from the auth process, encrypted & stored in the auth token // and provided to the DurableMCP as this.props type Props = { login: string; name: string; email: string; accessToken: string; }; const ALLOWED_USERNAMES = new Set<string>([ // Add GitHub usernames of users who should have access to database write operations // For example: 'yourusername', 'coworkerusername' 'coleam00' ]); // Sentry configuration helper function getSentryConfig(env: Env) { return { // You can disable Sentry by setting SENTRY_DSN to a falsey-value dsn: (env as any).SENTRY_DSN, // A sample rate of 1.0 means "capture all traces" tracesSampleRate: 1, }; } // Error handling helper for MCP tools function handleError(error: unknown): { content: Array<{ type: "text"; text: string; isError?: boolean }> } { const eventId = Sentry.captureException(error); const errorMessage = [ "**Error**", "There was a problem with your request.", "Please report the following to the support team:", `**Event ID**: ${eventId}`, process.env.NODE_ENV !== "production" ? error instanceof Error ? error.message : String(error) : "", ].join("\n\n"); return { content: [ { type: "text", text: errorMessage, isError: true, }, ], }; } /** * Take the arguments from an MCP tool call and format * them in an OTel-safe way for tracing attributes. */ function extractMcpParameters(args: Record<string, any>) { return Object.fromEntries( Object.entries(args).map(([key, value]) => { return [`mcp.param.${key}`, JSON.stringify(value)]; }), ); } // Base MCP class without Sentry instrumentation export class MyMCPBase extends McpAgent<Env, Record<string, never>, Props> { server = new McpServer({ name: "PostgreSQL Database MCP Server with Sentry", version: "1.0.0", }); constructor(state: DurableObjectState, env: Env) { super(state, env); } /** * Cleanup database connections when Durable Object is shutting down */ async cleanup(): Promise<void> { try { await closeDb(); console.log('Database connections closed successfully'); } catch (error) { console.error('Error during database cleanup:', error); Sentry.captureException(error); } } /** * Durable Objects alarm handler - used for cleanup */ async alarm(): Promise<void> { await this.cleanup(); } // Helper function to register tools with Sentry instrumentation private registerTool( name: string, description: string, schema: any, handler: (args: any) => Promise<any> ) { this.server.tool(name, description, schema, async (args: any) => { return await Sentry.startNewTrace(async () => { return await Sentry.startSpan( { name: `mcp.tool/${name}`, attributes: extractMcpParameters(args), }, async (span) => { Sentry.setUser({ username: this.props.login, email: this.props.email, }); try { const result = await handler(args); return result; } catch (error) { span.setStatus({ code: 2 }); // error return handleError(error); } }, ); }); }); } async init() { // Tool 1: List Tables - Available to all authenticated users this.registerTool( "listTables", "Get a list of all tables in the database along with their column information. Use this first to understand the database structure before querying.", {}, async () => { return await withDatabase((this.env as any).DATABASE_URL, async (db) => { // Single query to get all table and column information (using your working query) const columns = await db` SELECT table_name, column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_schema = 'public' ORDER BY table_name, ordinal_position `; // Group columns by table const tableMap = new Map(); for (const col of columns) { // Use snake_case property names as returned by the SQL query if (!tableMap.has(col.table_name)) { tableMap.set(col.table_name, { name: col.table_name, schema: 'public', columns: [] }); } tableMap.get(col.table_name).columns.push({ name: col.column_name, type: col.data_type, nullable: col.is_nullable === 'YES', default: col.column_default }); } const tableInfo = Array.from(tableMap.values()); return { content: [ { type: "text", text: `**Database Tables and Schema**\n\n${JSON.stringify(tableInfo, null, 2)}\n\n**Total tables found:** ${tableInfo.length}\n\n**Note:** Use the \`queryDatabase\` tool to run SELECT queries, or \`executeDatabase\` tool for write operations (if you have write access).` } ] }; }); } ); // Tool 2: Query Database - Available to all authenticated users (read-only) this.registerTool( "queryDatabase", "Execute a read-only SQL query against the PostgreSQL database. This tool only allows SELECT statements and other read operations. All authenticated users can use this tool.", { sql: z.string().describe("The SQL query to execute. Only SELECT statements and read operations are allowed.") }, async ({ sql }) => { // Validate the SQL query const validation = validateSqlQuery(sql); if (!validation.isValid) { throw new Error(`Invalid SQL query: ${validation.error}`); } // Check if it's a write operation if (isWriteOperation(sql)) { throw new Error("Write operations are not allowed with this tool. Use the executeDatabase tool if you have write permissions."); } return await withDatabase((this.env as any).DATABASE_URL, async (db) => { const results = await db.unsafe(sql); return { content: [ { type: "text", text: `**Query Results**\n\`\`\`sql\n${sql}\n\`\`\`\n\n**Results:**\n\`\`\`json\n${JSON.stringify(results, null, 2)}\n\`\`\`\n\n**Rows returned:** ${Array.isArray(results) ? results.length : 1}` } ] }; }); } ); // Tool 3: Execute Database - Only available to privileged users (write operations) if (ALLOWED_USERNAMES.has(this.props.login)) { this.registerTool( "executeDatabase", "Execute any SQL statement against the PostgreSQL database, including INSERT, UPDATE, DELETE, and DDL operations. This tool is restricted to specific GitHub users and can perform write transactions. **USE WITH CAUTION** - this can modify or delete data.", { sql: z.string().describe("The SQL statement to execute. Can be any valid SQL including INSERT, UPDATE, DELETE, CREATE, etc.") }, async ({ sql }) => { // Validate the SQL query const validation = validateSqlQuery(sql); if (!validation.isValid) { throw new Error(`Invalid SQL statement: ${validation.error}`); } const isWrite = isWriteOperation(sql); return await withDatabase((this.env as any).DATABASE_URL, async (db) => { const results = await db.unsafe(sql); const operationType = isWrite ? "Write Operation" : "Read Operation"; return { content: [ { type: "text", text: `**${operationType} Executed Successfully**\n\`\`\`sql\n${sql}\n\`\`\`\n\n**Results:**\n\`\`\`json\n${JSON.stringify(results, null, 2)}\n\`\`\`\n\n${isWrite ? '**⚠️ Database was modified**' : `**Rows returned:** ${Array.isArray(results) ? results.length : 1}`}\n\n**Executed by:** ${this.props.login} (${this.props.name})` } ] }; }); } ); } // Tool 4: Divide (for testing error handling) this.registerTool( "divide", "Divide two numbers to test error handling", { a: z.number().describe("The dividend (number to be divided)"), b: z.number().describe("The divisor (number to divide by)") }, async ({ a, b }) => { // Intentionally cause an error when dividing by zero if (b === 0) { throw new Error("Cannot divide by zero"); } const result = a / b; return { content: [ { type: "text", text: `**Division Result**\n\n${a} ÷ ${b} = ${result}` } ] }; } ); } } // Export the instrumented Durable Object export const MyMCP = Sentry.instrumentDurableObjectWithSentry( getSentryConfig, MyMCPBase, ); // Create the OAuth provider const worker = new OAuthProvider({ apiHandlers: { '/sse': MyMCP.serveSSE('/sse') as any, '/mcp': MyMCP.serve('/mcp') as any, }, authorizeEndpoint: "/authorize", clientRegistrationEndpoint: "/register", defaultHandler: GitHubHandler as any, tokenEndpoint: "/token", }); // Export the worker wrapped with Sentry export default Sentry.withSentry( getSentryConfig, worker, ) satisfies ExportedHandler<Env>;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Shreyans481/remote-mcp-server-with-auth'

If you have feedback or need assistance with the MCP directory API, please join our Discord server