# Directory Structure
```
├── .env.example
├── .eslintrc
├── .github
│ └── workflows
│ └── node.yml
├── .gitignore
├── Dockerfile
├── jest.config.js
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── src
│ ├── config
│ │ ├── index.test.ts
│ │ └── index.ts
│ ├── controllers
│ │ ├── mcp.controller.test.ts
│ │ └── mcp.controller.ts
│ ├── index.ts
│ ├── middleware
│ │ └── auth.middleware.ts
│ ├── models
│ │ ├── mcp-client-config.ts
│ │ └── mcp.types.ts
│ ├── routes
│ │ └── mcp.routes.ts
│ ├── services
│ │ ├── falkordb.service.test.ts
│ │ └── falkordb.service.ts
│ └── utils
│ └── connection-parser.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
```
# Server Configuration
PORT=3000
NODE_ENV=development
# FalkorDB Configuration
FALKORDB_HOST=localhost
FALKORDB_PORT=6379
FALKORDB_USERNAME=
FALKORDB_PASSWORD=
# MCP Configuration
MCP_API_KEY=your_mcp_api_key_here
```
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
```
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"@typescript-eslint/explicit-module-boundary-types": "warn",
"@typescript-eslint/no-explicit-any": "off",
"no-console": "off"
},
"env": {
"node": true,
"es6": true
}
}
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# vitepress build output
**/.vitepress/dist
# vitepress cache directory
**/.vitepress/cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# FalkorDB MCP Server
A Model Context Protocol (MCP) server for FalkorDB, allowing AI models to query and interact with graph databases.
## Overview
This project implements a server that follows the Model Context Protocol (MCP) specification to connect AI models with FalkorDB graph databases. The server translates and routes MCP requests to FalkorDB and formats the responses according to the MCP standard.
## Prerequisites
* Node.js (v16 or later)
* npm or yarn
* FalkorDB instance (can be run locally or remotely)
## Installation
1. Clone this repository:
```bash
git clone https://github.com/falkordb/falkordb-mcpserver.git
cd falkordb-mcpserver
```
2. Install dependencies:
```bash
npm install
```
3. Copy the example environment file and configure it:
```bash
cp .env.example .env
```
Edit `.env` with your configuration details.
## Configuration
Configuration is managed through environment variables in the `.env` file:
* `PORT`: Server port (default: 3000)
* `NODE_ENV`: Environment (development, production)
* `FALKORDB_HOST`: FalkorDB host (default: localhost)
* `FALKORDB_PORT`: FalkorDB port (default: 6379)
* `FALKORDB_USERNAME`: Username for FalkorDB authentication (if required)
* `FALKORDB_PASSWORD`: Password for FalkorDB authentication (if required)
* `MCP_API_KEY`: API key for authenticating MCP requests
## Usage
### Development
Start the development server with hot-reloading:
```bash
npm run dev
```
### Production
Build and start the server:
```bash
npm run build
npm start
```
## API Endpoints
* `GET /api/mcp/metadata`: Get metadata about the FalkorDB instance and available capabilities
* `POST /api/mcp/context`: Execute queries against FalkorDB
* `GET /api/mcp/health`: Check server health
* `GET /api/mcp/graphs`: Returns the list of Graphs
*
## MCP Configuration
To use this server with MCP clients, you can add it to your MCP configuration:
```json
{
"mcpServers": {
"falkordb": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-p", "3000:3000",
"--env-file", ".env",
"falkordb-mcpserver",
"falkordb://host.docker.internal:6379"
]
}
}
}
```
For client-side configuration:
```json
{
"defaultServer": "falkordb",
"servers": {
"falkordb": {
"url": "http://localhost:3000/api/mcp",
"apiKey": "your_api_key_here"
}
}
}
```
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
```
--------------------------------------------------------------------------------
/src/config/index.ts:
--------------------------------------------------------------------------------
```typescript
import dotenv from 'dotenv';
// Load environment variables from .env file
dotenv.config();
export const config = {
server: {
port: process.env.PORT || 3000,
nodeEnv: process.env.NODE_ENV || 'development',
},
falkorDB: {
host: process.env.FALKORDB_HOST || 'localhost',
port: parseInt(process.env.FALKORDB_PORT || '6379'),
username: process.env.FALKORDB_USERNAME || '',
password: process.env.FALKORDB_PASSWORD || '',
},
mcp: {
apiKey: process.env.MCP_API_KEY || '',
},
};
```
--------------------------------------------------------------------------------
/src/routes/mcp.routes.ts:
--------------------------------------------------------------------------------
```typescript
import { Router } from 'express';
import { mcpController } from '../controllers/mcp.controller';
const router = Router();
// MCP API routes
router.post('/context', mcpController.processContextRequest.bind(mcpController));
router.get('/metadata', mcpController.processMetadataRequest.bind(mcpController));
router.get('/graphs', mcpController.listGraphs.bind(mcpController));
// Additional MCP related routes could be added here
router.get('/health', (req, res) => {
res.status(200).json({ status: 'ok' });
});
export const mcpRoutes = router;
```
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
```javascript
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
testRegex: '(\\.|/)(test|spec)\\.tsx?$',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/**/*.test.ts',
'!src/**/*.spec.ts',
],
coverageDirectory: 'coverage',
testPathIgnorePatterns: ['/node_modules/', '/dist/']
};
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
# Copy package files and install dependencies
COPY package*.json ./
RUN npm ci
# Copy source code
COPY . .
# Build the application
RUN npm run build
# Remove development dependencies
RUN npm prune --production
# Production image
FROM node:18-alpine
WORKDIR /app
# Set environment variables
ENV NODE_ENV=production
# Copy built application from builder stage
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package*.json ./
# Expose the port the app runs on
EXPOSE 3000
# Run the application
CMD ["node", "dist/index.js"]
```
--------------------------------------------------------------------------------
/src/models/mcp.types.ts:
--------------------------------------------------------------------------------
```typescript
/**
* MCP Types - based on Model Context Protocol specification
*/
export interface MCPContextRequest {
graphName: string;
query: string;
params?: Record<string, any>;
context?: Record<string, any>;
options?: MCPOptions;
}
export interface MCPOptions {
timeout?: number;
maxResults?: number;
[key: string]: any;
}
export interface MCPResponse {
data: any;
metadata: MCPMetadata;
}
export interface MCPMetadata {
timestamp: string;
queryTime: number;
provider?: string;
source?: string;
[key: string]: any;
}
export interface MCPProviderMetadata {
provider: string;
version: string;
capabilities: string[];
graphTypes: string[];
queryLanguages: string[];
[key: string]: any;
}
```
--------------------------------------------------------------------------------
/src/config/index.test.ts:
--------------------------------------------------------------------------------
```typescript
import { config } from '../config';
describe('Config', () => {
test('should have server configuration', () => {
expect(config).toHaveProperty('server');
expect(config.server).toHaveProperty('port');
expect(config.server).toHaveProperty('nodeEnv');
});
test('should have FalkorDB configuration', () => {
expect(config).toHaveProperty('falkorDB');
expect(config.falkorDB).toHaveProperty('host');
expect(config.falkorDB).toHaveProperty('port');
expect(config.falkorDB).toHaveProperty('username');
expect(config.falkorDB).toHaveProperty('password');
});
test('should have MCP configuration', () => {
expect(config).toHaveProperty('mcp');
expect(config.mcp).toHaveProperty('apiKey');
});
});
```
--------------------------------------------------------------------------------
/src/middleware/auth.middleware.ts:
--------------------------------------------------------------------------------
```typescript
import { Request, Response, NextFunction } from 'express';
import { config } from '../config';
/**
* Middleware to authenticate MCP API requests
*/
export const authenticateMCP = (req: Request, res: Response, next: NextFunction): Response<any, Record<string, any>> | void => {
const apiKey = req.headers['x-api-key'] || req.query.apiKey;
// Skip authentication for development environment
if (config.server.nodeEnv === 'development' && !config.mcp.apiKey) {
console.warn('Warning: Running without API key authentication in development mode');
return next();
}
if (!apiKey) {
return res.status(401).json({ error: 'Missing API key' });
}
if (apiKey !== config.mcp.apiKey) {
return res.status(403).json({ error: 'Invalid API key' });
}
next();
};
```
--------------------------------------------------------------------------------
/src/models/mcp-client-config.ts:
--------------------------------------------------------------------------------
```typescript
/**
* MCP Client Configuration Types
*/
export interface MCPServerConfig {
mcpServers: {
[key: string]: {
command: string;
args: string[];
};
};
}
export interface MCPClientConfig {
defaultServer?: string;
servers: {
[key: string]: {
url: string;
apiKey?: string;
};
};
}
/**
* Sample MCP Client Configuration
*/
export const sampleMCPClientConfig: MCPClientConfig = {
defaultServer: "falkordb",
servers: {
"falkordb": {
url: "http://localhost:3000/api/mcp",
apiKey: "your_api_key_here"
}
}
};
/**
* Sample MCP Server Configuration
*/
export const sampleMCPServerConfig: MCPServerConfig = {
mcpServers: {
"falkordb": {
command: "docker",
args: [
"run",
"-i",
"--rm",
"-p", "3000:3000",
"--env-file", ".env",
"falkordb-mcpserver",
"falkordb://host.docker.internal:6379"
]
}
}
};
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "falkordb-mcpserver",
"version": "1.0.0",
"description": "Model Context Protocol server for FalkorDB",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/index.ts",
"lint": "eslint . --ext .ts",
"test": "jest",
"test:coverage": "jest --coverage",
"docker:build": "docker build -t falkordb-mcpserver .",
"docker:run": "docker run -p 3000:3000 falkordb-mcpserver"
},
"keywords": [
"mcp",
"modelcontextprotocol",
"falkordb",
"graph",
"database"
],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^20.11.0",
"@typescript-eslint/eslint-plugin": "^6.15.0",
"@typescript-eslint/parser": "^6.15.0",
"eslint": "^8.56.0",
"jest": "^29.7.0",
"@types/jest": "^29.5.11",
"ts-jest": "^29.1.1",
"nodemon": "^3.0.2",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.8.0",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"falkordb": "^6.2.07"
}
}
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
import express from 'express';
import { config } from './config';
import { mcpRoutes } from './routes/mcp.routes';
import { authenticateMCP } from './middleware/auth.middleware';
import { falkorDBService } from './services/falkordb.service';
// Initialize Express app
const app = express();
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Apply authentication to MCP routes
app.use('/api/mcp', authenticateMCP, mcpRoutes);
// Basic routes
app.get('/', (req, res) => {
res.json({
name: 'FalkorDB MCP Server',
version: '1.0.0',
status: 'running',
});
});
// Start server
const PORT = config.server.port;
app.listen(PORT, () => {
console.log(`FalkorDB MCP Server listening on port ${PORT}`);
console.log(`Environment: ${config.server.nodeEnv}`);
});
// Handle graceful shutdown
process.on('SIGTERM', async () => {
console.log('SIGTERM received. Shutting down gracefully...');
// Close database connections
await falkorDBService.close();
process.exit(0);
});
process.on('SIGINT', async () => {
console.log('SIGINT received. Shutting down gracefully...');
// Close database connections
await falkorDBService.close();
process.exit(0);
});
export default app;
```
--------------------------------------------------------------------------------
/.github/workflows/node.yml:
--------------------------------------------------------------------------------
```yaml
name: Node.js CI/CD
on:
push:
branches: [ main, master, develop ]
pull_request:
branches: [ main, master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x, 18.x, 20.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build --if-present
- name: Lint
run: npm run lint --if-present
- name: Test
run: npm test --if-present
env:
CI: true
docker:
needs: build
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v4
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=sha,format=long
type=ref,event=branch
type=ref,event=tag
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
```
--------------------------------------------------------------------------------
/src/services/falkordb.service.ts:
--------------------------------------------------------------------------------
```typescript
import { FalkorDB } from 'falkordb';
import { config } from '../config';
class FalkorDBService {
private client: FalkorDB | null = null;
constructor() {
this.init();
}
private async init() {
try {
this.client = await FalkorDB.connect({
socket: {
host: config.falkorDB.host,
port: config.falkorDB.port,
},
password: config.falkorDB.password,
username: config.falkorDB.username,
});
// Test connection
const connection = await this.client.connection;
await connection.ping();
console.log('Successfully connected to FalkorDB');
} catch (error) {
console.error('Failed to connect to FalkorDB:', error);
// Retry connection after a delay
setTimeout(() => this.init(), 5000);
}
}
async executeQuery(graphName: string, query: string, params?: Record<string, any>): Promise<any> {
if (!this.client) {
throw new Error('FalkorDB client not initialized');
}
try {
const graph = this.client.selectGraph(graphName);
const result = await graph.query(query, params);
return result;
} catch (error) {
const sanitizedGraphName = graphName.replace(/\n|\r/g, "");
console.error('Error executing FalkorDB query on graph %s:', sanitizedGraphName, error);
throw error;
}
}
/**
* Lists all available graphs in FalkorDB
* @returns Array of graph names
*/
async listGraphs(): Promise<string[]> {
if (!this.client) {
throw new Error('FalkorDB client not initialized');
}
try {
// Using the simplified list method which always returns an array
return await this.client.list();
} catch (error) {
console.error('Error listing FalkorDB graphs:', error);
throw error;
}
}
async close() {
if (this.client) {
await this.client.close();
this.client = null;
}
}
}
// Export a singleton instance
export const falkorDBService = new FalkorDBService();
```
--------------------------------------------------------------------------------
/src/utils/connection-parser.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Utility to parse FalkorDB connection strings
*/
interface FalkorDBConnectionOptions {
host: string;
port: number;
username?: string;
password?: string;
}
/**
* Parse a FalkorDB connection string
* Format: falkordb://[username:password@]host:port
*
* @param connectionString The connection string to parse
* @returns Parsed connection options
*/
export function parseFalkorDBConnectionString(connectionString: string): FalkorDBConnectionOptions {
try {
// Default values
const defaultOptions: FalkorDBConnectionOptions = {
host: 'localhost',
port: 6379
};
// Handle empty or undefined input
if (!connectionString) {
return defaultOptions;
}
// Remove protocol prefix if present
let cleanString = connectionString;
if (cleanString.startsWith('falkordb://')) {
cleanString = cleanString.substring('falkordb://'.length);
}
// Parse authentication if present
let auth = '';
let hostPort = cleanString;
if (cleanString.includes('@')) {
const parts = cleanString.split('@');
auth = parts[0];
hostPort = parts[1];
}
// Parse host and port
let host = 'localhost';
let port = 6379;
if (hostPort.includes(':')) {
const parts = hostPort.split(':');
host = parts[0] || 'localhost';
port = parseInt(parts[1], 10) || 6379;
} else {
host = hostPort || 'localhost';
}
// Parse username and password
let username = undefined;
let password = undefined;
if (auth && auth.includes(':')) {
const parts = auth.split(':');
username = parts[0] || undefined;
password = parts[1] || undefined;
} else if (auth) {
password = auth;
}
return {
host,
port,
username,
password
};
} catch (error) {
console.error('Error parsing connection string:', error);
return {
host: 'localhost',
port: 6379
};
}
}
```
--------------------------------------------------------------------------------
/src/controllers/mcp.controller.ts:
--------------------------------------------------------------------------------
```typescript
import { Request, Response } from 'express';
import { falkorDBService } from '../services/falkordb.service';
import {
MCPContextRequest,
MCPResponse,
MCPProviderMetadata
} from '../models/mcp.types';
export class MCPController {
/**
* Process MCP context requests
*/
async processContextRequest(req: Request, res: Response): Promise<Response<any, Record<string, any>>> {
try {
const contextRequest: MCPContextRequest = req.body;
if (!contextRequest.query) {
return res.status(400).json({ error: 'Query is required' });
}
// Graph name is always required from the client
if (!contextRequest.graphName) {
return res.status(400).json({ error: 'Graph name is required' });
}
const startTime = Date.now();
// Execute the query on FalkorDB
const result = await falkorDBService.executeQuery(
contextRequest.graphName,
contextRequest.query,
contextRequest.params
);
const queryTime = Date.now() - startTime;
// Format the result according to MCP standards
const formattedResult: MCPResponse = {
data: result,
metadata: {
timestamp: new Date().toISOString(),
queryTime,
provider: 'FalkorDB MCP Server',
source: 'falkordb'
}
};
return res.status(200).json(formattedResult);
} catch (error: any) {
console.error('Error processing MCP context request:', error);
return res.status(500).json({
error: error.message,
metadata: {
timestamp: new Date().toISOString()
}
});
}
}
/**
* Process MCP metadata requests
*/
async processMetadataRequest(req: Request, res: Response): Promise<Response<any, Record<string, any>>> {
try {
// Return metadata about available graphs or capabilities
const metadata: MCPProviderMetadata = {
provider: 'FalkorDB MCP Server',
version: '1.0.0',
capabilities: [
'graph.query',
'graph.list',
'node.properties',
'relationship.properties'
],
graphTypes: ['property', 'directed'],
queryLanguages: ['cypher'],
};
return res.status(200).json(metadata);
} catch (error: any) {
console.error('Error processing MCP metadata request:', error);
return res.status(500).json({ error: error.message });
}
}
/**
* List available graphs in FalkorDB
*/
async listGraphs(req: Request, res: Response): Promise<Response<any, Record<string, any>>> {
try {
const graphNames = await falkorDBService.listGraphs();
// Format the graph list into a more structured response
const graphs = graphNames.map(name => ({
name,
// We don't have additional metadata from just the graph list
// If needed, additional queries could be made for each graph
// to fetch more detailed information
}));
return res.status(200).json({
data: graphs,
metadata: {
timestamp: new Date().toISOString(),
count: graphs.length
}
});
} catch (error: any) {
console.error('Error listing graphs:', error);
return res.status(500).json({ error: error.message });
}
}
}
export const mcpController = new MCPController();
```
--------------------------------------------------------------------------------
/src/controllers/mcp.controller.test.ts:
--------------------------------------------------------------------------------
```typescript
import { Request, Response } from 'express';
import { mcpController } from './mcp.controller';
import { falkorDBService } from '../services/falkordb.service';
// Mock the falkorDBService
jest.mock('../services/falkordb.service', () => ({
falkorDBService: {
executeQuery: jest.fn(),
listGraphs: jest.fn()
}
}));
describe('MCP Controller', () => {
let mockRequest: Partial<Request>;
let mockResponse: Partial<Response>;
let mockJson: jest.Mock;
let mockStatus: jest.Mock;
beforeEach(() => {
// Reset mocks
jest.clearAllMocks();
// Set up response mock
mockJson = jest.fn().mockReturnValue({});
mockStatus = jest.fn().mockReturnThis();
mockResponse = {
json: mockJson,
status: mockStatus
};
});
describe('processContextRequest', () => {
test('should return 400 if query is missing', async () => {
// Arrange
mockRequest = {
body: {
graphName: 'testGraph'
}
};
// Act
await mcpController.processContextRequest(
mockRequest as Request,
mockResponse as Response
);
// Assert
expect(mockStatus).toHaveBeenCalledWith(400);
expect(mockJson).toHaveBeenCalledWith({ error: 'Query is required' });
});
test('should return 400 if graphName is missing', async () => {
// Arrange
mockRequest = {
body: {
query: 'MATCH (n) RETURN n'
}
};
// Act
await mcpController.processContextRequest(
mockRequest as Request,
mockResponse as Response
);
// Assert
expect(mockStatus).toHaveBeenCalledWith(400);
expect(mockJson).toHaveBeenCalledWith({ error: 'Graph name is required' });
});
test('should execute query and return results', async () => {
// Arrange
const mockQueryResult = { records: [{ id: 1, name: 'test' }] };
(falkorDBService.executeQuery as jest.Mock).mockResolvedValue(mockQueryResult);
mockRequest = {
body: {
graphName: 'testGraph',
query: 'MATCH (n) RETURN n',
params: { param1: 'value1' }
}
};
// Act
await mcpController.processContextRequest(
mockRequest as Request,
mockResponse as Response
);
// Assert
expect(falkorDBService.executeQuery).toHaveBeenCalledWith(
'testGraph',
'MATCH (n) RETURN n',
{ param1: 'value1' }
);
expect(mockStatus).toHaveBeenCalledWith(200);
expect(mockJson).toHaveBeenCalledWith(expect.objectContaining({
data: mockQueryResult,
metadata: expect.any(Object)
}));
});
});
describe('listGraphs', () => {
test('should return list of graphs', async () => {
// Arrange
const mockGraphs = ['graph1', 'graph2'];
(falkorDBService.listGraphs as jest.Mock).mockResolvedValue(mockGraphs);
mockRequest = {};
// Act
await mcpController.listGraphs(
mockRequest as Request,
mockResponse as Response
);
// Assert
expect(falkorDBService.listGraphs).toHaveBeenCalled();
expect(mockStatus).toHaveBeenCalledWith(200);
expect(mockJson).toHaveBeenCalledWith(expect.objectContaining({
data: expect.arrayContaining([
expect.objectContaining({ name: 'graph1' }),
expect.objectContaining({ name: 'graph2' })
]),
metadata: expect.objectContaining({
count: 2
})
}));
});
});
});
```
--------------------------------------------------------------------------------
/src/services/falkordb.service.test.ts:
--------------------------------------------------------------------------------
```typescript
import { falkorDBService } from './falkordb.service';
// Mock the FalkorDB library
jest.mock('falkordb', () => {
const mockSelectGraph = jest.fn();
const mockQuery = jest.fn();
const mockList = jest.fn();
const mockClose = jest.fn();
const mockPing = jest.fn();
return {
FalkorDB: {
connect: jest.fn().mockResolvedValue({
connection: Promise.resolve({
ping: mockPing
}),
selectGraph: mockSelectGraph.mockReturnValue({
query: mockQuery
}),
list: mockList,
close: mockClose
})
},
mockSelectGraph,
mockQuery,
mockList,
mockClose,
mockPing
};
});
describe('FalkorDB Service', () => {
let mockFalkorDB: any;
beforeAll(() => {
// Access the mocks
mockFalkorDB = require('falkordb');
});
beforeEach(() => {
jest.clearAllMocks();
});
describe('executeQuery', () => {
it('should execute a query on the specified graph', async () => {
// Arrange
const graphName = 'testGraph';
const query = 'MATCH (n) RETURN n';
const params = { param1: 'value1' };
const expectedResult = { records: [{ id: 1 }] };
mockFalkorDB.mockQuery.mockResolvedValue(expectedResult);
// Ensure service is initialized (private method, so we need to call a method first)
await falkorDBService.executeQuery(graphName, query, params).catch(() => {});
// Reset mocks after initialization
jest.clearAllMocks();
// Force client to be available
(falkorDBService as any).client = {
selectGraph: mockFalkorDB.mockSelectGraph
};
// Act
const result = await falkorDBService.executeQuery(graphName, query, params);
// Assert
expect(mockFalkorDB.mockSelectGraph).toHaveBeenCalledWith(graphName);
expect(mockFalkorDB.mockQuery).toHaveBeenCalledWith(query, params);
expect(result).toEqual(expectedResult);
});
it('should throw an error if client is not initialized', async () => {
// Arrange
(falkorDBService as any).client = null;
// Act & Assert
await expect(falkorDBService.executeQuery('graph', 'query'))
.rejects
.toThrow('FalkorDB client not initialized');
});
});
describe('listGraphs', () => {
it('should return a list of graphs', async () => {
// Arrange
const expectedGraphs = ['graph1', 'graph2'];
mockFalkorDB.mockList.mockResolvedValue(expectedGraphs);
// Force client to be available
(falkorDBService as any).client = {
list: mockFalkorDB.mockList
};
// Act
const result = await falkorDBService.listGraphs();
// Assert
expect(mockFalkorDB.mockList).toHaveBeenCalled();
expect(result).toEqual(expectedGraphs);
});
it('should throw an error if client is not initialized', async () => {
// Arrange
(falkorDBService as any).client = null;
// Act & Assert
await expect(falkorDBService.listGraphs())
.rejects
.toThrow('FalkorDB client not initialized');
});
});
describe('close', () => {
it('should close the client connection', async () => {
// Arrange
(falkorDBService as any).client = {
close: mockFalkorDB.mockClose
};
// Act
await falkorDBService.close();
// Assert
expect(mockFalkorDB.mockClose).toHaveBeenCalled();
expect((falkorDBService as any).client).toBeNull();
});
it('should not throw if client is already null', async () => {
// Arrange
(falkorDBService as any).client = null;
// Act & Assert
await expect(falkorDBService.close()).resolves.not.toThrow();
});
});
});
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "libReplacement": true, /* Enable lib replacement. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
"rootDir": "./src", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
"baseUrl": ".", /* Specify the base directory to resolve non-relative module names. */
"paths": {
"@/*": ["src/*"]
}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
"resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "./dist", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
// "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
```