@falkordb/mcpserver 1.1.1 → 1.2.1
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 +3 -0
- package/README.md +41 -2
- package/dist/config/index.js +1 -0
- package/dist/config/index.test.js +2 -0
- package/dist/mcp/tools.js +8 -0
- package/dist/mcp/tools.test.js +172 -0
- package/package.json +10 -7
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
|
-
|
|
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:
|
package/dist/config/index.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "1.2.1",
|
|
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
|
-
"@types/node": "^
|
|
73
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
74
|
-
"@typescript-eslint/parser": "^8.
|
|
75
|
-
"eslint": "^
|
|
71
|
+
"@types/node": "^25.3.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",
|
|
@@ -84,12 +83,16 @@
|
|
|
84
83
|
"dotenv": "^17.2.1",
|
|
85
84
|
"falkordb": "^6.3.0",
|
|
86
85
|
"platformdirs": "^4.3.8-rc3",
|
|
87
|
-
"zod": "^3.
|
|
86
|
+
"zod": "^4.3.6"
|
|
88
87
|
},
|
|
89
88
|
"publishConfig": {
|
|
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"
|