Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
2db6776
Update dependencies, refactor server implementation, and add Redis se…
SecKatie Jul 25, 2025
7633849
Streamline to MCP-only architecture and enhance documentation
SecKatie Jul 27, 2025
96d702c
Fix module resolution and logging issues for ES modules
SecKatie Jul 28, 2025
32ab939
Enhance configuration, documentation, and CI/CD workflow
SecKatie Jul 28, 2025
02bce89
Add environment configuration for release workflow in GitHub Actions
SecKatie Jul 28, 2025
4f32b8c
Remove broken snyk test
SecKatie Jul 28, 2025
80267dc
ci: fix broken publish rule
SecKatie Jul 28, 2025
60778b4
chore: update version and enhance README with npm usage instructions
SecKatie Jul 28, 2025
9a3b4c2
Update src/mcp/prompts.ts
SecKatie Jul 28, 2025
98d560f
Merge pull request #1 from SecKatie/develop
SecKatie Jul 28, 2025
5206d09
feat: add read-only query support for replica instances
singhtanmay6735 Jan 28, 2026
ea60278
Merge pull request #2 from singhtanmay6735/feature/read-only-support
SecKatie Feb 2, 2026
1f99d35
Merge branch 'main' into rebase
gkorland Feb 8, 2026
feb4908
update
gkorland Feb 8, 2026
5a7ee62
Apply suggestion from @Copilot
gkorland Feb 8, 2026
6afbf5d
Potential fix for code scanning alert no. 10: Workflow does not conta…
gkorland Feb 8, 2026
387a664
Initial plan
Claude Feb 8, 2026
8616f19
Address PR review comments: fix concurrent init, improve error handli…
Claude Feb 8, 2026
b803fcf
Merge pull request #15 from FalkorDB/claude/sub-pr-14
gkorland Feb 8, 2026
5d7f215
Update .github/workflows/node.yml
gkorland Feb 8, 2026
acfbfb6
Update .env.example
gkorland Feb 8, 2026
4e02729
Update CONTRIBUTING.md
gkorland Feb 8, 2026
22f6095
Update CLAUDE.md
gkorland Feb 8, 2026
42473fc
Initial plan
Claude Feb 9, 2026
8b5185a
Address PR review comments: sanitize query logging, fix MCP level map…
Claude Feb 9, 2026
a2e403a
Merge pull request #16 from FalkorDB/claude/sub-pr-14
gkorland Feb 9, 2026
f7f154d
Update node.yml
gkorland Feb 12, 2026
f924c66
Update falkordb.service.ts
gkorland Feb 12, 2026
7d837bf
Initialize retryCount before initializing promise
gkorland Feb 12, 2026
bf0bbe3
update lock
gkorland Feb 15, 2026
7b95314
fix: suppress TS2589 deep type instantiation error in registerTool
gkorland Feb 23, 2026
a1f4111
fix: address security and reliability issues from code review
gkorland Feb 23, 2026
5d4d539
fix: improve FalkorDB service reliability and logging
gkorland Feb 23, 2026
80e1204
fix: address PR review findings from copilot reviewer
gkorland Feb 23, 2026
f50c816
feat: add Streamable HTTP transport support
gkorland Feb 23, 2026
09f4691
fix: improve Redis service reliability and fix review findings
gkorland Feb 23, 2026
beef955
docs: add HTTP transport testing instructions to README
gkorland Feb 23, 2026
1c674df
docs: add API key authentication usage to README
gkorland Feb 23, 2026
d238e32
feat: add Docker support for HTTP transport
gkorland Feb 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ FALKORDB_HOST=localhost
FALKORDB_PORT=6379
FALKORDB_USERNAME=
FALKORDB_PASSWORD=
# Set to 'true' to use read-only queries by default (useful for replica instances)
FALKORDB_DEFAULT_READONLY=false

# Redis Configuration (for key-value operations)
REDIS_URL=redis://localhost:6379
Expand Down
36 changes: 32 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ FalkorDB MCP Server enables AI assistants like Claude to interact with FalkorDB
## 🎯 What is this?

