index.ts•8.68 kB
import { Hono } from "hono";
import { cors } from "hono/cors";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import * as pg from "pg";
import {
renderDatabaseCredentialsForm,
parseDatabaseCredentials,
validateDatabaseCredentials,
type DatabaseCredentials,
} from "./database-form.js";
import { DatabaseTools } from "./tools/index.js";
interface Env {
OAUTH_KV: KVNamespace;
}
const app = new Hono<{ Bindings: Env }>();
// Enable CORS for all routes
app.use("/*", cors({
origin: "*",
allowMethods: ["GET", "POST", "OPTIONS"],
allowHeaders: ["Content-Type", "Authorization"],
}));
// Landing page with database connection form
app.get("/", async (c) => {
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Database MCP Server</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
}
.container {
background: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
h1 { color: #333; }
.info {
background: #e3f2fd;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
border-left: 4px solid #2196f3;
}
.btn {
display: inline-block;
background: #2196f3;
color: white;
padding: 12px 24px;
text-decoration: none;
border-radius: 6px;
margin: 10px 0;
}
.btn:hover { background: #1976d2; }
code {
background: #f5f5f5;
padding: 2px 6px;
border-radius: 4px;
font-family: 'Monaco', 'Courier New', monospace;
}
</style>
</head>
<body>
<div class="container">
<h1>🗄️ Database MCP Server</h1>
<p>A remote Model Context Protocol server for PostgreSQL and Supabase databases.</p>
<div class="info">
<h3>🚀 Quick Start</h3>
<ol>
<li>Click "Connect Database" below</li>
<li>Enter your database credentials</li>
<li>Get your MCP endpoint URL</li>
<li>Use the endpoint in your AI assistant (Claude, Cursor, etc.)</li>
</ol>
</div>
<a href="/connect" class="btn">Connect Database</a>
<h3>📋 Supported Databases</h3>
<ul>
<li><strong>Supabase</strong> - Use your project's database URL</li>
<li><strong>PostgreSQL</strong> - Any PostgreSQL 12+ database</li>
<li><strong>AWS RDS</strong> - PostgreSQL instances</li>
<li><strong>Google Cloud SQL</strong> - PostgreSQL instances</li>
<li><strong>Azure Database</strong> - PostgreSQL instances</li>
</ul>
<h3>🔧 Available Tools</h3>
<ul>
<li><code>list_tables</code> - List all tables in specified schemas</li>
<li><code>list_extensions</code> - List PostgreSQL extensions</li>
<li><code>list_migrations</code> - List applied migrations</li>
<li><code>apply_migration</code> - Apply DDL migrations</li>
<li><code>execute_sql</code> - Execute SQL queries and updates</li>
</ul>
<div class="info">
<strong>🔒 Security:</strong> Your database credentials are encrypted and only used for your session.
No data is stored permanently on our servers.
</div>
</div>
</body>
</html>`;
return c.html(html);
});
// Database connection form
app.get("/connect", async (c) => {
return renderDatabaseCredentialsForm(c.req.raw, {} as any);
});
// Handle database connection setup
app.post("/connect", async (c) => {
try {
const formData = await c.req.formData();
// Parse and validate database credentials
const dbCredentials = parseDatabaseCredentials(formData);
const validationErrors = validateDatabaseCredentials(dbCredentials);
if (validationErrors.length > 0) {
return c.html(`
<h2>Validation Errors</h2>
<ul>${validationErrors.map(err => `<li>${err}</li>`).join('')}</ul>
<a href="/connect">← Go Back</a>
`);
}
// Test database connection
const { host, port, database, user, password } = dbCredentials;
const connectionString = `postgresql://${encodeURIComponent(user)}:${encodeURIComponent(password)}@${host}:${port}/${database}?sslmode=prefer`;
const testPool = new pg.Pool({
connectionString,
max: 1,
connectionTimeoutMillis: 5000,
});
try {
const client = await testPool.connect();
await client.query('SELECT 1');
client.release();
await testPool.end();
// Generate a simple session token
const sessionToken = generateSessionToken(dbCredentials);
// Store credentials in KV (encrypted)
await c.env.OAUTH_KV.put(
`session:${sessionToken}`,
JSON.stringify(dbCredentials),
{ expirationTtl: 24 * 60 * 60 } // 24 hours
);
const mcpEndpoint = `${new URL(c.req.url).origin}/mcp/${sessionToken}`;
return c.html(`
<div style="max-width: 600px; margin: 0 auto; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
<h2>✅ Connection Successful!</h2>
<p>Your database connection has been established successfully.</p>
<div style="background: #e8f5e8; padding: 20px; border-radius: 8px; margin: 20px 0;">
<h3>🔗 Your MCP Endpoint:</h3>
<code style="background: white; padding: 10px; display: block; border-radius: 4px; word-break: break-all;">${mcpEndpoint}</code>
</div>
<div style="background: #f0f8ff; padding: 20px; border-radius: 8px; margin: 20px 0;">
<h3>📱 How to Use:</h3>
<ol>
<li>Copy the MCP endpoint URL above</li>
<li>Add it to your AI assistant (Claude, Cursor, etc.)</li>
<li>Start querying your database with natural language!</li>
</ol>
</div>
<p><strong>Session expires in 24 hours.</strong> You can reconnect anytime using the same process.</p>
<a href="/" style="display: inline-block; background: #2196f3; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px;">← Back to Home</a>
</div>
`);
} catch (dbError) {
await testPool.end();
return c.html(`
<h2>❌ Database Connection Failed</h2>
<p>Error: ${dbError instanceof Error ? dbError.message : 'Unknown error'}</p>
<a href="/connect">← Try Again</a>
`);
}
} catch (error) {
return c.html(`
<h2>❌ Server Error</h2>
<p>Error: ${error instanceof Error ? error.message : 'Unknown error'}</p>
<a href="/connect">← Try Again</a>
`);
}
});
// MCP Server endpoint with session token
app.all("/mcp/:sessionToken", async (c) => {
const sessionToken = c.req.param("sessionToken");
// Retrieve credentials from KV
const credentialsJson = await c.env.OAUTH_KV.get(`session:${sessionToken}`);
if (!credentialsJson) {
return c.text("Invalid or expired session", 401);
}
const dbCredentials = JSON.parse(credentialsJson) as DatabaseCredentials;
// Create MCP server with user's database credentials
const server = new McpServer({
name: "Database MCP Server",
version: "1.0.0",
});
// Set up database connection
const { host, port, database, user, password } = dbCredentials;
const connectionString = `postgresql://${encodeURIComponent(user)}:${encodeURIComponent(password)}@${host}:${port}/${database}?sslmode=prefer`;
const pool = new pg.Pool({
connectionString,
max: 10,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
// Register database tools
DatabaseTools(server, { pool });
// For now, return a simple response indicating the MCP server is ready
// In a full implementation, you would integrate with the MCP transport layer
return c.json({
success: true,
message: "MCP Server initialized successfully",
endpoint: `/mcp/${sessionToken}`,
tools: [
"list_tables",
"list_extensions",
"list_migrations",
"apply_migration",
"execute_sql"
]
});
});
function generateSessionToken(credentials: DatabaseCredentials): string {
// Generate a secure random token
const timestamp = Date.now().toString();
const random = Math.random().toString(36).substring(2);
const hash = btoa(`${credentials.connectionName}-${timestamp}-${random}`).replace(/[^a-zA-Z0-9]/g, '');
return hash.substring(0, 32);
}
export default app;