Connecting AI Assistants to Your Database
A technical deep-dive into my experience creating an MCP server that bridges AI assistants with ArangoDB. Learn why I built this tool, the challenges I faced, and how it's improving our development team's productivity.
Why I Built an ArangoDB MCP Server
When our team started using AI assistants like Claude and GitHub Copilot for development work, I quickly identified a critical gap: these powerful tools had no way to interact with our ArangoDB database directly. This meant our developers were constantly context-switching between their AI assistant and database interfaces, copying query results, and manually explaining database structures.
This inefficiency was costing us hours each week. I envisioned a solution where our developers could ask their AI assistant questions like What's the schema of our users collection? or Find all transactions over $500 in the last week and get immediate results without leaving their workflow.
That's what led me to build our ArangoDB Model Context Protocol (MCP) server - a bridge that allows AI assistants to execute database operations and return results directly within the conversation.
This tool is designed for local development environments only. While technically it could connect to a production database, this would create significant security risks and is explicitly discouraged. We use it exclusively with our development databases to maintain separation of concerns and protect production data.
For more on properly structuring development, staging, and production environments, see my related journal post: The Three Environments: Best Practices for Development, Staging and Production.
The Technical Architecture
The MCP server is essentially a specialized API server that follows the Model Context Protocol specification. Here's how I structured it:
// Core server setup
export class ArangoMCPServer {
private db: Database | null = null;
private reconnectionAttempts = 0;
private server: Server;
constructor(private config: ArangoConfig) {
this.server = new Server(
{
name: 'arango-server',
version: packageJson.version,
},
{
capabilities: {
tools: this.buildToolsDefinitions(),
},
}
);
this.server.on('callTool', this.handleCallTool.bind(this));
}
}The most critical part was designing the tool definitions that expose specific database operations. Each tool needs a clear schema that defines exactly what parameters it accepts:
function buildToolsDefinitions(): Record<string, ToolDefinition> {
return {
[API_TOOLS.QUERY]: {
description: 'Zap AQL queries at the ArangoDB database',
parameters: {
type: 'object',
required: ['query'],
properties: {
query: {
type: 'string',
description: 'The AQL query string you wanna run',
},
bindVars: {
type: 'object',
description: 'Optional key/value stuff for parameterized queries',
},
},
},
},
arango_insert: {
description: 'Shove some docs into a collection',
parameters: {
type: 'object',
required: ['collection', 'document'],
properties: {
collection: { type: 'string', description: 'Name of the collection to jam it into' },
document: { type: 'object', description: 'The actual data (JSON object) you wanna add' },
},
},
},
arango_update: {
description: 'Tweak docs that are already there',
parameters: {
type: 'object',
required: ['collection', 'key', 'updateData'],
properties: {
collection: { type: 'string', description: 'Collection where the doc lives' },
key: { type: 'string', description: 'The unique key (_key) of the doc to change' },
updateData: { type: 'object', description: 'The fields you wanna update' },
},
},
},
arango_remove: {
description: 'Nuke docs from a collection',
parameters: {
type: 'object',
required: ['collection', 'key'],
properties: {
collection: { type: 'string', description: 'Collection to delete from' },
key: { type: 'string', description: 'The unique key (_key) of the doc to zap' },
},
},
},
arango_backup: {
description: 'Dump all the collections to JSON files',
parameters: {
type: 'object',
required: ['outputDir'],
properties: {
outputDir: { type: 'string', description: 'Path where the JSON backup files should go' },
},
},
},
arango_list_collections: {
description: 'Spit out all the collections in the DB',
parameters: { type: 'object', properties: {} }, // No params needed
},
arango_create_collection: {
description: 'Whip up a new collection',
parameters: {
type: 'object',
required: ['name'],
properties: {
name: { type: 'string', description: 'The name for your new collection' },
type: { type: 'string', enum: ['document', 'edge'], description: 'Type? Doc or edge (optional, defaults to document)' },
waitForSync: { type: 'boolean', description: 'Wait for writes to hit disk? (optional, defaults false)' },
},
},
},
};
}Enhancing the Blog Post
Some Results and Use Cases
Result for arango_query
{
"query": "FOR user IN users RETURN user",
"bindVars": {},
"result": [
{ "_key": "123", "name": "John Doe", "email": "john@example.com" },
{ "_key": "124", "name": "Jane Doe", "email": "jane@example.com" }
]
}Use Case for arango_insert
Imagine you want to add a new user to the users collection. You can use the arango_insert tool like this:
{
"collection": "users",
"document": { "name": "Alice", "email": "alice@example.com" }
}Result:
{
"_key": "125",
"_id": "users/125",
"_rev": "_Wz1A2B3C4D"
}Use Case for arango_update
Updating a user's email address:
{
"collection": "users",
"key": "123",
"updateData": { "email": "newjohn@example.com" }
}Result:
{
"_key": "123",
"_id": "users/123",
"_rev": "_XyZ1A2B3C4D"
}Use Case for arango_remove
Deleting a user:
{
"collection": "users",
"key": "124"
}Result:
{
"_key": "124",
"_id": "users/124",
"_rev": "_YzZ1A2B3C4D"
}Use Case for arango_backup
Backing up all collections:
{
"outputDir": "./backup"
}Result:
{
"status": "success",
"message": "Backup completed successfully. Files saved to ./backup"
}Use Cases via AI Interaction
One of the most exciting aspects of the ArangoDB MCP server is how it enables seamless interaction with AI models. Here's an example of how you can use it:
Example Prompt
Prompt: "I need a new user called Alice with alice@example.com."
AI Response:
"Sure! I'll add a new user to the users collection. Here's the operation I'll perform:"
{
"collection": "users",
"document": { "name": "Alice", "email": "alice@example.com" }
}Result:
{
"_key": "125",
"_id": "users/125",
"_rev": "_Wz1A2B3C4D"
}Another Example
Prompt: "Can you update John Doe's email to john.doe@example.com?"
AI Response:
"Got it! I'll update the email for the user with key 123 in the users collection. Here's the operation I'll perform:"
{
"collection": "users",
"key": "123",
"updateData": { "email": "john.doe@example.com" }
}Result:
{
"_key": "123",
"_id": "users/123",
"_rev": "_XyZ1A2B3C4D"
}Why This Matters
These examples demonstrate how the MCP server empowers AI models to act as intelligent database assistants. Developers can focus on what they need, and the AI handles the "how" by translating natural language prompts into database operations. The mentioned examples are really basic ones but actually you can get crazy with this and ask for complex queries that will possible give you better insights about your data. This reduces friction, accelerates development, and makes database interactions more intuitive.
The Connection Challenge
One of the trickiest technical challenges I encountered was maintaining a reliable database connection. Even in local development environments, connections can drop unexpectedly, and I didn't want our developers facing errors mid-conversation with their AI assistant.
I implemented a reconnection system to handle this:
async function ensureConnection(): Promise<void> {
if (!this.db) {
await this.initializeDatabase();
return;
}
try {
// Test the connection with a simple query
await this.db.query('RETURN 1');
} catch (error) {
console.error('Database connection test failed:', error);
await this.handleConnectionError();
}
}
async function handleConnectionError(): Promise<void> {
if (this.reconnectionAttempts >= MAX_RECONNECTION_ATTEMPTS) {
throw new Error(`Failed to connect after ${MAX_RECONNECTION_ATTEMPTS} attempts`);
}
this.reconnectionAttempts++;
console.error(`Attempting to reconnect (${this.reconnectionAttempts}/${MAX_RECONNECTION_ATTEMPTS})...`);
await new Promise((resolve) => setTimeout(resolve, RECONNECTION_DELAY));
await this.initializeDatabase();
}Security Considerations
Since we're essentially allowing an AI to execute database operations, security was paramount. I implemented several safeguards:
- Environmental isolation - only connecting to development databases
- Strict schema validation for all incoming requests
- Query parameter binding to prevent injection attacks
The implementation looks like this:
async function handleCallTool(request: Request) {
try {
await this.ensureConnection();
switch (request.params.name) {
case API_TOOLS.QUERY: {
const args = request.params.arguments as QueryArgs;
// Always use bind variables to prevent injection
const cursor = await this.db.query(
args.query,
args.bindVars || {}
);
const result = await cursor.all();
return {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
};
}
// Additional case handlers for other operations
}
}
catch (error) {
return {
content: [{ type: 'text', text: `Error: ${error.message}` }],
};
}
}Real-World Impact: How Our Team Uses It
The impact on our development workflow has been dramatic. Here are some real examples of how our team is using the ArangoDB MCP server:
-
Schema exploration: "Show me the structure of the customer_orders collection" - instead of diving into database tools, developers get immediate insight.
-
Data validation: "Is there any user in the database with an invalid email format?" - quick data quality checks during development.
-
Query building: "Help me write an AQL query that joins users with their orders and filters by date" - the AI can test queries in real-time and refine them based on actual results.
-
Debugging: "Show me the document with ID '12345' and explain why it might be causing this error" - context-aware debugging with actual data.
The best part? Our junior developers have particularly benefited, as they can learn AQL and database concepts through interactive examples with real data.
Current Limitations
It's important to acknowledge the current limitations of this tool:
-
No built-in query analysis - There's no way to analyze AQL complexity before execution, which could lead to performance issues with complex queries.
-
No transaction support - Each operation is handled individually rather than supporting multi-statement transactions.
-
No automatic schema inference - The AI has to discover schema through queries rather than having it pre-loaded.
-
Only suitable for development - This tool should never be connected to production databases.
Contribute or Get Started with the ArangoDB MCP Server
If you're intrigued and want to collabarate or just want to try this with your own development team, you can check it out on GitHub: ArangoDB MCP Server.
Conclusion: The Developer Experience Revolution
Building this MCP server has fundamentally changed how our team interacts with our development databases. The barrier between thinking about data and accessing it has been dramatically reduced - we're spending more time solving problems and less time context-switching between tools.
For teams using ArangoDB in their development environments, I highly recommend exploring the MCP approach. The initial investment in setting up the server pays for itself quickly through improved developer productivity and a more seamless AI-assisted workflow.
Remember: keep this tool confined to your development environment where experimentation is safe, and never connect it to production systems.