ArangoDB MCP Server

  • dist
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { Database, aql } from "arangojs"; import { z } from "zod"; import { zodToJsonSchema } from "zod-to-json-schema"; const server = new Server({ name: "arango-mcp-server", version: "0.1.0", }, { capabilities: { logging: {}, resources: {}, tools: {}, }, }); function info(data) { server.sendLoggingMessage({ level: "info", data: data, }); } function debug(data) { server.sendLoggingMessage({ level: "debug", data: data, }); } const args = process.argv.slice(2); if (args.length < 1) { console.error("Please provide a database URL"); process.exit(1); } // Database URL should be in the format: // "http://localhost:8529" const databaseUrl = args[0]; const username = args.length > 1 ? args[1] : undefined; const password = args.length > 2 ? args[2] : undefined; const auth = username && password ? { username, password } : undefined; console.error("databaseUrl: " + databaseUrl); console.error("auth: " + JSON.stringify(auth)); const resourceBaseUrl = new URL(databaseUrl); const db = new Database({ url: databaseUrl, auth: auth, }); async function getCollections(db) { const cursor = await db.query(aql ` RETURN COLLECTIONS() `); const result = await cursor.all(); const allCollections = []; for (const collectionArray of result) { for (const collection of collectionArray) { allCollections.push(collectionSchema.parse(collection)); } } return allCollections; } const collectionSchema = z .object({ _id: z.string(), name: z.string(), }) .strict(); server.setRequestHandler(ListResourcesRequestSchema, async () => { server.sendLoggingMessage({ level: "debug", data: `ListResourcesRequestSchema with url ${databaseUrl}`, }); const allCollections = await getCollections(db); console.error("collections: " + allCollections); console.error("collections:" + JSON.stringify(allCollections)); return { resources: allCollections.map((collection) => ({ uri: new URL(`${resourceBaseUrl}/_api/document/${collection.name}`), mimeType: "application/json", name: `"${collection.name}" http endpoint`, })), }; }); server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => { return { resourceTemplates: [ { uriTemplate: "arangodb:///{database}/{collection}/{documentID}", name: "ArangoDB document", mimeType: "application/json", description: "A document in an ArangoDB collection", }, ], }; }); class InvalidArangoDBURIError extends Error { constructor(message) { super(message); this.name = "InvalidArangoDBURIError"; } } /** * Parses and validates an ArangoDB URI string of the format: * arangodb:///databaseName/collectionName/documentID * * @param uri The ArangoDB URI string to parse * @returns An object containing the parsed components * @throws InvalidArangoDBURIError if the URI format is invalid */ export function parseArangoDBURI(uri) { // Check if the string starts with the correct prefix if (!uri.startsWith("arangodb:///")) { throw new InvalidArangoDBURIError('URI must start with "arangodb:///"'); } // Remove the prefix and split the remaining path const path = uri.slice("arangodb:///".length); const components = path.split("/"); // Validate we have exactly 3 components if (components.length !== 3) { throw new InvalidArangoDBURIError("URI must have exactly three components: databaseName/collectionName/documentId"); } // Validate each component is non-empty const [databaseName, collectionName, documentId] = components; if (!databaseName) { throw new InvalidArangoDBURIError("Database name cannot be empty"); } if (!collectionName) { throw new InvalidArangoDBURIError("Collection name cannot be empty"); } if (!documentId) { throw new InvalidArangoDBURIError("Document ID cannot be empty"); } // Optional: Add additional validation for component formats if needed // For example, checking for valid characters, length limits, etc. return { databaseName, collectionName, documentId, }; } server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const uri = request.params.uri; const arangodbURI = parseArangoDBURI(uri); const db = new Database({ url: databaseUrl, databaseName: arangodbURI.databaseName, auth: auth, }); try { const document = await db .collection(arangodbURI.collectionName) .document(arangodbURI.documentId); return { contents: [ { uri, mimeType: "application/json", text: JSON.stringify(document), }, ], }; } catch (error) { console.error("error: " + error); throw error; } }); const readQuerySchema = z.object({ databaseName: z.string(), aql: z.string(), }); const readWriteQuerySchema = z.object({ databaseName: z.string(), aql: z.string(), }); const listDatabasesSchema = z.object({}); const listCollectionsSchema = z.object({ databaseName: z.string(), }); var ToolName; (function (ToolName) { ToolName["READ_QUERY"] = "readQuery"; ToolName["READ_WRITE_QUERY"] = "readWriteQuery"; ToolName["LIST_DATABASES"] = "listDatabases"; ToolName["LIST_COLLECTIONS"] = "listCollections"; })(ToolName || (ToolName = {})); const databaseConnections = new Map(); function getOrCreateDatabaseConnection(databaseName) { if (databaseConnections.has(databaseName)) { return databaseConnections.get(databaseName); } const dbConnector = new Database({ url: databaseUrl, databaseName: databaseName, auth: auth, }); databaseConnections.set(databaseName, dbConnector); return dbConnector; } server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: ToolName.READ_QUERY, description: "Run a read-only AQL query", inputSchema: zodToJsonSchema(readQuerySchema), }, { name: ToolName.READ_WRITE_QUERY, description: "Run an AQL query", inputSchema: zodToJsonSchema(readWriteQuerySchema), }, { name: ToolName.LIST_DATABASES, description: "List all the databases", inputSchema: zodToJsonSchema(listDatabasesSchema), }, { name: ToolName.LIST_COLLECTIONS, description: "List all the collections in a database", inputSchema: zodToJsonSchema(listCollectionsSchema), } ], }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { server.sendLoggingMessage({ level: "debug", data: `CallToolRequest with name ${request.params.name}`, }); try { if (request.params.name === ToolName.READ_QUERY) { const dbConnector = getOrCreateDatabaseConnection(request.params.arguments?.databaseName); const aql = request.params.arguments?.aql; const allCollections = await getCollections(dbConnector); const tx = await dbConnector.beginTransaction({ read: allCollections.map((collection) => collection.name), }); const cursor = await tx.step(() => dbConnector.query(aql)); const result = await cursor.all(); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], isError: false, }; } else if (request.params.name === ToolName.READ_WRITE_QUERY) { const dbConnector = getOrCreateDatabaseConnection(request.params.arguments?.databaseName); const aql = request.params.arguments?.aql; const allCollections = await getCollections(dbConnector); const tx = await dbConnector.beginTransaction({ read: allCollections.map((collection) => collection.name), write: allCollections.map((collection) => collection.name), }); const cursor = await tx.step(() => dbConnector.query(aql)); const result = await cursor.all(); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], isError: false, }; } else if (request.params.name === ToolName.LIST_DATABASES) { server.sendLoggingMessage({ level: "debug", data: `listDatabases`, }); const allDatabases = await db.databases(); return { content: [{ type: "text", text: JSON.stringify(allDatabases.map(database => { return { name: database.name, }; }), null, 2) }], isError: false, }; } else if (request.params.name === ToolName.LIST_COLLECTIONS) { const dbConnector = getOrCreateDatabaseConnection(request.params.arguments?.databaseName); const allCollections = await getCollections(dbConnector); return { content: [{ type: "text", text: JSON.stringify(allCollections.map(collection => ({ name: collection.name })), null, 2) }], isError: false, }; } } catch (error) { server.sendLoggingMessage({ level: "error", data: `Error: ${error}`, }); console.error("error inside the CallToolRequestSchema: " + error); throw error; } throw new Error("Unknown tool"); }); async function runServer() { const transport = new StdioServerTransport(); await server.connect(transport); } runServer().catch(console.error);