@falkordb/mcpserver 1.0.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.
@@ -0,0 +1,258 @@
1
+ import { z } from 'zod';
2
+ import { falkorDBService } from '../services/falkordb.service.js';
3
+ import { redisService } from '../services/redis.service.js';
4
+ import { logger } from '../services/logger.service.js';
5
+ import { AppError, CommonErrors } from '../errors/AppError.js';
6
+ import { config } from '../config/index.js';
7
+ // Define schemas as simple objects first to avoid TS2589 deep recursion
8
+ const queryGraphSchema = {
9
+ graphName: z.string().describe("The name of the graph to query"),
10
+ query: z.string().describe("The OpenCypher query to run"),
11
+ 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."),
12
+ };
13
+ const queryGraphReadOnlySchema = {
14
+ graphName: z.string().describe("The name of the graph to query"),
15
+ query: z.string().describe("The read-only OpenCypher query to run (write operations will fail)"),
16
+ };
17
+ const deleteGraphSchema = {
18
+ graphName: z.string().describe("The name of the graph to delete"),
19
+ confirmDelete: z.literal(true).describe("Must be set to true to confirm deletion. This is a safety measure to prevent accidental data loss."),
20
+ };
21
+ const setKeySchema = {
22
+ key: z.string().describe("The key to set"),
23
+ value: z.string().describe("The value to set"),
24
+ };
25
+ const getKeySchema = {
26
+ key: z.string().describe("The key to get."),
27
+ };
28
+ const deleteKeySchema = {
29
+ key: z.string().describe("The key to delete"),
30
+ confirmDelete: z.literal(true).describe("Must be set to true to confirm deletion. This is a safety measure to prevent accidental data loss."),
31
+ };
32
+ function registerQueryGraphTool(server) {
33
+ server.registerTool("query_graph", {
34
+ title: "Query Graph",
35
+ description: "Run an OpenCypher query on a graph. Supports both read-write and read-only queries.",
36
+ inputSchema: queryGraphSchema, // Cast to any to prevent TS2589 (deep recursion) during type inference
37
+ }, async (args) => {
38
+ // Manual validation since we're using raw shape for registration
39
+ const { graphName, query, readOnly } = z.object(queryGraphSchema).parse(args);
40
+ try {
41
+ if (!graphName?.trim()) {
42
+ throw new AppError(CommonErrors.INVALID_INPUT, 'Graph name is required and cannot be empty', true);
43
+ }
44
+ if (!query?.trim()) {
45
+ throw new AppError(CommonErrors.INVALID_INPUT, 'Query is required and cannot be empty', true);
46
+ }
47
+ // Use the provided readOnly flag, or fall back to the default from config
48
+ const isReadOnly = readOnly !== undefined ? readOnly : config.falkorDB.defaultReadOnly;
49
+ const result = await falkorDBService.executeQuery(graphName, query, undefined, isReadOnly);
50
+ await logger.debug('Query tool executed successfully', { graphName, readOnly: isReadOnly });
51
+ return {
52
+ content: [{
53
+ type: "text",
54
+ text: JSON.stringify(result, null, 2)
55
+ }]
56
+ };
57
+ }
58
+ catch (error) {
59
+ await logger.error('Query tool execution failed', error instanceof Error ? error : new Error(String(error)), { graphName, query: query.substring(0, 100) + (query.length > 100 ? '...' : '') });
60
+ throw error;
61
+ }
62
+ });
63
+ }
64
+ function registerQueryGraphReadOnlyTool(server) {
65
+ server.registerTool("query_graph_readonly", {
66
+ title: "Query Graph (Read-Only)",
67
+ 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.",
68
+ inputSchema: queryGraphReadOnlySchema,
69
+ }, async (args) => {
70
+ const { graphName, query } = z.object(queryGraphReadOnlySchema).parse(args);
71
+ try {
72
+ if (!graphName?.trim()) {
73
+ throw new AppError(CommonErrors.INVALID_INPUT, 'Graph name is required and cannot be empty', true);
74
+ }
75
+ if (!query?.trim()) {
76
+ throw new AppError(CommonErrors.INVALID_INPUT, 'Query is required and cannot be empty', true);
77
+ }
78
+ const result = await falkorDBService.executeReadOnlyQuery(graphName, query);
79
+ await logger.debug('Read-only query tool executed successfully', { graphName });
80
+ return {
81
+ content: [{
82
+ type: "text",
83
+ text: JSON.stringify(result, null, 2)
84
+ }]
85
+ };
86
+ }
87
+ catch (error) {
88
+ await logger.error('Read-only query tool execution failed', error instanceof Error ? error : new Error(String(error)), { graphName, query: query.substring(0, 100) + (query.length > 100 ? '...' : '') });
89
+ throw error;
90
+ }
91
+ });
92
+ }
93
+ function registerListGraphsTool(server) {
94
+ // Register list_graphs tool
95
+ server.registerTool("list_graphs", {
96
+ title: "List Graphs",
97
+ description: "List all graphs available to query",
98
+ inputSchema: {},
99
+ }, async () => {
100
+ try {
101
+ const result = await falkorDBService.listGraphs();
102
+ await logger.debug('List graphs tool executed', { count: result.length });
103
+ return {
104
+ content: [{
105
+ type: "text",
106
+ text: result.join("\n"),
107
+ }]
108
+ };
109
+ }
110
+ catch (error) {
111
+ await logger.error('List graphs tool execution failed', error instanceof Error ? error : new Error(String(error)));
112
+ throw error;
113
+ }
114
+ });
115
+ }
116
+ function registerDeleteGraphTool(server) {
117
+ // Register delete_graph tool
118
+ server.registerTool("delete_graph", {
119
+ title: "Delete Graph",
120
+ description: "Permanently delete a graph from the database. WARNING: This action is irreversible. You must set confirmDelete to true to proceed.",
121
+ inputSchema: deleteGraphSchema,
122
+ }, async (args) => {
123
+ const { graphName } = z.object(deleteGraphSchema).parse(args);
124
+ try {
125
+ if (!graphName?.trim()) {
126
+ throw new AppError(CommonErrors.INVALID_INPUT, 'Graph name is required and cannot be empty', true);
127
+ }
128
+ await falkorDBService.deleteGraph(graphName);
129
+ await logger.info('Delete graph tool executed successfully', { graphName });
130
+ return {
131
+ content: [{
132
+ type: "text",
133
+ text: `Graph ${graphName} deleted`
134
+ }]
135
+ };
136
+ }
137
+ catch (error) {
138
+ await logger.error('Delete graph tool execution failed', error instanceof Error ? error : new Error(String(error)), { graphName });
139
+ throw error;
140
+ }
141
+ });
142
+ }
143
+ function registerListKeysTool(server) {
144
+ server.registerTool("list_keys", {
145
+ title: "List Keys",
146
+ description: "List all keys in Redis",
147
+ inputSchema: {},
148
+ }, async () => {
149
+ try {
150
+ const keys = await redisService.listKeys();
151
+ await logger.debug('List keys tool executed', { count: keys.length });
152
+ return {
153
+ content: [{
154
+ type: "text",
155
+ text: keys.join("\n"),
156
+ }]
157
+ };
158
+ }
159
+ catch (error) {
160
+ await logger.error('List keys tool execution failed', error instanceof Error ? error : new Error(String(error)));
161
+ throw error;
162
+ }
163
+ });
164
+ }
165
+ function registerSetKeyTool(server) {
166
+ // Register set_key tool
167
+ server.registerTool("set_key", {
168
+ title: "Set Key",
169
+ description: "Set a key in Redis",
170
+ inputSchema: setKeySchema,
171
+ }, async (args) => {
172
+ const { key, value } = z.object(setKeySchema).parse(args);
173
+ try {
174
+ if (!key?.trim()) {
175
+ throw new AppError(CommonErrors.INVALID_INPUT, 'Key is required and cannot be empty', true);
176
+ }
177
+ if (value === undefined || value === null) {
178
+ throw new AppError(CommonErrors.INVALID_INPUT, 'Value is required', true);
179
+ }
180
+ await redisService.set(key, value);
181
+ await logger.debug('Set key tool executed successfully', { key });
182
+ return {
183
+ content: [{
184
+ type: "text",
185
+ text: `Key ${key} set successfully`
186
+ }]
187
+ };
188
+ }
189
+ catch (error) {
190
+ await logger.error('Set key tool execution failed', error instanceof Error ? error : new Error(String(error)), { key });
191
+ throw error;
192
+ }
193
+ });
194
+ }
195
+ function registerGetKeyTool(server) {
196
+ // Register get_key tool
197
+ server.registerTool("get_key", {
198
+ title: "Get Key",
199
+ description: "Get a key from Redis",
200
+ inputSchema: getKeySchema,
201
+ }, async (args) => {
202
+ const { key } = z.object(getKeySchema).parse(args);
203
+ try {
204
+ if (!key?.trim()) {
205
+ throw new AppError(CommonErrors.INVALID_INPUT, 'Key is required and cannot be empty', true);
206
+ }
207
+ const value = await redisService.get(key);
208
+ await logger.debug('Get key tool executed successfully', { key, hasValue: value !== null });
209
+ return {
210
+ content: [{
211
+ type: "text",
212
+ text: `Key ${key} is ${value ?? 'null (not found)'}`
213
+ }]
214
+ };
215
+ }
216
+ catch (error) {
217
+ await logger.error('Get key tool execution failed', error instanceof Error ? error : new Error(String(error)), { key });
218
+ throw error;
219
+ }
220
+ });
221
+ }
222
+ function registerDeleteKeyTool(server) {
223
+ server.registerTool("delete_key", {
224
+ title: "Delete Key",
225
+ description: "Permanently delete a key from Redis. WARNING: This action is irreversible. You must set confirmDelete to true to proceed.",
226
+ inputSchema: deleteKeySchema,
227
+ }, async (args) => {
228
+ const { key } = z.object(deleteKeySchema).parse(args);
229
+ try {
230
+ if (!key?.trim()) {
231
+ throw new AppError(CommonErrors.INVALID_INPUT, 'Key is required and cannot be empty', true);
232
+ }
233
+ await redisService.delete(key);
234
+ await logger.debug('Delete key tool executed successfully', { key });
235
+ return {
236
+ content: [{
237
+ type: "text",
238
+ text: `Key ${key} deleted`
239
+ }]
240
+ };
241
+ }
242
+ catch (error) {
243
+ await logger.error('Delete key tool execution failed', error instanceof Error ? error : new Error(String(error)), { key });
244
+ throw error;
245
+ }
246
+ });
247
+ }
248
+ export default function registerAllTools(server) {
249
+ // Register query_graph tools
250
+ registerQueryGraphTool(server);
251
+ registerQueryGraphReadOnlyTool(server);
252
+ registerListGraphsTool(server);
253
+ registerDeleteGraphTool(server);
254
+ registerSetKeyTool(server);
255
+ registerGetKeyTool(server);
256
+ registerDeleteKeyTool(server);
257
+ registerListKeysTool(server);
258
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * MCP Client Configuration Types
3
+ */
4
+ /**
5
+ * Sample MCP Client Configuration
6
+ */
7
+ export const sampleMCPClientConfig = {
8
+ defaultServer: "falkordb",
9
+ servers: {
10
+ "falkordb": {
11
+ url: "http://localhost:3000/api/mcp",
12
+ apiKey: "your_api_key_here"
13
+ }
14
+ }
15
+ };
16
+ /**
17
+ * Sample MCP Server Configuration
18
+ */
19
+ export const sampleMCPServerConfig = {
20
+ mcpServers: {
21
+ "falkordb": {
22
+ command: "docker",
23
+ args: [
24
+ "run",
25
+ "-i",
26
+ "--rm",
27
+ "-p", "3000:3000",
28
+ "--env-file", ".env",
29
+ "falkordb-mcpserver",
30
+ "falkordb://host.docker.internal:6379"
31
+ ]
32
+ }
33
+ }
34
+ };
@@ -0,0 +1,173 @@
1
+ import { sampleMCPClientConfig, sampleMCPServerConfig } from './mcp-client-config';
2
+ describe('MCP Client Configuration Models', () => {
3
+ describe('Type Definitions', () => {
4
+ it('should define MCPServerConfig interface correctly', () => {
5
+ // Arrange
6
+ const config = {
7
+ mcpServers: {
8
+ 'test-server': {
9
+ command: 'node',
10
+ args: ['server.js']
11
+ }
12
+ }
13
+ };
14
+ // Assert - TypeScript compilation validates the interface
15
+ expect(config.mcpServers).toBeDefined();
16
+ expect(config.mcpServers['test-server'].command).toBe('node');
17
+ expect(config.mcpServers['test-server'].args).toEqual(['server.js']);
18
+ });
19
+ it('should define MCPClientConfig interface correctly', () => {
20
+ // Arrange
21
+ const config = {
22
+ defaultServer: 'test',
23
+ servers: {
24
+ 'test': {
25
+ url: 'http://localhost:3000',
26
+ apiKey: 'secret'
27
+ }
28
+ }
29
+ };
30
+ // Assert - TypeScript compilation validates the interface
31
+ expect(config.defaultServer).toBe('test');
32
+ expect(config.servers).toBeDefined();
33
+ expect(config.servers['test'].url).toBe('http://localhost:3000');
34
+ expect(config.servers['test'].apiKey).toBe('secret');
35
+ });
36
+ it('should allow optional properties in MCPClientConfig', () => {
37
+ // Arrange
38
+ const configWithoutDefaults = {
39
+ servers: {
40
+ 'test': {
41
+ url: 'http://localhost:3000'
42
+ // apiKey is optional
43
+ }
44
+ }
45
+ // defaultServer is optional
46
+ };
47
+ // Assert
48
+ expect(configWithoutDefaults.defaultServer).toBeUndefined();
49
+ expect(configWithoutDefaults.servers['test'].apiKey).toBeUndefined();
50
+ expect(configWithoutDefaults.servers['test'].url).toBe('http://localhost:3000');
51
+ });
52
+ });
53
+ describe('Sample Configurations', () => {
54
+ describe('sampleMCPClientConfig', () => {
55
+ it('should have correct structure and values', () => {
56
+ // Assert
57
+ expect(sampleMCPClientConfig).toBeDefined();
58
+ expect(sampleMCPClientConfig.defaultServer).toBe('falkordb');
59
+ expect(sampleMCPClientConfig.servers).toBeDefined();
60
+ expect(sampleMCPClientConfig.servers.falkordb).toBeDefined();
61
+ expect(sampleMCPClientConfig.servers.falkordb.url).toBe('http://localhost:3000/api/mcp');
62
+ expect(sampleMCPClientConfig.servers.falkordb.apiKey).toBe('your_api_key_here');
63
+ });
64
+ it('should be a valid MCPClientConfig', () => {
65
+ // Act - assign to typed variable to ensure type compliance
66
+ const config = sampleMCPClientConfig;
67
+ // Assert
68
+ expect(config).toBe(sampleMCPClientConfig);
69
+ });
70
+ it('should be immutable reference', () => {
71
+ // Act - get multiple references
72
+ const ref1 = sampleMCPClientConfig;
73
+ const ref2 = sampleMCPClientConfig;
74
+ // Assert
75
+ expect(ref1).toBe(ref2);
76
+ });
77
+ });
78
+ describe('sampleMCPServerConfig', () => {
79
+ it('should have correct structure and values', () => {
80
+ // Assert
81
+ expect(sampleMCPServerConfig).toBeDefined();
82
+ expect(sampleMCPServerConfig.mcpServers).toBeDefined();
83
+ expect(sampleMCPServerConfig.mcpServers.falkordb).toBeDefined();
84
+ expect(sampleMCPServerConfig.mcpServers.falkordb.command).toBe('docker');
85
+ expect(sampleMCPServerConfig.mcpServers.falkordb.args).toBeInstanceOf(Array);
86
+ });
87
+ it('should have correct docker arguments', () => {
88
+ // Arrange
89
+ const expectedArgs = [
90
+ 'run',
91
+ '-i',
92
+ '--rm',
93
+ '-p', '3000:3000',
94
+ '--env-file', '.env',
95
+ 'falkordb-mcpserver',
96
+ 'falkordb://host.docker.internal:6379'
97
+ ];
98
+ // Assert
99
+ expect(sampleMCPServerConfig.mcpServers.falkordb.args).toEqual(expectedArgs);
100
+ });
101
+ it('should be a valid MCPServerConfig', () => {
102
+ // Act - assign to typed variable to ensure type compliance
103
+ const config = sampleMCPServerConfig;
104
+ // Assert
105
+ expect(config).toBe(sampleMCPServerConfig);
106
+ });
107
+ it('should include Docker container configuration', () => {
108
+ // Arrange
109
+ const falkordbConfig = sampleMCPServerConfig.mcpServers.falkordb;
110
+ // Assert
111
+ expect(falkordbConfig.command).toBe('docker');
112
+ expect(falkordbConfig.args).toContain('run');
113
+ expect(falkordbConfig.args).toContain('falkordb-mcpserver');
114
+ expect(falkordbConfig.args).toContain('falkordb://host.docker.internal:6379');
115
+ expect(falkordbConfig.args).toContain('-p');
116
+ expect(falkordbConfig.args).toContain('3000:3000');
117
+ expect(falkordbConfig.args).toContain('--env-file');
118
+ expect(falkordbConfig.args).toContain('.env');
119
+ });
120
+ });
121
+ });
122
+ describe('Configuration Usage Examples', () => {
123
+ it('should support multiple servers in client config', () => {
124
+ // Arrange
125
+ const multiServerConfig = {
126
+ defaultServer: 'primary',
127
+ servers: {
128
+ 'primary': {
129
+ url: 'http://localhost:3000/api/mcp',
130
+ apiKey: 'primary-key'
131
+ },
132
+ 'backup': {
133
+ url: 'http://backup.example.com/api/mcp',
134
+ apiKey: 'backup-key'
135
+ },
136
+ 'dev': {
137
+ url: 'http://dev.localhost:3001/api/mcp'
138
+ // No API key for dev
139
+ }
140
+ }
141
+ };
142
+ // Assert
143
+ expect(Object.keys(multiServerConfig.servers)).toHaveLength(3);
144
+ expect(multiServerConfig.servers.primary.apiKey).toBeDefined();
145
+ expect(multiServerConfig.servers.backup.apiKey).toBeDefined();
146
+ expect(multiServerConfig.servers.dev.apiKey).toBeUndefined();
147
+ });
148
+ it('should support multiple server processes in server config', () => {
149
+ // Arrange
150
+ const multiProcessConfig = {
151
+ mcpServers: {
152
+ 'falkordb-main': {
153
+ command: 'node',
154
+ args: ['dist/index.js']
155
+ },
156
+ 'falkordb-worker': {
157
+ command: 'node',
158
+ args: ['dist/worker.js', '--port', '3001']
159
+ },
160
+ 'falkordb-docker': {
161
+ command: 'docker',
162
+ args: ['run', '-p', '3002:3000', 'falkordb-mcpserver']
163
+ }
164
+ }
165
+ };
166
+ // Assert
167
+ expect(Object.keys(multiProcessConfig.mcpServers)).toHaveLength(3);
168
+ expect(multiProcessConfig.mcpServers['falkordb-main'].command).toBe('node');
169
+ expect(multiProcessConfig.mcpServers['falkordb-worker'].args).toContain('--port');
170
+ expect(multiProcessConfig.mcpServers['falkordb-docker'].command).toBe('docker');
171
+ });
172
+ });
173
+ });
@@ -0,0 +1,4 @@
1
+ /**
2
+ * MCP Types - based on Model Context Protocol specification
3
+ */
4
+ export {};
@@ -0,0 +1,175 @@
1
+ import { FalkorDB } from 'falkordb';
2
+ import { config } from '../config/index.js';
3
+ import { AppError, CommonErrors } from '../errors/AppError.js';
4
+ import { logger } from './logger.service.js';
5
+ class FalkorDBService {
6
+ client = null;
7
+ maxRetries = 5;
8
+ retryCount = 0;
9
+ initializingPromise = null;
10
+ constructor() {
11
+ // Don't initialize in constructor - use explicit initialization
12
+ }
13
+ async initialize() {
14
+ // Idempotency guard: skip if already connected
15
+ if (this.client) {
16
+ return;
17
+ }
18
+ if (this.initializingPromise) {
19
+ return this.initializingPromise;
20
+ }
21
+ this.retryCount = 0;
22
+ this.initializingPromise = this._initialize();
23
+ try {
24
+ await this.initializingPromise;
25
+ }
26
+ finally {
27
+ this.initializingPromise = null;
28
+ }
29
+ }
30
+ async _initialize() {
31
+ for (;; this.retryCount++) {
32
+ try {
33
+ // Fire-and-forget: informational log, not critical
34
+ logger.info('Attempting to connect to FalkorDB', {
35
+ host: config.falkorDB.host,
36
+ port: config.falkorDB.port,
37
+ attempt: this.retryCount + 1
38
+ });
39
+ this.client = await FalkorDB.connect({
40
+ socket: {
41
+ host: config.falkorDB.host,
42
+ port: config.falkorDB.port,
43
+ },
44
+ password: config.falkorDB.password,
45
+ username: config.falkorDB.username,
46
+ });
47
+ // Test connection
48
+ const connection = await this.client.connection;
49
+ await connection.ping();
50
+ // Fire-and-forget: informational log, not critical
51
+ logger.info('Successfully connected to FalkorDB');
52
+ this.retryCount = 0;
53
+ return;
54
+ }
55
+ catch (error) {
56
+ // Clean up any partially connected client before retrying or throwing
57
+ if (this.client) {
58
+ try {
59
+ await this.client.close();
60
+ }
61
+ catch {
62
+ // Ignore cleanup errors
63
+ }
64
+ this.client = null;
65
+ }
66
+ if (this.retryCount < this.maxRetries) {
67
+ // Fire-and-forget: informational log before retry delay
68
+ logger.warn('Failed to connect to FalkorDB, retrying...', {
69
+ attempt: this.retryCount + 1,
70
+ maxRetries: this.maxRetries,
71
+ error: error instanceof Error ? error.message : String(error)
72
+ });
73
+ const delay = Math.min(5000 * 2 ** this.retryCount, 30000) + Math.random() * 1000;
74
+ await new Promise(resolve => setTimeout(resolve, delay));
75
+ }
76
+ else {
77
+ const appError = new AppError(CommonErrors.CONNECTION_FAILED, `Failed to connect to FalkorDB after ${this.maxRetries} attempts: ${error instanceof Error ? error.message : String(error)}`, true);
78
+ await logger.error('FalkorDB connection failed permanently', appError);
79
+ throw appError;
80
+ }
81
+ }
82
+ }
83
+ }
84
+ async executeQuery(graphName, query, params, readOnly = false) {
85
+ if (!this.client) {
86
+ throw new AppError(CommonErrors.CONNECTION_FAILED, 'FalkorDB client not initialized. Call initialize() first.', true);
87
+ }
88
+ try {
89
+ const graph = this.client.selectGraph(graphName);
90
+ const result = readOnly
91
+ ? await graph.roQuery(query, params)
92
+ : await graph.query(query, params);
93
+ // Fire-and-forget: informational log, not critical
94
+ logger.debug('Query executed successfully', {
95
+ graphName,
96
+ query: query.substring(0, 100) + (query.length > 100 ? '...' : ''),
97
+ hasParams: !!params,
98
+ readOnly
99
+ });
100
+ return result;
101
+ }
102
+ catch (error) {
103
+ const appError = new AppError(CommonErrors.OPERATION_FAILED, `Failed to execute ${readOnly ? 'read-only ' : ''}query on graph '${graphName}': ${error instanceof Error ? error.message : String(error)}`, true);
104
+ // Sanitize query for error logging using same truncation as debug logs
105
+ const safeQuery = query.substring(0, 100) + (query.length > 100 ? '...' : '');
106
+ await logger.error('Query execution failed', appError, { graphName, query: safeQuery, readOnly });
107
+ throw appError;
108
+ }
109
+ }
110
+ /**
111
+ * Execute a read-only query on a specific graph
112
+ * This is useful for replica instances or when you want to ensure no writes occur
113
+ * @param graphName - The name of the graph to query
114
+ * @param query - The OpenCypher query to execute
115
+ * @param params - Optional query parameters
116
+ * @returns Query result
117
+ */
118
+ async executeReadOnlyQuery(graphName, query, params) {
119
+ return this.executeQuery(graphName, query, params, true);
120
+ }
121
+ /**
122
+ * Lists all available graphs in FalkorDB
123
+ * @returns Array of graph names
124
+ */
125
+ async listGraphs() {
126
+ if (!this.client) {
127
+ throw new AppError(CommonErrors.CONNECTION_FAILED, 'FalkorDB client not initialized. Call initialize() first.', true);
128
+ }
129
+ try {
130
+ const graphs = await this.client.list();
131
+ // Fire-and-forget: informational log, not critical
132
+ logger.debug('Listed graphs successfully', { count: graphs.length });
133
+ return graphs;
134
+ }
135
+ catch (error) {
136
+ const appError = new AppError(CommonErrors.OPERATION_FAILED, `Failed to list graphs: ${error instanceof Error ? error.message : String(error)}`, true);
137
+ await logger.error('Failed to list graphs', appError);
138
+ throw appError;
139
+ }
140
+ }
141
+ async deleteGraph(graphName) {
142
+ if (!this.client) {
143
+ throw new AppError(CommonErrors.CONNECTION_FAILED, 'FalkorDB client not initialized. Call initialize() first.', true);
144
+ }
145
+ try {
146
+ await this.client.selectGraph(graphName).delete();
147
+ // Fire-and-forget: informational log, not critical
148
+ logger.info('Graph deleted successfully', { graphName });
149
+ }
150
+ catch (error) {
151
+ const appError = new AppError(CommonErrors.OPERATION_FAILED, `Failed to delete graph '${graphName}': ${error instanceof Error ? error.message : String(error)}`, true);
152
+ await logger.error('Failed to delete graph', appError, { graphName });
153
+ throw appError;
154
+ }
155
+ }
156
+ async close() {
157
+ if (this.client) {
158
+ try {
159
+ await this.client.close();
160
+ // Fire-and-forget: informational log, not critical
161
+ logger.info('FalkorDB connection closed successfully');
162
+ }
163
+ catch (error) {
164
+ // Fire-and-forget: best-effort log during shutdown
165
+ logger.error('Error closing FalkorDB connection', error instanceof Error ? error : new Error(String(error)));
166
+ }
167
+ finally {
168
+ this.client = null;
169
+ this.retryCount = 0;
170
+ }
171
+ }
172
+ }
173
+ }
174
+ // Export a singleton instance
175
+ export const falkorDBService = new FalkorDBService();