This server implements the [Model Context Protocol (MCP)](https://modelcontextprotocol.io), allowing AI models to:
- **Query graph databases** using OpenCypher
- **Query graph databases** using OpenCypher (with read-only mode support)
- **Create and manage** nodes and relationships
- **Store and retrieve** key-value data
- **List and explore** multiple graphs
- **Delete graphs** when needed
- **Read-only queries** for replica instances or to prevent accidental writes

## 🚀 Quick Start

Expand Down Expand Up @@ -72,6 +73,7 @@ Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_
FALKORDB_PORT=6379
FALKORDB_USERNAME= # Optional
FALKORDB_PASSWORD= # Optional
FALKORDB_DEFAULT_READONLY=false # Set to 'true' for read-only mode (useful for replicas)

# Redis Configuration (for key-value operations)
REDIS_URL=redis://localhost:6379
Expand Down Expand Up @@ -115,8 +117,16 @@ Once connected, you can ask Claude to:
"Show me all people who know each other"
"Find the shortest path between two nodes"
"What relationships does John have?"
"Run a read-only query on the replica instance"
```

**Note:** The `query_graph` tool now supports a `readOnly` parameter to execute queries in read-only mode using `GRAPH.RO_QUERY`. This is ideal for:
- Running queries on replica instances
- Preventing accidental write operations
- Ensuring data integrity in production environments

There's also a dedicated `query_graph_readonly` tool that always executes queries in read-only mode.

### 📝 Manage Data
```
"Create a new person named Alice who knows Bob"
Expand Down Expand Up @@ -211,6 +221,22 @@ FALKORDB_USERNAME=your-username
FALKORDB_PASSWORD=your-secure-password
```

### Read-Only Mode for Replica Instances

If you're connecting to a FalkorDB replica instance or want to ensure no write operations are performed, you can enable read-only mode by default:

```env
FALKORDB_DEFAULT_READONLY=true
```

This will make all queries execute using `GRAPH.RO_QUERY` by default. You can still override this per-query by setting the `readOnly` parameter in the `query_graph` tool.

**Use cases:**
- **Replica instances**: Prevent writes to read replicas in replication setups
- **Production safety**: Ensure critical data isn't accidentally modified
- **Reporting/analytics**: Run queries for dashboards without risk of data changes
- **Multi-tenant environments**: Provide read-only access to certain users

### Running Multiple Instances

You can run multiple MCP servers for different FalkorDB instances:
Expand All @@ -222,14 +248,16 @@ You can run multiple MCP servers for different FalkorDB instances:
"command": "node",
"args": ["path/to/server/dist/index.js"],
"env": {
"FALKORDB_HOST": "dev.falkordb.local"
"FALKORDB_HOST": "dev.falkordb.local",
"FALKORDB_DEFAULT_READONLY": "false"
}
},
"falkordb-prod": {
"falkordb-prod-replica": {
"command": "node",
"args": ["path/to/server/dist/index.js"],
"env": {
"FALKORDB_HOST": "prod.falkordb.com"
"FALKORDB_HOST": "replica.falkordb.com",
"FALKORDB_DEFAULT_READONLY": "true"
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/config/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ describe('Config', () => {
expect(config.falkorDB).toHaveProperty('port');
expect(config.falkorDB).toHaveProperty('username');
expect(config.falkorDB).toHaveProperty('password');
expect(config.falkorDB).toHaveProperty('defaultReadOnly');
expect(typeof config.falkorDB.defaultReadOnly).toBe('boolean');
});

test('should have MCP configuration', () => {
Expand Down
1 change: 1 addition & 0 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const config = {
port: parseInt(process.env.FALKORDB_PORT || '6379'),
username: process.env.FALKORDB_USERNAME || '',
password: process.env.FALKORDB_PASSWORD || '',
defaultReadOnly: process.env.FALKORDB_DEFAULT_READONLY === 'true',
},
redis: {
url: process.env.REDIS_URL || 'redis://localhost:6379',
Expand Down
62 changes: 57 additions & 5 deletions src/mcp/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,21 @@ import { falkorDBService } from '../services/falkordb.service.js';
import { redisService } from '../services/redis.service.js';
import { logger } from '../services/logger.service.js';
import { AppError, CommonErrors } from '../errors/AppError.js';
import { config } from '../config/index.js';

function registerQueryGraphTool(server: McpServer): void {
server.registerTool(
"query_graph",
{
title: "Query Graph",
description: "Run a OpenCypher query on a graph",
description: "Run a OpenCypher query on a graph. Supports both read-write and read-only queries.",
Comment thread
gkorland marked this conversation as resolved.
Outdated
inputSchema: {
graphName: z.string().describe("The name of the graph to query"),
query: z.string().describe("The OpenCypher query to run"),
readOnly: z.boolean().optional().describe("If true, executes as a read-only query (GRAPH.RO_QUERY). Useful for replica instances or to prevent accidental writes. Defaults to FALKORDB_DEFAULT_READONLY environment variable."),
},
},
async ({graphName, query}) => {
async ({graphName, query, readOnly}) => {
try {
if (!graphName?.trim()) {
throw new AppError(
Expand All @@ -34,8 +36,11 @@ function registerQueryGraphTool(server: McpServer): void {
);
}

const result = await falkorDBService.executeQuery(graphName, query);
await logger.debug('Query tool executed successfully', { graphName });
// Use the provided readOnly flag, or fall back to the default from config
const isReadOnly = readOnly !== undefined ? readOnly : config.falkorDB.defaultReadOnly;

const result = await falkorDBService.executeQuery(graphName, query, undefined, isReadOnly);
await logger.debug('Query tool executed successfully', { graphName, readOnly: isReadOnly });

return {
content: [{
Expand All @@ -51,6 +56,52 @@ function registerQueryGraphTool(server: McpServer): void {
)
}

function registerQueryGraphReadOnlyTool(server: McpServer): void {
server.registerTool(
"query_graph_readonly",
{
title: "Query Graph (Read-Only)",
description: "Run a read-only OpenCypher query on a graph using GRAPH.RO_QUERY. This ensures no write operations are performed and is ideal for replica instances.",
inputSchema: {
graphName: z.string().describe("The name of the graph to query"),
query: z.string().describe("The read-only OpenCypher query to run (write operations will fail)"),
},
},
async ({graphName, query}) => {
try {
if (!graphName?.trim()) {
throw new AppError(
CommonErrors.INVALID_INPUT,
'Graph name is required and cannot be empty',
true
);
}

if (!query?.trim()) {
throw new AppError(
CommonErrors.INVALID_INPUT,
'Query is required and cannot be empty',
true
);
}

const result = await falkorDBService.executeReadOnlyQuery(graphName, query);
await logger.debug('Read-only query tool executed successfully', { graphName });

return {
content: [{
type: "text",
text: JSON.stringify(result, null, 2)
}]
};
} catch (error) {
await logger.error('Read-only query tool execution failed', error instanceof Error ? error : new Error(String(error)), { graphName, query });
throw error;
}
}
)
}

function registerListGraphsTool(server: McpServer): void {
// Register list_graphs tool
server.registerTool(
Expand Down Expand Up @@ -267,8 +318,9 @@ function registerDeleteKeyTool(server: McpServer): void {
}

export default function registerAllTools(server: McpServer): void {
// Register query_graph tool
// Register query_graph tools
registerQueryGraphTool(server);
registerQueryGraphReadOnlyTool(server);
registerListGraphsTool(server);
registerDeleteGraphTool(server);
registerSetKeyTool(server);
Expand Down
117 changes: 116 additions & 1 deletion src/services/falkordb.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ jest.mock('../config/index.js', () => ({
host: 'localhost',
port: 6379,
username: 'testuser',
password: 'testpass'
password: 'testpass',
defaultReadOnly: false
}
}
}));
Expand All @@ -27,6 +28,7 @@ jest.mock('../config/index.js', () => ({
jest.mock('falkordb', () => {
const mockSelectGraph = jest.fn();
const mockQuery = jest.fn();
const mockRoQuery = jest.fn();
const mockList = jest.fn();
const mockClose = jest.fn();
const mockPing = jest.fn();
Expand All @@ -40,6 +42,7 @@ jest.mock('falkordb', () => {
}),
selectGraph: mockSelectGraph.mockReturnValue({
query: mockQuery,
roQuery: mockRoQuery,
delete: mockDelete
}),
list: mockList,
Expand All @@ -48,6 +51,7 @@ jest.mock('falkordb', () => {
},
mockSelectGraph,
mockQuery,
mockRoQuery,
mockList,
mockClose,
mockPing,
Expand Down Expand Up @@ -218,6 +222,7 @@ describe('FalkorDB Service', () => {
// Assert
expect(mockFalkorDB.mockSelectGraph).toHaveBeenCalledWith(graphName);
expect(mockFalkorDB.mockQuery).toHaveBeenCalledWith(query, params);
expect(mockFalkorDB.mockRoQuery).not.toHaveBeenCalled();
expect(result).toEqual(expectedResult);
});

Expand All @@ -242,6 +247,53 @@ describe('FalkorDB Service', () => {
expect(mockFalkorDB.mockQuery).toHaveBeenCalledWith(query, undefined);
expect(result).toEqual(expectedResult);
});

it('should execute a read-only query when readOnly flag is true', async () => {
// Arrange
const graphName = 'testGraph';
const query = 'MATCH (n) RETURN n';
const params = { param1: 'value1' };
const expectedResult = { records: [{ id: 1 }] };

mockFalkorDB.mockRoQuery.mockResolvedValue(expectedResult);

// Force client to be available
(falkorDBService as any).client = {
selectGraph: mockFalkorDB.mockSelectGraph
};

// Act
const result = await falkorDBService.executeQuery(graphName, query, params, true);

// Assert
expect(mockFalkorDB.mockSelectGraph).toHaveBeenCalledWith(graphName);
expect(mockFalkorDB.mockRoQuery).toHaveBeenCalledWith(query, params);
expect(mockFalkorDB.mockQuery).not.toHaveBeenCalled();
expect(result).toEqual(expectedResult);
});

it('should execute a read-only query without params', async () => {
// Arrange
const graphName = 'testGraph';
const query = 'MATCH (n) RETURN n';
const expectedResult = { records: [{ id: 1 }] };

mockFalkorDB.mockRoQuery.mockResolvedValue(expectedResult);

// Force client to be available
(falkorDBService as any).client = {
selectGraph: mockFalkorDB.mockSelectGraph
};

// Act
const result = await falkorDBService.executeQuery(graphName, query, undefined, true);

// Assert
expect(mockFalkorDB.mockSelectGraph).toHaveBeenCalledWith(graphName);
expect(mockFalkorDB.mockRoQuery).toHaveBeenCalledWith(query, undefined);
expect(mockFalkorDB.mockQuery).not.toHaveBeenCalled();
expect(result).toEqual(expectedResult);
});

it('should throw AppError if client is not initialized', async () => {
// Arrange
Expand Down Expand Up @@ -278,6 +330,69 @@ describe('FalkorDB Service', () => {
expect((error as AppError).name).toBe(CommonErrors.OPERATION_FAILED);
}
});

it('should throw AppError when read-only query execution fails', async () => {
// Arrange
const graphName = 'testGraph';
const query = 'CREATE (n) RETURN n';
const queryError = new Error('Write operations not allowed in read-only mode');

mockFalkorDB.mockRoQuery.mockRejectedValue(queryError);

// Force client to be available
(falkorDBService as any).client = {
selectGraph: mockFalkorDB.mockSelectGraph
};

// Act & Assert
try {
await falkorDBService.executeQuery(graphName, query, undefined, true);
fail('Expected executeQuery to throw AppError');
} catch (error) {
expect(error).toBeInstanceOf(AppError);
expect((error as AppError).name).toBe(CommonErrors.OPERATION_FAILED);
expect((error as AppError).message).toContain('read-only');
}
});
});

describe('executeReadOnlyQuery', () => {
it('should execute a read-only query using ro_query', async () => {
// Arrange
const graphName = 'testGraph';
const query = 'MATCH (n) RETURN n';
const params = { param1: 'value1' };
const expectedResult = { records: [{ id: 1 }] };

mockFalkorDB.mockRoQuery.mockResolvedValue(expectedResult);

// Force client to be available
(falkorDBService as any).client = {
selectGraph: mockFalkorDB.mockSelectGraph
};

// Act
const result = await falkorDBService.executeReadOnlyQuery(graphName, query, params);

// Assert
expect(mockFalkorDB.mockSelectGraph).toHaveBeenCalledWith(graphName);
expect(mockFalkorDB.mockRoQuery).toHaveBeenCalledWith(query, params);
expect(mockFalkorDB.mockQuery).not.toHaveBeenCalled();
expect(result).toEqual(expectedResult);
});

it('should throw AppError if client is not initialized', async () => {
// Arrange
(falkorDBService as any).client = null;

// Act & Assert
await expect(falkorDBService.executeReadOnlyQuery('graph', 'query'))
.rejects
.toThrow(AppError);
await expect(falkorDBService.executeReadOnlyQuery('graph', 'query'))
.rejects
.toThrow('FalkorDB client not initialized');
});
});

describe('listGraphs', () => {
Expand Down
Loading
Loading