@falkordb/mcpserver 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example CHANGED
@@ -10,12 +10,15 @@ MCP_TRANSPORT=stdio
10
10
  # MCP_API_KEY=
11
11
 
12
12
  # FalkorDB Configuration
13
+ # When using Docker Compose, set FALKORDB_HOST=falkordb (the service name)
13
14
  FALKORDB_HOST=localhost
14
15
  FALKORDB_PORT=6379
15
16
  FALKORDB_USERNAME=
16
17
  FALKORDB_PASSWORD=
17
18
  # Set to 'true' to use read-only queries by default (useful for replica instances)
18
19
  FALKORDB_DEFAULT_READONLY=false
20
+ # Set to 'true' to enforce strict read-only mode - blocks all write operations (queries, graph deletions, key modifications)
21
+ FALKORDB_STRICT_READONLY=false
19
22
 
20
23
  # Logging Configuration (optional)
21
24
  ENABLE_FILE_LOGGING=false
package/README.md CHANGED
@@ -79,6 +79,27 @@ This is useful for:
79
79
  - Running the server standalone without Claude Desktop
80
80
  - Custom integrations and scripting
81
81
 
82
+ ### Docker Compose
83
+
84
+ Run FalkorDB and the MCP server together:
85
+
86
+ ```bash
87
+ cp .env.example .env # create env file; edit to set MCP_API_KEY, FALKORDB_PASSWORD, etc.
88
+ docker compose up -d
89
+ ```
90
+
91
+ > **Note:** Skipping the `.env` file leaves variables like `MCP_API_KEY` and `FALKORDB_PASSWORD` empty, which disables API key authentication and uses no database password.
92
+
93
+ This starts FalkorDB with health checks and persistent volumes, plus the MCP server pre-configured to connect to it.
94
+
95
+ The MCP server runs in **HTTP transport** mode and is exposed on `localhost:3000` by default. To connect a client, configure it to use:
96
+
97
+ - **Transport:** `http`
98
+ - **URL:** `http://localhost:3000`
99
+ - **API Key:** Set via the `MCP_API_KEY` environment variable (optional)
100
+
101
+ See `docker-compose.yml` for the exact port and configuration values.
102
+
82
103
  ### Installation
83
104
 
84
105
  1. **Clone and install:**
@@ -286,7 +307,25 @@ Requests without a valid key receive a `401 Unauthorized` response. Auth is only
286
307
 
287
308
  ### Using with Docker
288
309
 
289
- Build and run the MCP server in a Docker container (defaults to HTTP transport):
310
+ **Using pre-built images from Docker Hub:**
311
+
312
+ ```bash
313
+ # Use the latest stable release
314
+ docker pull falkordb/mcpserver:latest
315
+ docker run -p 3000:3000 \
316
+ -e FALKORDB_HOST=host.docker.internal \
317
+ -e FALKORDB_PORT=6379 \
318
+ -e MCP_API_KEY=your-secret-key \
319
+ falkordb/mcpserver:latest
320
+
321
+ # Or use the edge version (latest main branch)
322
+ docker pull falkordb/mcpserver:edge
323
+
324
+ # Or pin to a specific version
325
+ docker pull falkordb/mcpserver:1.0.0
326
+ ```
327
+
328
+ **Building locally:**
290
329
 
