Skip to main content
Glama

SurrealDB MCP Server

by nsxdavid
#!/usr/bin/env node /** * SurrealDB MCP Server * * This MCP server provides a standardized interface for AI assistants to interact with a SurrealDB database. * It implements tools for common SurrealDB operations including: * - Executing raw SurrealQL queries * - Selecting, creating, updating, and deleting records * - Merging and patching data * - Upserting records * - Inserting multiple records * - Creating graph relations between records */ // Redirect console.log and console.error to ensure no logs go to stdout console.log = (...args) => process.stderr.write(args.join(' ') + '\n'); console.error = (...args) => process.stderr.write(args.join(' ') + '\n'); // MCP SDK Imports import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListPromptsRequestSchema, McpError, ErrorCode, } from "@modelcontextprotocol/sdk/types.js"; // SurrealDB Import import { Surreal, RecordId } from "surrealdb"; // Import RecordId import { inspect } from "util"; // Custom logger that writes to stderr to avoid interfering with JSON-RPC communication const logger = { info: (...args: unknown[]) => { const timestamp = new Date().toISOString(); process.stderr.write(`${timestamp} [surrealdb] [info] ${args.map(arg => typeof arg === 'string' ? arg : inspect(arg)).join(' ')}\n`); }, error: (...args: unknown[]) => { const timestamp = new Date().toISOString(); process.stderr.write(`${timestamp} [surrealdb] [error] ${args.map(arg => typeof arg === 'string' ? arg : inspect(arg)).join(' ')}\n`); } }; // --- Database Configuration --- // Read configuration from environment variables provided by MCP host const DB_ENDPOINT = process.env.SURREALDB_URL; const DB_NAMESPACE = process.env.SURREALDB_NS; const DB_DATABASE = process.env.SURREALDB_DB; const DB_USER = process.env.SURREALDB_USER; const DB_PASS = process.env.SURREALDB_PASS; // ----------------------------- // Validate that all required environment variables are set if (!DB_ENDPOINT || !DB_NAMESPACE || !DB_DATABASE || !DB_USER || !DB_PASS) { logger.error( "FATAL ERROR: Missing one or more required SurrealDB environment variables (SURREALDB_URL, SURREALDB_NS, SURREALDB_DB, SURREALDB_USER, SURREALDB_PASS)" ); process.exit(1); } // Instantiate the SurrealDB client const db = new Surreal(); /** * Create an MCP server instance. * Capabilities will be added as tools are implemented. */ // Create server instance const server = new Server( { name: "surrealdb-mcp-server", version: "0.1.10", // Updated to match current package version }, { // Declare tool capability capabilities: { tools: {}, // This enables tool-related handlers like ListTools and CallTool resources: {}, // Add resources capability prompts: {}, // Add prompts capability }, } ); // --- MCP Request Handlers --- // Handler for listing resources (required by MCP protocol) server.setRequestHandler(ListResourcesRequestSchema, async () => { logger.info("Handling ListResources request"); return { resources: [], // Return empty array as we don't have any resources }; }); // Handler for listing resource templates (required by MCP protocol) server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => { logger.info("Handling ListResourceTemplates request"); return { resourceTemplates: [], // Return empty array as we don't have any resource templates }; }); // Handler for listing prompts (required by MCP protocol) server.setRequestHandler(ListPromptsRequestSchema, async () => { logger.info("Handling ListPrompts request"); return { prompts: [], // Return an empty array as we don't have any prompts }; }); // Handler for listing available tools server.setRequestHandler(ListToolsRequestSchema, async () => { logger.info("Handling ListTools request"); return { tools: [ { name: "query", description: "Execute a raw SurrealQL query against the connected database.", inputSchema: { type: "object", properties: { query_string: { type: "string", description: "The SurrealQL query string to execute.", }, }, required: ["query_string"], }, }, { name: "select", description: "Select all records from a table or a specific record by ID.", inputSchema: { type: "object", properties: { table: { type: "string", description: "The name of the table to select from.", }, id: { type: "string", description: "Optional: The specific ID of the record to select (e.g., 'user:john'). If omitted, selects all records.", }, }, required: ["table"], }, }, { name: "create", description: "Create a new record in a table with the specified data.", inputSchema: { type: "object", properties: { table: { type: "string", description: "The name of the table to create the record in.", }, data: { type: "object", description: "An object containing the data for the new record.", additionalProperties: true, // Allow any properties in the data object }, }, required: ["table", "data"], }, }, { name: "update", description: "Update a specific record with new data. Replaces the entire record content.", inputSchema: { type: "object", properties: { thing: { type: "string", description: "The full record ID to update (e.g., 'table:id').", }, data: { type: "object", description: "An object containing the new data for the record.", additionalProperties: true, }, }, required: ["thing", "data"], }, }, { name: "delete", description: "Delete a specific record by ID.", inputSchema: { type: "object", properties: { thing: { type: "string", description: "The full record ID to delete (e.g., 'table:id').", }, }, required: ["thing"], }, }, { name: "merge", description: "Merge data into a specific record. Only updates specified fields.", inputSchema: { type: "object", properties: { thing: { type: "string", description: "The full record ID to merge data into (e.g., 'table:id').", }, data: { type: "object", description: "An object containing the data to merge into the record.", additionalProperties: true, }, }, required: ["thing", "data"], }, }, { name: "patch", description: "Apply JSON patches to a specific record.", inputSchema: { type: "object", properties: { thing: { type: "string", description: "The full record ID to patch (e.g., 'table:id').", }, patches: { type: "array", description: "An array of JSON Patch operations (RFC 6902).", items: { type: "object", properties: { op: { type: "string", enum: ["add", "remove", "replace", "move", "copy", "test"], }, path: { type: "string" }, value: { description: "Value for add, replace, test operations", }, from: { type: "string", description: "Source path for move, copy operations", }, }, required: ["op", "path"], }, }, }, required: ["thing", "patches"], }, }, { name: "upsert", description: "Upsert a record: create if it doesn't exist, update if it does.", inputSchema: { type: "object", properties: { thing: { type: "string", description: "The full record ID to upsert (e.g., 'table:id').", }, data: { type: "object", description: "An object containing the data for the record.", additionalProperties: true, }, }, required: ["thing", "data"], }, }, { name: "insert", description: "Insert multiple records into a table. Use 'create' for single records.", inputSchema: { type: "object", properties: { table: { type: "string", description: "The name of the table to insert records into.", }, data: { type: "array", description: "An array of objects, each representing a record to insert.", items: { type: "object", additionalProperties: true, }, }, }, required: ["table", "data"], }, }, { name: "insertRelation", description: "Create a graph relation (edge) between two records.", inputSchema: { type: "object", properties: { from_thing: { type: "string", description: "The full record ID of the 'from' record (e.g., 'user:john').", }, relation_name: { type: "string", description: "The name of the relation/edge table (e.g., 'likes').", }, to_thing: { type: "string", description: "The full record ID of the 'to' record (e.g., 'product:apple').", }, data: { type: "object", description: "Optional: An object containing data for the relation itself.", additionalProperties: true, }, }, required: ["from_thing", "relation_name", "to_thing"], }, }, ], }; }); // Handler for executing tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { switch (request.params.name) { case "query": { // Validate input arguments const queryString = request.params.arguments?.query_string; if (typeof queryString !== "string" || !queryString.trim()) { throw new McpError( ErrorCode.InvalidParams, "Missing or invalid 'query_string' argument." ); } try { logger.info(`Executing query tool with: ${queryString}`); // Execute the query using the globally connected db instance const result = await db.query(queryString); logger.info("Query executed successfully via tool."); // Return the result (SurrealDB returns an array of results per query) return { content: [ { type: "text", // Convert result to JSON string for transport text: JSON.stringify(result, null, 2), }, ], }; } catch (e) { logger.error(`Error executing query tool:`, e); // Log full error object // Rethrow as an MCPError for the client throw new McpError( ErrorCode.InternalError, `SurrealDB query failed: ${e instanceof Error ? e.message : String(e)}` ); } } case "select": { // Validate input arguments const table = request.params.arguments?.table; const id = request.params.arguments?.id; // Optional if (typeof table !== "string" || !table.trim()) { throw new McpError( ErrorCode.InvalidParams, "Missing or invalid 'table' argument." ); } if (id !== undefined && (typeof id !== "string" || !id.trim())) { // Allow empty string ID? For now, require non-empty if provided. throw new McpError( ErrorCode.InvalidParams, "Invalid 'id' argument. Must be a non-empty string if provided." ); } // Construct the 'thing' string for SurrealDB (e.g., 'user' or 'user:john') // Ensure ID is correctly formatted if it contains the table prefix already let thing: string; if (id) { // If ID already contains ':', assume it's fully qualified (e.g., 'table:id') // Otherwise, prepend the table name. thing = id.includes(":") ? id : `${table}:${id}`; } else { thing = table; // Select all from table } // Determine what to pass to db.select based on whether ID is present // If ID is present, create a RecordId instance. // If not, pass the table name string. let selectTarget: string | RecordId; if (id) { // Extract the ID part if the input 'id' is already 'table:id' const idPart = id.includes(":") ? id.split(":")[1] : id; selectTarget = new RecordId(table, idPart); } else { selectTarget = table; } try { // Log the type being passed to db.select logger.info( `Executing select tool for target: ${thing} (passing type: ${selectTarget.constructor.name}) using db.select` ); // Execute the select using the globally connected db instance const result = await db.select(selectTarget); logger.info(`Select executed successfully for target: ${thing}.`); // Check if result is empty (record not found) const responseText = result && (!Array.isArray(result) || result.length > 0) ? JSON.stringify(result, null, 2) : "[]"; // Return empty array string if not found return { content: [ { type: "text", text: responseText, }, ], }; } catch (e) { logger.error(`Error executing select tool for ${thing}:`, e); // Log full error object // Rethrow as an MCPError for the client throw new McpError( ErrorCode.InternalError, `SurrealDB select failed for ${thing}: ${e instanceof Error ? e.message : String(e)}` ); } } case "create": { // Validate input arguments const table = request.params.arguments?.table; const data = request.params.arguments?.data; if (typeof table !== "string" || !table.trim()) { throw new McpError( ErrorCode.InvalidParams, "Missing or invalid 'table' argument." ); } if (typeof data !== "object" || data === null || Array.isArray(data)) { throw new McpError( ErrorCode.InvalidParams, "Missing or invalid 'data' argument. Must be an object." ); } try { logger.info(`Executing create tool for table: ${table}`); // Execute the create using the globally connected db instance // We pass the table name and the data object directly // Assert data type to satisfy TypeScript's index signature requirement const result = await db.create( table, data as { [key: string]: unknown } ); logger.info(`Create executed successfully for table: ${table}.`); // Return the created record(s) (db.create returns an array) return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], }; } catch (e) { logger.error(`Error executing create tool for table ${table}:`, e); // Log full error object // Rethrow as an MCPError for the client throw new McpError( ErrorCode.InternalError, `SurrealDB create failed for table ${table}: ${e instanceof Error ? e.message : String(e)}` ); } } case "update": { // Validate input arguments const thing = request.params.arguments?.thing; const data = request.params.arguments?.data; if (typeof thing !== "string" || !thing.includes(":")) { throw new McpError( ErrorCode.InvalidParams, "Missing or invalid 'thing' argument. Must be a full record ID (e.g., 'table:id')." ); } if (typeof data !== "object" || data === null || Array.isArray(data)) { throw new McpError( ErrorCode.InvalidParams, "Missing or invalid 'data' argument. Must be an object." ); } // Similar to db.select, db.update likely requires a RecordId instance // despite documentation examples showing a string. const [table, idPart] = thing.split(":"); if (!table || !idPart) { throw new McpError( ErrorCode.InvalidParams, "Invalid 'thing' format. Must be 'table:id'." ); } const recordId = new RecordId(table, idPart); try { logger.info(`Executing update tool for: ${thing} (using RecordId)`); // Execute the update using the globally connected db instance // Pass the RecordId instance and assert data type const result = await db.update( recordId, data as { [key: string]: unknown } ); logger.info(`Update executed successfully for: ${thing}.`); // Return the updated record (db.update returns the updated record or array if multiple) // Note: The SDK documentation for update is slightly ambiguous on single vs array return, // but testing shows it returns the single updated object for a specific ID. return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], }; } catch (e) { logger.error(`Error executing update tool for ${thing}:`, e); // Log full error object // Rethrow as an MCPError for the client throw new McpError( ErrorCode.InternalError, `SurrealDB update failed for ${thing}: ${e instanceof Error ? e.message : String(e)}` ); } } case "delete": { // Validate input arguments const thing = request.params.arguments?.thing; if (typeof thing !== "string" || !thing.includes(":")) { throw new McpError( ErrorCode.InvalidParams, "Missing or invalid 'thing' argument. Must be a full record ID (e.g., 'table:id')." ); } // Assume db.delete also requires a RecordId instance based on select/update behavior const [table, idPart] = thing.split(":"); if (!table || !idPart) { throw new McpError( ErrorCode.InvalidParams, "Invalid 'thing' format. Must be 'table:id'." ); } const recordId = new RecordId(table, idPart); try { logger.info(`Executing delete tool for: ${thing} (using RecordId)`); // Execute the delete using the globally connected db instance // Pass the RecordId instance const result = await db.delete(recordId); logger.info(`Delete executed successfully for: ${thing}.`); // Check if result is empty (record might not have existed) // db.delete returns the deleted record(s) or potentially undefined/empty array if not found const responseText = result && (!Array.isArray(result) || result.length > 0) ? JSON.stringify(result, null, 2) : "Record not found or already deleted."; // Provide informative message return { content: [ { type: "text", text: responseText, }, ], }; } catch (e) { logger.error(`Error executing delete tool for ${thing}:`, e); // Log full error object // Rethrow as an MCPError for the client throw new McpError( ErrorCode.InternalError, `SurrealDB delete failed for ${thing}: ${e instanceof Error ? e.message : String(e)}` ); } } case "merge": { // Validate input arguments const thing = request.params.arguments?.thing; const data = request.params.arguments?.data; if (typeof thing !== "string" || !thing.includes(":")) { throw new McpError( ErrorCode.InvalidParams, "Missing or invalid 'thing' argument. Must be a full record ID (e.g., 'table:id')." ); } if (typeof data !== "object" || data === null || Array.isArray(data)) { throw new McpError( ErrorCode.InvalidParams, "Missing or invalid 'data' argument. Must be an object." ); } // db.merge likely requires a RecordId instance const [table, idPart] = thing.split(":"); if (!table || !idPart) { throw new McpError( ErrorCode.InvalidParams, "Invalid 'thing' format. Must be 'table:id'." ); } const recordId = new RecordId(table, idPart); try { logger.info(`Executing merge tool for: ${thing} (using RecordId)`); // Execute the merge using the globally connected db instance // Pass the RecordId instance and assert data type const result = await db.merge( recordId, data as { [key: string]: unknown } ); logger.info(`Merge executed successfully for: ${thing}.`); // Return the merged record return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], }; } catch (e) { logger.error(`Error executing merge tool for ${thing}:`, e); // Log full error object // Rethrow as an MCPError for the client throw new McpError( ErrorCode.InternalError, `SurrealDB merge failed for ${thing}: ${e instanceof Error ? e.message : String(e)}` ); } } case "patch": { // Validate input arguments const thing = request.params.arguments?.thing; const patches = request.params.arguments?.patches; if (typeof thing !== "string" || !thing.includes(":")) { throw new McpError( ErrorCode.InvalidParams, "Missing or invalid 'thing' argument. Must be a full record ID (e.g., 'table:id')." ); } // Basic validation for patches array - could be more thorough if (!Array.isArray(patches) || patches.length === 0) { throw new McpError( ErrorCode.InvalidParams, "Missing or invalid 'patches' argument. Must be a non-empty array of patch operations." ); } // Add more specific validation for each patch object if needed // db.patch likely requires a RecordId instance const [table, idPart] = thing.split(":"); if (!table || !idPart) { throw new McpError( ErrorCode.InvalidParams, "Invalid 'thing' format. Must be 'table:id'." ); } const recordId = new RecordId(table, idPart); try { logger.info(`Executing patch tool for: ${thing} (using RecordId)`); // Execute the patch using the globally connected db instance // Pass the RecordId instance and the patches array // Use a more generic type assertion to avoid compatibility issues // eslint-disable-next-line @typescript-eslint/no-explicit-any const result = await db.patch(recordId, patches as any); logger.info(`Patch executed successfully for: ${thing}.`); // Return the patched record return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], }; } catch (e) { logger.error(`Error executing patch tool for ${thing}:`, e); // Log full error object // Rethrow as an MCPError for the client throw new McpError( ErrorCode.InternalError, `SurrealDB patch failed for ${thing}: ${e instanceof Error ? e.message : String(e)}` ); } } case "upsert": { // Validate input arguments const thing = request.params.arguments?.thing; const data = request.params.arguments?.data; if (typeof thing !== "string" || !thing.includes(":")) { throw new McpError( ErrorCode.InvalidParams, "Missing or invalid 'thing' argument. Must be a full record ID (e.g., 'table:id')." ); } if (typeof data !== "object" || data === null || Array.isArray(data)) { throw new McpError( ErrorCode.InvalidParams, "Missing or invalid 'data' argument. Must be an object." ); } // db.upsert likely requires a RecordId instance const [table, idPart] = thing.split(":"); if (!table || !idPart) { throw new McpError( ErrorCode.InvalidParams, "Invalid 'thing' format. Must be 'table:id'." ); } const recordId = new RecordId(table, idPart); try { logger.info(`Executing upsert tool for: ${thing} (using RecordId)`); // Execute the upsert using the globally connected db instance // Pass the RecordId instance and assert data type const result = await db.upsert( recordId, data as { [key: string]: unknown } ); logger.info(`Upsert executed successfully for: ${thing}.`); // Return the upserted record return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], }; } catch (e) { logger.error(`Error executing upsert tool for ${thing}:`, e); // Log full error object // Rethrow as an MCPError for the client throw new McpError( ErrorCode.InternalError, `SurrealDB upsert failed for ${thing}: ${e instanceof Error ? e.message : String(e)}` ); } } case "insert": { // Validate input arguments const table = request.params.arguments?.table; const data = request.params.arguments?.data; if (typeof table !== "string" || !table.trim()) { throw new McpError( ErrorCode.InvalidParams, "Missing or invalid 'table' argument." ); } if ( !Array.isArray(data) || data.length === 0 || !data.every((item) => typeof item === "object" && item !== null) ) { throw new McpError( ErrorCode.InvalidParams, "Missing or invalid 'data' argument. Must be a non-empty array of objects." ); } try { logger.info( `Executing insert tool for table: ${table} with ${data.length} records` ); // Execute the insert using the globally connected db instance // Pass the table name and the array of data objects const result = await db.insert( table, data as { [key: string]: unknown }[] ); logger.info(`Insert executed successfully for table: ${table}.`); // Return the inserted records (db.insert returns an array) return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], }; } catch (e) { logger.error(`Error executing insert tool for table ${table}:`, e); // Log full error object // Rethrow as an MCPError for the client throw new McpError( ErrorCode.InternalError, `SurrealDB insert failed for table ${table}: ${e instanceof Error ? e.message : String(e)}` ); } } case "insertRelation": { // Validate input arguments const fromThing = request.params.arguments?.from_thing; const relationName = request.params.arguments?.relation_name; const toThing = request.params.arguments?.to_thing; const data = request.params.arguments?.data; // Optional if (typeof fromThing !== "string" || !fromThing.includes(":")) { throw new McpError( ErrorCode.InvalidParams, "Missing or invalid 'from_thing' argument. Must be a full record ID (e.g., 'table:id')." ); } if (typeof relationName !== "string" || !relationName.trim()) { throw new McpError( ErrorCode.InvalidParams, "Missing or invalid 'relation_name' argument." ); } if (typeof toThing !== "string" || !toThing.includes(":")) { throw new McpError( ErrorCode.InvalidParams, "Missing or invalid 'to_thing' argument. Must be a full record ID (e.g., 'table:id')." ); } if ( data !== undefined && (typeof data !== "object" || data === null || Array.isArray(data)) ) { throw new McpError( ErrorCode.InvalidParams, "Invalid 'data' argument. Must be an object if provided." ); } // Parse record IDs const [fromTable, fromIdPart] = fromThing.split(":"); const [toTable, toIdPart] = toThing.split(":"); if (!fromTable || !fromIdPart || !toTable || !toIdPart) { throw new McpError( ErrorCode.InvalidParams, "Invalid 'from_thing' or 'to_thing' format. Must be 'table:id'." ); } const fromRecordId = new RecordId(fromTable, fromIdPart); const toRecordId = new RecordId(toTable, toIdPart); // Construct the data object for the relation record const relationData = { in: fromRecordId, out: toRecordId, ...(data as { [key: string]: unknown } | undefined), // Spread optional data }; try { logger.info( `Executing insertRelation tool: ${fromThing} -> ${relationName} -> ${toThing}` ); // Execute the insertRelation using the globally connected db instance // Pass the relation table name and the constructed data object const result = await db.insertRelation(relationName, relationData); logger.info(`insertRelation executed successfully.`); // Return the created relation record(s) (db.insertRelation returns an array) return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], }; } catch (e) { logger.error( `Error executing insertRelation tool (${fromThing} -> ${relationName} -> ${toThing}):`, e ); // Log full error object // Rethrow as an MCPError for the client throw new McpError( ErrorCode.InternalError, `SurrealDB insertRelation failed: ${e instanceof Error ? e.message : String(e)}` ); } } default: // Use McpError for unknown tools throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}` ); } }); // -------------------------- /** * Main function to start the server. * Connects to SurrealDB first, then starts the MCP server transport. */ async function main() { try { // Log connection attempt using the environment variable logger.info(`Attempting to connect to SurrealDB at ${DB_ENDPOINT}...`); // Connect to SurrealDB using environment variables // Use non-null assertion (!) because we've already validated these variables exist await db.connect(DB_ENDPOINT!, { namespace: DB_NAMESPACE!, database: DB_DATABASE!, auth: { username: DB_USER!, password: DB_PASS!, }, }); // Log success using the environment variables logger.info( `Successfully connected to SurrealDB (NS: ${DB_NAMESPACE}, DB: ${DB_DATABASE})` ); // Start the MCP server communication const transport = new StdioServerTransport(); await server.connect(transport); logger.info("SurrealDB MCP Server connected via stdio transport."); } catch (error) { logger.error("--- FATAL ERROR ---"); if (error instanceof Error) { logger.error( "Failed to connect to SurrealDB or start MCP server:", error.message ); logger.error(error.stack || "No stack trace available"); } else { logger.error("An unknown error occurred during startup:", error); } logger.error("-------------------"); // Ensure DB connection is closed if it was partially opened or if error occurred after connect await db.close(); process.exit(1); // Exit if we can't connect to the DB or start the server } } // Graceful shutdown process.on("SIGINT", async () => { logger.info("\nCaught interrupt signal (Ctrl+C)."); logger.info("Closing SurrealDB connection..."); await db.close(); logger.info("Database connection closed."); logger.info("Shutting down MCP server..."); await server.close(); // Close the MCP server itself logger.info("MCP server shut down."); process.exit(0); }); // Start the main application logic main().catch((error) => { // This catch is mainly for unexpected errors not caught within main's try/catch logger.error("Unhandled error in main execution:", error); process.exit(1); });

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/nsxdavid/surrealdb-mcp-server'

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