Gel Database MCP Server
by christian561
- src
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { createClient } from "gel"; // The Gel client library
import * as gelLib from "gel"; // Import all exports from Gel
import e from "./edgeql-js/index.js";
// Import the EdgeQL query builder
// import { createClient as createEdgeQLClient } from "../dbschema/edgeql-js/index.js";
let gelClient: any; // Explicitly type as any for now
// Note: Due to TypeScript ESM compatibility issues with the generated EdgeQL query builder,
// we're using 'any' type here. In a production setting, you would properly integrate
// the EdgeQL query builder types.
async function getBranchClient(branchName: string) {
console.error(`[getBranchClient] Starting to get client for branch: ${branchName}`);
const branchClient = createClient({
tlsSecurity: "insecure",
database: branchName,
host: "localhost",
port: 10700,
user: "edgedb",
password: "6GmB7hZI5NTziQ1xEdGaGoQe",
// Add other connection parameters if needed (host, user, password, etc.)
});
console.error(`[getBranchClient] Successfully created client for branch: ${branchName}`);
return branchClient;
}
// Initialize Gel client
async function initGelClient() {
try {
// You can configure this with environment variables or directly here
// Use the branch ID from environment or default to example
const branchId = process.env.GEL_BRANCH_ID || "3802d36f-f2e8-45e5-abe4-8f5d2ecdff0e";
gelClient = await getBranchClient(branchId);
// Test connection
const result = await gelClient.query('SELECT "Gel MCP Server connection test"');
console.error('Gel database connection successful:', result);
return true;
} catch (error) {
console.error('Error connecting to Gel database:', error);
return false;
}
}
// Create server instance
const server = new McpServer({
name: "gel-database",
version: "1.0.0",
});
// ===== GEL DATABASE QUERY TOOLS =====
// Tool to execute raw EdgeQL queries
server.tool(
"execute-edgeql",
"Execute a raw EdgeQL query on the Gel database",
{
query: z.string().describe("The EdgeQL query to execute"),
args: z.record(z.any()).optional().describe("Optional query arguments"),
},
async (args, extra) => {
try {
console.error(`[execute-edgeql] Executing query: ${args.query}`);
if (!gelClient) {
return {
content: [
{
type: "text",
text: "Database client is not initialized.",
},
],
};
}
// Execute query with or without arguments based on whether args are provided
let result;
if (args.args && Object.keys(args.args).length > 0) {
result = await gelClient.query(args.query, args.args);
} else {
result = await gelClient.query(args.query);
}
return {
content: [
{
type: "text",
text: "Query executed successfully:",
},
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error: any) {
console.error("Error executing EdgeQL query:", error);
return {
content: [
{
type: "text",
text: `Error executing query: ${error?.message || "Unknown error"}`,
},
],
};
}
},
);
// Tool to describe the database schema for a type
server.tool(
"describe-schema",
"Get schema information for a specific type",
{
typeName: z.string().describe("Name of the type to describe"),
},
async (args, extra) => {
try {
if (!gelClient) {
return {
content: [
{
type: "text",
text: "Database client is not initialized.",
},
],
};
}
// Use introspection query to get schema information
const query = `
WITH module schema
SELECT ObjectType {
name,
properties: {
name,
target: { name },
cardinality,
required
},
links: {
name,
target: { name },
cardinality,
required
}
}
FILTER .name = <str>$typeName
`;
const result = await gelClient.query(query, { typeName: `default::${args.typeName}` });
if (!result || result.length === 0) {
return {
content: [
{
type: "text",
text: `Type '${args.typeName}' not found in the schema.`,
},
],
};
}
return {
content: [
{
type: "text",
text: `Schema for type '${args.typeName}':`,
},
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error: any) {
console.error("Error describing schema:", error);
return {
content: [
{
type: "text",
text: `Error describing schema: ${error?.message || "Unknown error"}`,
},
],
};
}
},
);
// Tool to validate EdgeQL query syntax
server.tool(
"validate-query",
"Validate EdgeQL query syntax without executing it",
{
query: z.string().describe("The EdgeQL query to validate"),
},
async (args, extra) => {
try {
if (!gelClient) {
return {
content: [
{
type: "text",
text: "Database client is not initialized.",
},
],
};
}
// Using a query with ANALYZE to validate syntax
const analyzeQuery = `ANALYZE ${args.query};`;
await gelClient.query(analyzeQuery);
return {
content: [
{
type: "text",
text: "Query syntax is valid.",
},
],
};
} catch (error: any) {
console.error("Error validating query:", error);
return {
content: [
{
type: "text",
text: "Query validation failed:",
},
{
type: "text",
text: error?.message || "Unknown error",
},
],
};
}
},
);
// Tool to execute TypeScript code with access to the EdgeQL query builder
server.tool(
"execute-typescript",
"Execute TypeScript with EdgeDB query builder access. USAGE: 1) The gelClient is ALREADY SET UP - use it directly. 2) Query builder pattern: 'const query = e.select(...); const result = await query.run(gelClient);'. 3) For direct EdgeQL: 'const result = await gelClient.query(`SELECT...`);'. 4) Common patterns: e.select(e.Type, obj => ({properties, filter: e.op(...)})), e.insert(e.Type, {properties}), e.update(e.Type, obj => ({filter_single: {id: '...'}, set: {prop: value}})), e.delete(e.Type, obj => ({filter: ...})). 5) Always use try/catch for error handling. 6) Check schema first with mcp__describe_schema if unsure about types. 7) Use e.cast() for proper type conversion when needed.",
{
code: z.string().describe("The TypeScript code to execute"),
timeout: z.number().optional().describe("Maximum execution time in milliseconds (default: 5000)"),
use_gel_client: z.boolean().optional().describe("Automatically inject the configured Gel client (default: true)"),
memory_limit: z.number().optional().describe("Maximum memory usage in MB (default: 512)"),
},
async (args, extra) => {
try {
console.error("[execute-typescript] Starting execution");
// Initialize metrics for monitoring execution
const startTime = Date.now();
let isExecutionCompleted = false;
let hasConnectionError = false;
// Check if gelClient is required but not initialized
if (args.use_gel_client !== false) {
if (!gelClient) {
console.error("[execute-typescript] Database client not initialized");
return {
content: [
{
type: "text",
text: "Database client is not initialized. Please initialize the database connection first.",
},
],
};
}
// Test the connection before executing
try {
await gelClient.query('SELECT 1;');
} catch (connError) {
console.error("[execute-typescript] Connection test failed:", connError);
hasConnectionError = true;
// Try to reinitialize the connection
try {
console.error("[execute-typescript] Attempting to reconnect to database");
await initGelClient();
// Test the connection again
await gelClient.query('SELECT 1;');
hasConnectionError = false;
console.error("[execute-typescript] Successfully reconnected to database");
} catch (reconnectError) {
console.error("[execute-typescript] Failed to reconnect:", reconnectError);
}
// If still having connection issues
if (hasConnectionError) {
return {
content: [
{
type: "text",
text: "Database connection error. The server has attempted to reconnect but was unsuccessful.",
},
{
type: "text",
text: "Please try again later or check your database connection configuration.",
},
],
};
}
}
}
// Log the code for debugging (but sanitize it first to avoid exposing sensitive data)
const sanitizedCode = args.code.replace(/password\s*[:=]\s*["'].*?["']/gi, 'password: "[REDACTED]"');
console.error(`[execute-typescript] Executing code:\n${sanitizedCode}`);
// Set memory limit (default to 512MB)
const memoryLimitMB = args.memory_limit || 512;
const memoryLimitBytes = memoryLimitMB * 1024 * 1024;
// Set up the evaluation context with explicit log capture and error handling
let capturedLogs: string[] = [];
let hasUncaughtError = false;
// Create a sandboxed context with enhanced utilities
const context = {
e, // EdgeQL query builder
gelClient: args.use_gel_client !== false ? gelClient : undefined,
console: {
log: (...args: any[]) => {
try {
const logMsg = args.map(a => {
try {
return typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a);
} catch (jsonError) {
return `[Unserializable object: ${typeof a}]`;
}
}).join(' ');
// Truncate very long log messages to prevent memory issues
const maxLogLength = 10000;
const truncatedMsg = logMsg.length > maxLogLength
? logMsg.substring(0, maxLogLength) + `... [truncated, ${logMsg.length - maxLogLength} more characters]`
: logMsg;
capturedLogs.push(truncatedMsg);
process.stderr.write(`[TS Script Log]: ${truncatedMsg.substring(0, 1000)}${truncatedMsg.length > 1000 ? '...' : ''}\n`);
} catch (logError) {
process.stderr.write(`[TS Script Log Error]: Failed to process log message\n`);
}
},
error: (...args: any[]) => {
try {
const logMsg = args.map(a => {
try {
return typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a);
} catch (jsonError) {
return `[Unserializable object: ${typeof a}]`;
}
}).join(' ');
const truncatedMsg = logMsg.length > 10000
? logMsg.substring(0, 10000) + `... [truncated, ${logMsg.length - 10000} more characters]`
: logMsg;
capturedLogs.push(`ERROR: ${truncatedMsg}`);
process.stderr.write(`[TS Script Error]: ${truncatedMsg.substring(0, 1000)}${truncatedMsg.length > 1000 ? '...' : ''}\n`);
} catch (logError) {
process.stderr.write(`[TS Script Error]: Failed to process error message\n`);
}
},
},
// Utilities with safety guards
setTimeout: (fn: Function, ms: number) => {
// Ensure timeouts don't exceed the overall execution timeout
const safeMs = Math.min(ms, args.timeout || 5000);
return setTimeout(fn, safeMs);
},
clearTimeout,
setInterval,
clearInterval,
Promise,
String,
Number,
Boolean,
Array,
Object,
JSON,
Date,
Math,
Error,
// Add utility function for safe database queries
safeQuery: async (queryStr: string, params: any = {}) => {
try {
if (!gelClient) throw new Error("Database client not available");
return { success: true, data: await gelClient.query(queryStr, params), error: null };
} catch (queryError: any) {
return {
success: false,
data: null,
error: queryError?.message || String(queryError),
code: queryError?.code
};
}
}
};
// Prepare the code for execution with a well-instrumented IIFE
// Include memory usage monitoring, timeouts, and comprehensive error handling
const scriptToExecute = `
(async () => {
console.log("[Inside] Starting script");
// Define safeguards
let executionStartTime = Date.now();
let executionStats = {
executionTime: 0,
errorCount: 0
};
// Simple monitoring function
const checkExecution = () => {
try {
// Update execution time
executionStats.executionTime = Date.now() - executionStartTime;
} catch (checkError) {
console.error("Execution check failed:", checkError.message);
}
};
// Set up periodic checks
const checkInterval = setInterval(checkExecution, 500);
try {
// Execute the user code in a controlled manner
let __result;
__result = await (async () => {
// Insert user code here, wrapped in extra try/catch
try {
${args.code}
} catch (userCodeError) {
console.error("User code error:", userCodeError.message);
if (userCodeError.stack) console.error(userCodeError.stack);
throw userCodeError;
}
})();
// Clean up
clearInterval(checkInterval);
console.log("[Inside] Code executed");
// Final check
checkExecution();
return {
result: __result,
stats: executionStats
};
} catch (error) {
// Clean up
clearInterval(checkInterval);
// Check if this is a database connection error
const isConnectionError = error.message && (
error.message.includes("Not connected") ||
error.message.includes("Connection closed") ||
error.message.includes("network") ||
error.message.includes("socket") ||
error.message.includes("ECONNRESET") ||
error.message.includes("timeout")
);
// Log error details
console.error("Script execution error:", error.message);
if (error.stack) console.error(error.stack);
executionStats.errorCount++;
return {
error: error.message || "Unknown error occurred",
stack: error.stack,
isConnectionError: isConnectionError,
stats: executionStats
};
}
})()
`;
// Set up timeout mechanism
const timeoutMs = args.timeout || 5000;
let timeoutId: NodeJS.Timeout;
const timeoutPromise = new Promise((_, reject) => {
timeoutId = setTimeout(() => {
isExecutionCompleted = true;
reject(new Error(`Execution timed out after ${timeoutMs}ms`));
}, timeoutMs);
});
console.error("[execute-typescript] Creating execution script");
// Use vm module for execution with resource constraints
const vm = require('vm');
try {
const script = new vm.Script(scriptToExecute, {
filename: 'user-script.js',
timeout: timeoutMs, // Apply the timeout limit to compilation as well
});
// Create isolated context
const contextObject = vm.createContext(context);
console.error("[execute-typescript] Starting execution with timeout:", timeoutMs);
const executionPromise = Promise.resolve(script.runInContext(contextObject, {
timeout: timeoutMs,
displayErrors: true,
}));
// Race between execution and timeout
const result = await Promise.race([executionPromise, timeoutPromise])
.finally(() => {
clearTimeout(timeoutId);
isExecutionCompleted = true;
});
// Calculate execution metrics
const executionTime = Date.now() - startTime;
console.error(`[execute-typescript] Execution completed in ${executionTime}ms, result type:`, typeof result);
// Check if there was a connection error in the result
if (result && result.isConnectionError) {
hasConnectionError = true;
console.error("[execute-typescript] Database connection error detected in script execution");
}
// Add execution stats to the output
const executionStats = result && result.stats ? result.stats : {
executionTime,
errorCount: result && result.error ? 1 : 0
};
// Handle potential connection errors
if (hasConnectionError) {
// Add a note about connection issues
capturedLogs.push("NOTE: A database connection error occurred. The server might need to be restarted if this persists.");
}
// If result is too large, truncate it
let resultStr;
try {
resultStr = JSON.stringify(result && result.result ? result.result : result, null, 2);
if (resultStr.length > 50000) {
resultStr = resultStr.substring(0, 50000) + `... [truncated, ${resultStr.length - 50000} more characters]`;
}
} catch (stringifyError) {
resultStr = `[Unserializable result of type ${typeof result}]`;
}
// Return the result, captured logs, and execution stats
return {
content: [
{
type: "text",
text: `Execution result:\n${resultStr}`,
},
{
type: "text",
text: capturedLogs.length > 0 ? `Console output:\n${capturedLogs.join('\n')}` : "No console output.",
},
{
type: "text",
text: `Execution stats: Time ${executionStats.executionTime}ms | Errors ${executionStats.errorCount}`,
},
],
};
} catch (vmError: any) {
console.error("[execute-typescript] VM execution error:", vmError);
// Handle specific types of VM errors
let errorMessage = vmError?.message || "Unknown VM error";
let errorType = "VM Error";
// Check for specific error types
if (errorMessage.includes("timed out")) {
errorType = "Timeout Error";
errorMessage = `Script execution timed out after ${timeoutMs}ms. Please simplify your code or increase the timeout limit.`;
} else if (errorMessage.includes("memory")) {
errorType = "Memory Error";
errorMessage = `Script exceeded memory limits. Please reduce memory usage in your code.`;
} else if (errorMessage.includes("Unexpected token")) {
errorType = "Syntax Error";
}
// Include any captured logs to help with debugging
return {
content: [
{
type: "text",
text: `${errorType}: ${errorMessage}`,
},
{
type: "text",
text: vmError?.stack || "",
},
{
type: "text",
text: capturedLogs.length > 0 ? `Console output (before error):\n${capturedLogs.join('\n')}` : "No console output before error.",
},
],
};
}
} catch (error: any) {
console.error("[execute-typescript] Top level error:", error);
// Categorize the error
let errorCategory = "Uncategorized Error";
const errorMsg = error?.message || "Unknown error";
if (errorMsg.includes("out of memory") || errorMsg.includes("heap") || errorMsg.includes("memory")) {
errorCategory = "Out of Memory Error";
} else if (errorMsg.includes("timeout") || errorMsg.includes("timed out")) {
errorCategory = "Timeout Error";
} else if (errorMsg.includes("connection") || errorMsg.includes("network") || errorMsg.includes("socket")) {
errorCategory = "Connection Error";
} else if (errorMsg.includes("syntax") || errorMsg.includes("unexpected token")) {
errorCategory = "Syntax Error";
}
return {
content: [
{
type: "text",
text: `${errorCategory} executing TypeScript: ${errorMsg}`,
},
{
type: "text",
text: error?.stack || "",
},
{
type: "text",
text: "Consider simplifying your code, checking for syntax errors, or trying again later.",
},
],
};
}
},
);
// Tool to search the Gel documentation file
server.tool(
"search_gel_docs",
"Search the Gel documentation for specific terms or patterns. Returns relevant sections with context. Use this tool to find information about EdgeQL syntax, features, or examples in the documentation.",
{
search_term: z.string().describe("The term or pattern to search for in the documentation"),
context_lines: z.number().optional().describe("Number of lines of context to show before and after matches (default: 5)"),
match_all_terms: z.boolean().optional().describe("If true, search must match all terms in a multi-word query (default: false)"),
},
async (args, extra) => {
try {
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
// Try multiple possible locations for the documentation file
const possiblePaths = [
path.join(__dirname, '..', 'gel_llm.txt'), // Relative to script dir
path.join(process.cwd(), 'gel_llm.txt'), // Relative to current working dir
'./gel_llm.txt' // Directly in workspace root
];
let docFilePath = null;
for (const potentialPath of possiblePaths) {
if (fs.existsSync(potentialPath)) {
docFilePath = potentialPath;
console.error(`[search-gel-docs] Found documentation at: ${docFilePath}`);
break;
}
}
// Validate that the file exists
if (!docFilePath) {
return {
content: [
{
type: "text" as const,
text: "Documentation file not found. Checked paths: " + possiblePaths.join(', '),
},
],
};
}
// Set default values
const contextLines = args.context_lines || 20;
const matchAllTerms = args.match_all_terms || false;
const searchTerm = args.search_term;
console.error("[search-gel-docs] Starting search for:", searchTerm);
// Build the grep command
let grepCommand;
if (matchAllTerms && searchTerm.includes(' ')) {
// For multi-term search with matchAllTerms, we need to perform multiple grep operations
const terms = searchTerm.split(/\s+/).filter(t => t.trim().length > 0);
grepCommand = `grep -n -i "${terms[0]}" "${docFilePath}"`;
for (let i = 1; i < terms.length; i++) {
grepCommand += ` | grep -i "${terms[i]}"`;
}
} else {
// For single term search, we can use grep with line numbers
grepCommand = `grep -n -i "${searchTerm}" "${docFilePath}"`;
}
try {
// Execute the command
const grepOutput = execSync(grepCommand, { encoding: 'utf8' });
const matches = grepOutput.trim().split('\n').filter(Boolean);
console.error(`[search-gel-docs] Found ${matches.length} matching lines`);
if (matches.length === 0) {
return {
content: [{ type: "text" as const, text: "No matches found." }]
};
}
// Parse match lines into line numbers and content
const parsedMatches = matches.map((match: string) => {
const colonIndex = match.indexOf(':');
if (colonIndex !== -1) {
return {
lineNumber: parseInt(match.substring(0, colonIndex), 10),
lineContent: match.substring(colonIndex + 1)
};
}
return null;
}).filter(Boolean);
// Read the file for context
const fileLines = fs.readFileSync(docFilePath, 'utf8').split('\n');
// Generate output with context
let output = `Found ${parsedMatches.length} matches for "${searchTerm}":\n\n`;
// Track sections to group results
const sectionMatches: Record<string, {
matches: Array<{lineNumber: number, lineContent: string}>;
context?: string[];
}> = {};
// Identify sections
const sectionBoundaries: Array<{lineNumber: number, title: string}> = [];
// Find file boundaries and major section headers
for (let i = 0; i < fileLines.length; i++) {
const line = fileLines[i].trim();
// Check for file declarations
if (line.startsWith('.. File:')) {
sectionBoundaries.push({
lineNumber: i,
title: line.substring('.. File:'.length).trim()
});
continue;
}
// Check for section headers (underlined titles)
if (i > 0 && i < fileLines.length - 1) {
const prevLine = fileLines[i-1].trim();
// Section markers are lines of repeated characters
if (line.length > 0 && /^[=\-~"'`.+_^*]+$/.test(line) && prevLine.length > 0) {
sectionBoundaries.push({
lineNumber: i-1,
title: prevLine
});
}
}
}
// Sort section boundaries by line number
sectionBoundaries.sort((a, b) => a.lineNumber - b.lineNumber);
// Group matches by section
for (const match of parsedMatches) {
// Find the section this match belongs to
let sectionTitle = "Unknown";
for (let i = sectionBoundaries.length - 1; i >= 0; i--) {
if (match.lineNumber >= sectionBoundaries[i].lineNumber) {
sectionTitle = sectionBoundaries[i].title;
break;
}
}
if (!sectionMatches[sectionTitle]) {
sectionMatches[sectionTitle] = { matches: [] };
}
sectionMatches[sectionTitle].matches.push(match);
}
// Format output by section
for (const [sectionTitle, { matches }] of Object.entries(sectionMatches)) {
output += `## ${sectionTitle}\n\n`;
// Get context for the first match in each section
const firstMatch = matches[0];
const startLine = Math.max(0, firstMatch.lineNumber - contextLines);
const endLine = Math.min(fileLines.length - 1, firstMatch.lineNumber + contextLines);
// Show context with line numbers, highlighting the matching line
for (let i = startLine; i <= endLine; i++) {
const isMatchLine = matches.some(m => m.lineNumber === i);
const prefix = isMatchLine ? '> ' : ' ';
output += `${prefix}${i+1}: ${fileLines[i]}\n`;
}
if (matches.length > 1) {
output += `\n... and ${matches.length - 1} more match(es) in this section.\n`;
}
output += '\n---\n\n';
}
return {
content: [{ type: "text" as const, text: output }]
};
} catch (err) {
// If grep exits with code 1, it means no matches were found (not an error)
const error = err as {status?: number; stderr?: string};
if (error.status === 1 && !error.stderr) {
return {
content: [{ type: "text" as const, text: "No matches found." }]
};
}
throw err;
}
} catch (error: unknown) {
console.error("Error searching documentation:", error);
return {
content: [
{
type: "text" as const,
text: `Error searching documentation: ${error instanceof Error ? error.message : String(error)}`,
},
],
};
}
}
);
// Start the server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
const connected = await initGelClient();
console.error("Gel MCP Server running on stdio");
}
main().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});