291
330
  ```bash
292
331
  docker build -t falkordb-mcpserver .
@@ -307,7 +346,7 @@ services:
307
346
  - "6379:6379"
308
347
 
309
348
  mcp-server:
310
- build: .
349
+ image: falkordb/mcpserver:latest # or use 'build: .' to build locally
311
350
  ports:
312
351
  - "3000:3000"
313
352
  environment:
@@ -14,6 +14,7 @@ export const config = {
14
14
  username: process.env.FALKORDB_USERNAME || '',
15
15
  password: process.env.FALKORDB_PASSWORD || '',
16
16
  defaultReadOnly: process.env.FALKORDB_DEFAULT_READONLY === 'true',
17
+ strictReadOnly: process.env.FALKORDB_STRICT_READONLY === 'true',
17
18
  },
18
19
  mcp: {
19
20
  transport: (process.env.MCP_TRANSPORT || 'stdio'),
@@ -13,6 +13,8 @@ describe('Config', () => {
13
13
  expect(config.falkorDB).toHaveProperty('password');
14
14
  expect(config.falkorDB).toHaveProperty('defaultReadOnly');
15
15
  expect(typeof config.falkorDB.defaultReadOnly).toBe('boolean');
16
+ expect(config.falkorDB).toHaveProperty('strictReadOnly');
17
+ expect(typeof config.falkorDB.strictReadOnly).toBe('boolean');
16
18
  });
17
19
  test('should have MCP configuration', () => {
18
20
  expect(config).toHaveProperty('mcp');
package/dist/mcp/tools.js CHANGED
@@ -34,6 +34,10 @@ function registerQueryGraphTool(server) {
34
34
  }
35
35
  // Use the provided readOnly flag, or fall back to the default from config
36
36
  const isReadOnly = readOnly !== undefined ? readOnly : config.falkorDB.defaultReadOnly;
37
+ // Enforce strict read-only mode if enabled
38
+ if (config.falkorDB.strictReadOnly && !isReadOnly) {
39
+ throw new AppError(CommonErrors.INVALID_INPUT, 'Cannot execute write queries: server is in strict read-only mode (FALKORDB_STRICT_READONLY=true)', true);
40
+ }
37
41
  const result = await falkorDBService.executeQuery(graphName, query, undefined, isReadOnly);
38
42
  await logger.debug('Query tool executed successfully', { graphName, readOnly: isReadOnly });
39
43
  return {
@@ -113,6 +117,10 @@ function registerDeleteGraphTool(server) {
113
117
  if (!graphName?.trim()) {
114
118
  throw new AppError(CommonErrors.INVALID_INPUT, 'Graph name is required and cannot be empty', true);
115
119
  }
120
+ // Enforce strict read-only mode if enabled
121
+ if (config.falkorDB.strictReadOnly) {
122
+ throw new AppError(CommonErrors.INVALID_INPUT, 'Cannot delete graphs: server is in strict read-only mode (FALKORDB_STRICT_READONLY=true)', true);
123
+ }
116
124
  await falkorDBService.deleteGraph(graphName);
117
125
  await logger.info('Delete graph tool executed successfully', { graphName });
118
126
  return {
@@ -0,0 +1,172 @@
1
+ import { AppError, CommonErrors } from '../errors/AppError.js';
2
+ // Mock the logger service
3
+ jest.mock('../services/logger.service.js', () => ({
4
+ logger: {
5
+ info: jest.fn().mockResolvedValue(undefined),
6
+ warn: jest.fn().mockResolvedValue(undefined),
7
+ error: jest.fn().mockResolvedValue(undefined),
8
+ debug: jest.fn().mockResolvedValue(undefined),
9
+ }
10
+ }));
11
+ // Mock the FalkorDB service
12
+ jest.mock('../services/falkordb.service.js', () => ({
13
+ falkorDBService: {
14
+ executeQuery: jest.fn(),
15
+ executeReadOnlyQuery: jest.fn(),
16
+ listGraphs: jest.fn(),
17
+ deleteGraph: jest.fn(),
18
+ }
19
+ }));
20
+ // Mock config with different scenarios
21
+ let mockConfig = {
22
+ falkorDB: {
23
+ defaultReadOnly: false,
24
+ strictReadOnly: false,
25
+ }
26
+ };
27
+ jest.mock('../config/index.js', () => ({
28
+ get config() {
29
+ return mockConfig;
30
+ }
31
+ }));
32
+ // Import after mocks are set up
33
+ import registerAllTools from './tools.js';
34
+ import { falkorDBService } from '../services/falkordb.service.js';
35
+ describe('MCP Tools - Strict Read-Only Mode', () => {
36
+ let server;
37
+ let queryGraphHandler;
38
+ beforeEach(() => {
39
+ jest.clearAllMocks();
40
+ // Reset mock config
41
+ mockConfig = {
42
+ falkorDB: {
43
+ defaultReadOnly: false,
44
+ strictReadOnly: false,
45
+ }
46
+ };
47
+ // Create a minimal mock server that captures tool handlers
48
+ server = {
49
+ registerTool: jest.fn((name, schema, handler) => {
50
+ if (name === 'query_graph') {
51
+ queryGraphHandler = handler;
52
+ }
53
+ }),
54
+ };
55
+ registerAllTools(server);
56
+ });
57
+ describe('query_graph tool with strictReadOnly=false', () => {
58
+ beforeEach(() => {
59
+ mockConfig.falkorDB.strictReadOnly = false;
60
+ mockConfig.falkorDB.defaultReadOnly = false;
61
+ });
62
+ it('should allow readOnly=false when strictReadOnly is disabled', async () => {
63
+ const mockResult = { records: [] };
64
+ falkorDBService.executeQuery.mockResolvedValue(mockResult);
65
+ await queryGraphHandler({
66
+ graphName: 'test',
67
+ query: 'CREATE (n:Test) RETURN n',
68
+ readOnly: false,
69
+ });
70
+ expect(falkorDBService.executeQuery).toHaveBeenCalledWith('test', 'CREATE (n:Test) RETURN n', undefined, false);
71
+ });
72
+ it('should allow readOnly=true when strictReadOnly is disabled', async () => {
73
+ const mockResult = { records: [] };
74
+ falkorDBService.executeQuery.mockResolvedValue(mockResult);
75
+ await queryGraphHandler({
76
+ graphName: 'test',
77
+ query: 'MATCH (n) RETURN n',
78
+ readOnly: true,
79
+ });
80
+ expect(falkorDBService.executeQuery).toHaveBeenCalledWith('test', 'MATCH (n) RETURN n', undefined, true);
81
+ });
82
+ it('should use default when readOnly is not specified', async () => {
83
+ const mockResult = { records: [] };
84
+ falkorDBService.executeQuery.mockResolvedValue(mockResult);
85
+ await queryGraphHandler({
86
+ graphName: 'test',
87
+ query: 'MATCH (n) RETURN n',
88
+ });
89
+ expect(falkorDBService.executeQuery).toHaveBeenCalledWith('test', 'MATCH (n) RETURN n', undefined, false);
90
+ });
91
+ });
92
+ describe('query_graph tool with strictReadOnly=true', () => {
93
+ beforeEach(() => {
94
+ mockConfig.falkorDB.strictReadOnly = true;
95
+ mockConfig.falkorDB.defaultReadOnly = true;
96
+ });
97
+ it('should reject readOnly=false when strictReadOnly is enabled', async () => {
98
+ await expect(queryGraphHandler({
99
+ graphName: 'test',
100
+ query: 'CREATE (n:Test) RETURN n',
101
+ readOnly: false,
102
+ })).rejects.toThrow(AppError);
103
+ await expect(queryGraphHandler({
104
+ graphName: 'test',
105
+ query: 'CREATE (n:Test) RETURN n',
106
+ readOnly: false,
107
+ })).rejects.toThrow('strict read-only mode');
108
+ expect(falkorDBService.executeQuery).not.toHaveBeenCalled();
109
+ });
110
+ it('should allow readOnly=true when strictReadOnly is enabled', async () => {
111
+ const mockResult = { records: [] };
112
+ falkorDBService.executeQuery.mockResolvedValue(mockResult);
113
+ await queryGraphHandler({
114
+ graphName: 'test',
115
+ query: 'MATCH (n) RETURN n',
116
+ readOnly: true,
117
+ });
118
+ expect(falkorDBService.executeQuery).toHaveBeenCalledWith('test', 'MATCH (n) RETURN n', undefined, true);
119
+ });
120
+ it('should use defaultReadOnly when readOnly is not specified in strict mode', async () => {
121
+ const mockResult = { records: [] };
122
+ falkorDBService.executeQuery.mockResolvedValue(mockResult);
123
+ await queryGraphHandler({
124
+ graphName: 'test',
125
+ query: 'MATCH (n) RETURN n',
126
+ });
127
+ expect(falkorDBService.executeQuery).toHaveBeenCalledWith('test', 'MATCH (n) RETURN n', undefined, true);
128
+ });
129
+ it('should include proper error information when rejecting write queries', async () => {
130
+ try {
131
+ await queryGraphHandler({
132
+ graphName: 'test',
133
+ query: 'CREATE (n:Test) RETURN n',
134
+ readOnly: false,
135
+ });
136
+ fail('Expected error to be thrown');
137
+ }
138
+ catch (error) {
139
+ expect(error).toBeInstanceOf(AppError);
140
+ expect(error.name).toBe(CommonErrors.INVALID_INPUT);
141
+ expect(error.message).toContain('FALKORDB_STRICT_READONLY=true');
142
+ }
143
+ });
144
+ it('should reject write queries when strictReadOnly is enabled and defaultReadOnly=false and readOnly is not specified', async () => {
145
+ // Override defaultReadOnly for this specific test case
146
+ mockConfig.falkorDB.defaultReadOnly = false;
147
+ await expect(queryGraphHandler({
148
+ graphName: 'test',
149
+ query: 'CREATE (n:Test) RETURN n',
150
+ })).rejects.toThrow(AppError);
151
+ await expect(queryGraphHandler({
152
+ graphName: 'test',
153
+ query: 'CREATE (n:Test) RETURN n',
154
+ })).rejects.toThrow('strict read-only mode');
155
+ expect(falkorDBService.executeQuery).not.toHaveBeenCalled();
156
+ });
157
+ });
158
+ describe('query_graph tool input validation', () => {
159
+ it('should reject empty graph name', async () => {
160
+ await expect(queryGraphHandler({
161
+ graphName: '',
162
+ query: 'MATCH (n) RETURN n',
163
+ })).rejects.toThrow('Graph name is required and cannot be empty');
164
+ });
165
+ it('should reject empty query', async () => {
166
+ await expect(queryGraphHandler({
167
+ graphName: 'test',
168
+ query: '',
169
+ })).rejects.toThrow('Query is required and cannot be empty');
170
+ });
171
+ });
172
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@falkordb/mcpserver",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Model Context Protocol server for FalkorDB graph databases - enables AI assistants to query and manage graph data using natural language",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -64,15 +64,14 @@
64
64
  "test:ci": "jest --coverage --ci --watchAll=false",
65
65
  "inspect": "npm run build && npx -y @modelcontextprotocol/inspector node $PWD/dist/index.js",
66
66
  "clean": "rm -rf dist coverage",
67
- "prepublish": "npm run test:ci && npm run lint && npm run build",
68
67
  "prepublishOnly": "npm run clean && npm run test:ci && npm run lint && npm run build"
69
68
  },
70
69
  "devDependencies": {
71
70
  "@types/jest": "^30.0.0",
72
71
  "@types/node": "^20.14.0",
73
- "@typescript-eslint/eslint-plugin": "^8.38.0",
74
- "@typescript-eslint/parser": "^8.38.0",
75
- "eslint": "^9.31.0",
72
+ "@typescript-eslint/eslint-plugin": "^8.56.0",
73
+ "@typescript-eslint/parser": "^8.56.0",
74
+ "eslint": "^10.0.1",
76
75
  "jest": "^30.0.5",
77
76
  "nodemon": "^3.1.10",
78
77
  "ts-jest": "^29.4.0",
@@ -90,6 +89,10 @@
90
89
  "access": "public",
91
90
  "registry": "https://registry.npmjs.org/"
92
91
  },
92
+ "overrides": {
93
+ "minimatch": "^10.2.1",
94
+ "test-exclude": "^7.0.2"
95
+ },
93
96
  "funding": {
94
97
  "type": "github",
95
98
  "url": "https://github.com/sponsors/SecKatie"