@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.
- package/.env.example +26 -0
- package/LICENSE +21 -0
- package/README.md +412 -0
- package/dist/config/index.js +27 -0
- package/dist/config/index.test.js +23 -0
- package/dist/errors/AppError.js +27 -0
- package/dist/errors/ErrorHandler.js +46 -0
- package/dist/errors/ErrorHandler.test.js +146 -0
- package/dist/index.js +234 -0
- package/dist/mcp/prompts.js +229 -0
- package/dist/mcp/resources.js +26 -0
- package/dist/mcp/tools.js +258 -0
- package/dist/models/mcp-client-config.js +34 -0
- package/dist/models/mcp-client-config.test.js +173 -0
- package/dist/models/mcp.types.js +4 -0
- package/dist/services/falkordb.service.js +175 -0
- package/dist/services/falkordb.service.test.js +489 -0
- package/dist/services/logger.service.js +151 -0
- package/dist/services/logger.service.test.js +115 -0
- package/dist/services/redis.service.js +179 -0
- package/dist/services/redis.service.test.js +399 -0
- package/dist/utils/connection-parser.js +71 -0
- package/dist/utils/connection-parser.test.js +232 -0
- package/package.json +99 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { ErrorHandler, errorHandler } from './ErrorHandler';
|
|
2
|
+
import { AppError, CommonErrors } from './AppError';
|
|
3
|
+
// Mock the logger service
|
|
4
|
+
jest.mock('../services/logger.service.js', () => ({
|
|
5
|
+
logger: {
|
|
6
|
+
error: jest.fn().mockResolvedValue(undefined),
|
|
7
|
+
info: jest.fn().mockResolvedValue(undefined),
|
|
8
|
+
errorSync: jest.fn(),
|
|
9
|
+
}
|
|
10
|
+
}));
|
|
11
|
+
// Get mock logger from the mocked module
|
|
12
|
+
let mockLogger;
|
|
13
|
+
describe('ErrorHandler', () => {
|
|
14
|
+
let handler;
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
handler = new ErrorHandler();
|
|
17
|
+
jest.clearAllMocks();
|
|
18
|
+
// Import the mock logger
|
|
19
|
+
const loggerModule = await import('../services/logger.service.js');
|
|
20
|
+
mockLogger = loggerModule.logger;
|
|
21
|
+
});
|
|
22
|
+
describe('handleError', () => {
|
|
23
|
+
it('should handle operational errors gracefully', async () => {
|
|
24
|
+
// Arrange
|
|
25
|
+
const operationalError = new AppError(CommonErrors.OPERATION_FAILED, 'Test operational error', true);
|
|
26
|
+
// Act
|
|
27
|
+
await handler.handleError(operationalError);
|
|
28
|
+
// Assert
|
|
29
|
+
expect(mockLogger.error).toHaveBeenCalledWith('Unhandled error occurred', operationalError, expect.objectContaining({
|
|
30
|
+
timestamp: expect.any(String),
|
|
31
|
+
errorType: 'AppError'
|
|
32
|
+
}));
|
|
33
|
+
expect(mockLogger.info).toHaveBeenCalledWith('Operational error handled gracefully', {
|
|
34
|
+
errorName: operationalError.name,
|
|
35
|
+
errorMessage: operationalError.message
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
it('should handle programmer errors with critical logging', async () => {
|
|
39
|
+
// Arrange
|
|
40
|
+
const programmerError = new AppError(CommonErrors.INVALID_INPUT, 'Test programmer error', false // not operational
|
|
41
|
+
);
|
|
42
|
+
// Act
|
|
43
|
+
await handler.handleError(programmerError);
|
|
44
|
+
// Assert
|
|
45
|
+
expect(mockLogger.error).toHaveBeenCalledWith('Unhandled error occurred', programmerError, expect.objectContaining({
|
|
46
|
+
timestamp: expect.any(String),
|
|
47
|
+
errorType: 'AppError'
|
|
48
|
+
}));
|
|
49
|
+
expect(mockLogger.error).toHaveBeenCalledWith('Programmer error detected - may require process restart', programmerError, {
|
|
50
|
+
recommendation: 'Review code for bugs',
|
|
51
|
+
severity: 'critical'
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
it('should handle generic errors as programmer errors', async () => {
|
|
55
|
+
// Arrange
|
|
56
|
+
const genericError = new Error('Generic error message');
|
|
57
|
+
// Act
|
|
58
|
+
await handler.handleError(genericError);
|
|
59
|
+
// Assert
|
|
60
|
+
expect(mockLogger.error).toHaveBeenCalledWith('Unhandled error occurred', genericError, expect.objectContaining({
|
|
61
|
+
timestamp: expect.any(String),
|
|
62
|
+
errorType: 'Error'
|
|
63
|
+
}));
|
|
64
|
+
expect(mockLogger.error).toHaveBeenCalledWith('Programmer error detected - may require process restart', genericError, {
|
|
65
|
+
recommendation: 'Review code for bugs',
|
|
66
|
+
severity: 'critical'
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe('isTrustedError', () => {
|
|
71
|
+
it('should return true for operational AppError', () => {
|
|
72
|
+
// Arrange
|
|
73
|
+
const operationalError = new AppError(CommonErrors.OPERATION_FAILED, 'Test operational error', true);
|
|
74
|
+
// Act
|
|
75
|
+
const result = handler.isTrustedError(operationalError);
|
|
76
|
+
// Assert
|
|
77
|
+
expect(result).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
it('should return false for non-operational AppError', () => {
|
|
80
|
+
// Arrange
|
|
81
|
+
const nonOperationalError = new AppError(CommonErrors.INVALID_INPUT, 'Test programmer error', false);
|
|
82
|
+
// Act
|
|
83
|
+
const result = handler.isTrustedError(nonOperationalError);
|
|
84
|
+
// Assert
|
|
85
|
+
expect(result).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
it('should return false for generic Error', () => {
|
|
88
|
+
// Arrange
|
|
89
|
+
const genericError = new Error('Generic error');
|
|
90
|
+
// Act
|
|
91
|
+
const result = handler.isTrustedError(genericError);
|
|
92
|
+
// Assert
|
|
93
|
+
expect(result).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
it('should return false for non-Error objects', () => {
|
|
96
|
+
// Arrange
|
|
97
|
+
const nonError = { message: 'Not an error' };
|
|
98
|
+
// Act
|
|
99
|
+
const result = handler.isTrustedError(nonError);
|
|
100
|
+
// Assert
|
|
101
|
+
expect(result).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
describe('crashIfUntrustedError', () => {
|
|
105
|
+
let processExitSpy;
|
|
106
|
+
beforeEach(() => {
|
|
107
|
+
processExitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
|
|
108
|
+
throw new Error('process.exit called');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
afterEach(() => {
|
|
112
|
+
processExitSpy.mockRestore();
|
|
113
|
+
});
|
|
114
|
+
it('should not crash for trusted operational errors', () => {
|
|
115
|
+
// Arrange
|
|
116
|
+
const operationalError = new AppError(CommonErrors.OPERATION_FAILED, 'Test operational error', true);
|
|
117
|
+
// Act & Assert
|
|
118
|
+
expect(() => handler.crashIfUntrustedError(operationalError)).not.toThrow();
|
|
119
|
+
expect(processExitSpy).not.toHaveBeenCalled();
|
|
120
|
+
expect(mockLogger.errorSync).not.toHaveBeenCalled();
|
|
121
|
+
});
|
|
122
|
+
it('should crash for untrusted programmer errors', () => {
|
|
123
|
+
// Arrange
|
|
124
|
+
const programmerError = new AppError(CommonErrors.INVALID_INPUT, 'Test programmer error', false);
|
|
125
|
+
// Act & Assert
|
|
126
|
+
expect(() => handler.crashIfUntrustedError(programmerError)).toThrow('process.exit called');
|
|
127
|
+
expect(mockLogger.errorSync).toHaveBeenCalledWith('Crashing process due to untrusted error', programmerError);
|
|
128
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
129
|
+
});
|
|
130
|
+
it('should crash for generic errors', () => {
|
|
131
|
+
// Arrange
|
|
132
|
+
const genericError = new Error('Generic error');
|
|
133
|
+
// Act & Assert
|
|
134
|
+
expect(() => handler.crashIfUntrustedError(genericError)).toThrow('process.exit called');
|
|
135
|
+
expect(mockLogger.errorSync).toHaveBeenCalledWith('Crashing process due to untrusted error', genericError);
|
|
136
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
describe('singleton instance', () => {
|
|
140
|
+
it('should export a singleton instance', () => {
|
|
141
|
+
// Assert
|
|
142
|
+
expect(errorHandler).toBeInstanceOf(ErrorHandler);
|
|
143
|
+
expect(errorHandler).toBe(errorHandler); // Same instance
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'module';
|
|
3
|
+
import { createServer } from 'http';
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
5
|
+
import { falkorDBService } from './services/falkordb.service.js';
|
|
6
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
9
|
+
import { redisService } from './services/redis.service.js';
|
|
10
|
+
import { errorHandler } from './errors/ErrorHandler.js';
|
|
11
|
+
import { logger } from './services/logger.service.js';
|
|
12
|
+
import { config } from './config/index.js';
|
|
13
|
+
import registerAllTools from './mcp/tools.js';
|
|
14
|
+
import registerAllResources from './mcp/resources.js';
|
|
15
|
+
import registerAllPrompts from './mcp/prompts.js';
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
const { version } = require('../package.json');
|
|
18
|
+
// Setup global error handlers following Node.js best practices
|
|
19
|
+
process.on('uncaughtException', (error) => {
|
|
20
|
+
logger.errorSync('Uncaught exception occurred', error);
|
|
21
|
+
void errorHandler.handleError(error).catch((handlerError) => {
|
|
22
|
+
logger.errorSync('Error while handling uncaught exception', handlerError instanceof Error ? handlerError : new Error(String(handlerError)));
|
|
23
|
+
});
|
|
24
|
+
errorHandler.crashIfUntrustedError(error);
|
|
25
|
+
});
|
|
26
|
+
process.on('unhandledRejection', (reason) => {
|
|
27
|
+
// Re-throw as error to be caught by uncaughtException handler
|
|
28
|
+
const error = reason instanceof Error ? reason : new Error(String(reason));
|
|
29
|
+
throw error;
|
|
30
|
+
});
|
|
31
|
+
// Graceful shutdown handler
|
|
32
|
+
let httpServer = null;
|
|
33
|
+
const gracefulShutdown = async (signal) => {
|
|
34
|
+
await logger.info(`Received ${signal}, shutting down gracefully`);
|
|
35
|
+
try {
|
|
36
|
+
if (httpServer) {
|
|
37
|
+
await new Promise((resolve, reject) => {
|
|
38
|
+
httpServer.close((err) => err ? reject(err) : resolve());
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
await falkorDBService.close();
|
|
42
|
+
await redisService.close();
|
|
43
|
+
await logger.info('All services closed successfully');
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
await logger.error('Error during graceful shutdown', error instanceof Error ? error : new Error(String(error)));
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
52
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
53
|
+
// Create an MCP server
|
|
54
|
+
const server = new McpServer({
|
|
55
|
+
name: "falkordb",
|
|
56
|
+
version: version
|
|
57
|
+
}, {
|
|
58
|
+
capabilities: {
|
|
59
|
+
tools: {
|
|
60
|
+
listChanged: true,
|
|
61
|
+
},
|
|
62
|
+
resources: {
|
|
63
|
+
listChanged: true,
|
|
64
|
+
},
|
|
65
|
+
prompts: {
|
|
66
|
+
listChanged: true,
|
|
67
|
+
},
|
|
68
|
+
logging: {},
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
// Note: Current MCP TypeScript SDK doesn't directly support elicitation in tool handlers
|
|
72
|
+
// This is a conceptual implementation - you'd need to implement session access
|
|
73
|
+
// Configure logger to send notifications to MCP clients
|
|
74
|
+
logger.setMcpServer(server);
|
|
75
|
+
// Register all tools and resources
|
|
76
|
+
registerAllTools(server);
|
|
77
|
+
registerAllResources(server);
|
|
78
|
+
registerAllPrompts(server);
|
|
79
|
+
// Initialize services before starting server
|
|
80
|
+
async function initializeServices() {
|
|
81
|
+
await logger.info('Initializing FalkorDB MCP server...');
|
|
82
|
+
try {
|
|
83
|
+
await falkorDBService.initialize();
|
|
84
|
+
await redisService.initialize();
|
|
85
|
+
await logger.info('All services initialized successfully');
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
await logger.error('Failed to initialize services', error instanceof Error ? error : new Error(String(error)));
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Main server startup
|
|
93
|
+
async function startServer() {
|
|
94
|
+
try {
|
|
95
|
+
await initializeServices();
|
|
96
|
+
if (config.mcp.transport === 'http') {
|
|
97
|
+
await startHTTPServer();
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
await startStdioServer();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
await logger.error('Failed to start MCP server', error instanceof Error ? error : new Error(String(error)));
|
|
105
|
+
await gracefulShutdown('STARTUP_ERROR');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function startStdioServer() {
|
|
109
|
+
const transport = new StdioServerTransport();
|
|
110
|
+
await server.connect(transport);
|
|
111
|
+
await logger.info('MCP server started successfully (stdio transport)');
|
|
112
|
+
}
|
|
113
|
+
async function startHTTPServer() {
|
|
114
|
+
const port = config.server.port;
|
|
115
|
+
const apiKey = config.mcp.apiKey;
|
|
116
|
+
// Map session IDs to their transports for session management
|
|
117
|
+
const sessions = new Map();
|
|
118
|
+
httpServer = createServer(async (req, res) => {
|
|
119
|
+
// API key authentication for HTTP transport
|
|
120
|
+
if (apiKey) {
|
|
121
|
+
const authHeader = req.headers['authorization'];
|
|
122
|
+
if (!authHeader || authHeader !== `Bearer ${apiKey}`) {
|
|
123
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
124
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
129
|
+
if (req.method === 'POST') {
|
|
130
|
+
// Read the request body
|
|
131
|
+
const body = await readRequestBody(req);
|
|
132
|
+
const parsedBody = JSON.parse(body);
|
|
133
|
+
if (sessionId && sessions.has(sessionId)) {
|
|
134
|
+
// Existing session — route to its transport
|
|
135
|
+
const transport = sessions.get(sessionId);
|
|
136
|
+
await transport.handleRequest(req, res, parsedBody);
|
|
137
|
+
}
|
|
138
|
+
else if (!sessionId && isInitializeRequest(parsedBody)) {
|
|
139
|
+
// New session initialization
|
|
140
|
+
const transport = new StreamableHTTPServerTransport({
|
|
141
|
+
sessionIdGenerator: () => randomUUID(),
|
|
142
|
+
});
|
|
143
|
+
transport.onclose = () => {
|
|
144
|
+
const sid = transport.sessionId;
|
|
145
|
+
if (sid)
|
|
146
|
+
sessions.delete(sid);
|
|
147
|
+
};
|
|
148
|
+
// Connect a fresh McpServer for this session
|
|
149
|
+
const sessionServer = createSessionServer();
|
|
150
|
+
await sessionServer.connect(transport);
|
|
151
|
+
await transport.handleRequest(req, res, parsedBody);
|
|
152
|
+
// Store session after handling (sessionId is set after init)
|
|
153
|
+
if (transport.sessionId) {
|
|
154
|
+
sessions.set(transport.sessionId, transport);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
159
|
+
res.end(JSON.stringify({ error: 'Bad Request: No valid session or initialization' }));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else if (req.method === 'GET') {
|
|
163
|
+
// SSE stream for server-initiated messages
|
|
164
|
+
if (sessionId && sessions.has(sessionId)) {
|
|
165
|
+
const transport = sessions.get(sessionId);
|
|
166
|
+
await transport.handleRequest(req, res);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
170
|
+
res.end(JSON.stringify({ error: 'Bad Request: Invalid or missing session ID' }));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else if (req.method === 'DELETE') {
|
|
174
|
+
// Session termination
|
|
175
|
+
if (sessionId && sessions.has(sessionId)) {
|
|
176
|
+
const transport = sessions.get(sessionId);
|
|
177
|
+
await transport.handleRequest(req, res);
|
|
178
|
+
sessions.delete(sessionId);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
182
|
+
res.end(JSON.stringify({ error: 'Bad Request: Invalid or missing session ID' }));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
187
|
+
res.end(JSON.stringify({ error: 'Method Not Allowed' }));
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
httpServer.listen(port, () => {
|
|
191
|
+
// Fire-and-forget: non-critical startup log
|
|
192
|
+
logger.info(`MCP server started successfully (HTTP transport on port ${port})`);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
function createSessionServer() {
|
|
196
|
+
const sessionServer = new McpServer({
|
|
197
|
+
name: "falkordb",
|
|
198
|
+
version: version,
|
|
199
|
+
}, {
|
|
200
|
+
capabilities: {
|
|
201
|
+
tools: { listChanged: true },
|
|
202
|
+
resources: { listChanged: true },
|
|
203
|
+
prompts: { listChanged: true },
|
|
204
|
+
logging: {},
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
logger.setMcpServer(sessionServer);
|
|
208
|
+
registerAllTools(sessionServer);
|
|
209
|
+
registerAllResources(sessionServer);
|
|
210
|
+
registerAllPrompts(sessionServer);
|
|
211
|
+
return sessionServer;
|
|
212
|
+
}
|
|
213
|
+
function readRequestBody(req) {
|
|
214
|
+
return new Promise((resolve, reject) => {
|
|
215
|
+
let body = '';
|
|
216
|
+
req.on('data', (chunk) => { body += chunk.toString(); });
|
|
217
|
+
req.on('end', () => resolve(body));
|
|
218
|
+
req.on('error', reject);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
function isInitializeRequest(body) {
|
|
222
|
+
if (typeof body === 'object' && body !== null && 'method' in body) {
|
|
223
|
+
return body.method === 'initialize';
|
|
224
|
+
}
|
|
225
|
+
if (Array.isArray(body)) {
|
|
226
|
+
return body.some(msg => typeof msg === 'object' && msg !== null && 'method' in msg && msg.method === 'initialize');
|
|
227
|
+
}
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
// Start the server
|
|
231
|
+
startServer().catch(async (error) => {
|
|
232
|
+
await logger.error('Fatal startup error', error instanceof Error ? error : new Error(String(error)));
|
|
233
|
+
process.exit(1);
|
|
234
|
+
});
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// Define schemas as simple objects first to avoid TS2589 deep recursion
|
|
3
|
+
const userSetupArgsSchema = {
|
|
4
|
+
name: z.string().describe("The name of the user"),
|
|
5
|
+
};
|
|
6
|
+
const memoryQueryArgsSchema = {
|
|
7
|
+
query: z.string().describe("The query or topic to search for in memory"),
|
|
8
|
+
context: z.string().optional().describe("Additional context to help scope the search"),
|
|
9
|
+
relationship_depth: z.coerce.number().min(1).max(3).describe("How many relationship hops to traverse (1-3)")
|
|
10
|
+
};
|
|
11
|
+
function registerUserSetupPrompt(server) {
|
|
12
|
+
// Register user_setup prompt
|
|
13
|
+
server.registerPrompt("user_setup", {
|
|
14
|
+
title: "User Setup",
|
|
15
|
+
description: "Setup the user graph node and connect it to the rest of the relevant nodes",
|
|
16
|
+
argsSchema: userSetupArgsSchema, // Cast to any to prevent TS2589 (deep recursion) during type inference
|
|
17
|
+
}, async (args) => {
|
|
18
|
+
const { name } = z.object(userSetupArgsSchema).parse(args);
|
|
19
|
+
const userMessage = `# User Setup Task
|
|
20
|
+
|
|
21
|
+
You are working with a FalkorDB graph database to manage user information and relationships.
|
|
22
|
+
|
|
23
|
+
**User Information:**
|
|
24
|
+
- Name: ${name}
|
|
25
|
+
|
|
26
|
+
**Your task is to:**
|
|
27
|
+
|
|
28
|
+
1. **Check if user exists**: Search for a node with the name "${name}" in the memory graph using a parameterized query: \`MATCH (u) WHERE u.name = $name RETURN u\` with params \`{name: "${name}"}\`
|
|
29
|
+
2. **Create user node if needed**: If no node exists with this name, create a new user node using a parameterized query with the following properties:
|
|
30
|
+
- name: "${name}"
|
|
31
|
+
- type: "User"
|
|
32
|
+
- created_at: current timestamp
|
|
33
|
+
3. **Establish relationships**: Ensure the user node is properly connected to relevant nodes in the graph, such as:
|
|
34
|
+
- Recent conversations or interactions
|
|
35
|
+
- Associated topics or interests
|
|
36
|
+
- Related entities or contexts
|
|
37
|
+
|
|
38
|
+
**Guidelines:**
|
|
39
|
+
- Use appropriate Cypher queries to search, create, and connect nodes
|
|
40
|
+
- Maintain data consistency and avoid duplicate user nodes
|
|
41
|
+
- Consider the existing graph structure when establishing new relationships
|
|
42
|
+
- Log any operations performed for debugging purposes
|
|
43
|
+
|
|
44
|
+
Please proceed with setting up the user "${name}" in the memory graph.`;
|
|
45
|
+
return {
|
|
46
|
+
messages: [
|
|
47
|
+
{
|
|
48
|
+
role: "user",
|
|
49
|
+
content: {
|
|
50
|
+
type: "text",
|
|
51
|
+
text: userMessage
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function registerMemoryQueryPrompt(server) {
|
|
59
|
+
server.registerPrompt("memory_query", {
|
|
60
|
+
title: "Memory Query",
|
|
61
|
+
description: "Query the memory graph to retrieve and analyze stored information",
|
|
62
|
+
argsSchema: memoryQueryArgsSchema, // Cast to any to prevent TS2589 (deep recursion) during type inference
|
|
63
|
+
}, async (args) => {
|
|
64
|
+
const { query, context, relationship_depth } = z.object(memoryQueryArgsSchema).parse(args);
|
|
65
|
+
const memoryMessage = `# Memory Query Task
|
|
66
|
+
|
|
67
|
+
You are working with a FalkorDB graph database to retrieve and analyze stored memory information.
|
|
68
|
+
|
|
69
|
+
**Query Information:**
|
|
70
|
+
- Search Query: ${query}
|
|
71
|
+
${context ? `- Additional Context: ${context}` : ''}
|
|
72
|
+
- Relationship Depth: ${relationship_depth} hops
|
|
73
|
+
|
|
74
|
+
**Your task is to:**
|
|
75
|
+
|
|
76
|
+
1. **Search for relevant nodes**: Use parameterized Cypher queries to find nodes that match or relate to the search query
|
|
77
|
+
- Always use query parameters (e.g., \`$query\`) instead of string interpolation to prevent injection
|
|
78
|
+
- Look for nodes with matching names, properties, or content
|
|
79
|
+
- Consider partial matches and semantic relationships
|
|
80
|
+
|
|
81
|
+
2. **Traverse relationships**: Explore connected nodes up to ${relationship_depth} relationship hops to gather context:
|
|
82
|
+
- Follow relationships like RELATES_TO, MENTIONED_IN, CONNECTED_TO
|
|
83
|
+
- Include timestamps and relationship properties
|
|
84
|
+
|
|
85
|
+
3. **Analyze and synthesize**: Process the retrieved information to:
|
|
86
|
+
- Identify key patterns and connections
|
|
87
|
+
- Extract relevant facts and relationships
|
|
88
|
+
- Organize information chronologically or by relevance
|
|
89
|
+
|
|
90
|
+
4. **Provide structured results**: Format your findings including:
|
|
91
|
+
- Direct matches and their properties
|
|
92
|
+
- Related nodes and connection paths
|
|
93
|
+
- Temporal patterns if applicable
|
|
94
|
+
- Confidence levels for relationships
|
|
95
|
+
|
|
96
|
+
**Query Guidelines:**
|
|
97
|
+
- Use MATCH patterns to find relevant nodes
|
|
98
|
+
- Utilize WHERE clauses for filtering
|
|
99
|
+
- Consider using OPTIONAL MATCH for related information
|
|
100
|
+
- Include LIMIT clauses to manage result size
|
|
101
|
+
- Order results by relevance or timestamp
|
|
102
|
+
|
|
103
|
+
**Example Cypher patterns (always use parameterized queries):**
|
|
104
|
+
\`\`\`cypher
|
|
105
|
+
// Use $query parameter - never interpolate user input directly
|
|
106
|
+
MATCH (n) WHERE n.name CONTAINS $query OR n.content CONTAINS $query
|
|
107
|
+
MATCH (n)-[r*1..${relationship_depth}]-(related)
|
|
108
|
+
RETURN n, r, related ORDER BY n.timestamp DESC
|
|
109
|
+
// Pass params: {query: "${query}"}
|
|
110
|
+
\`\`\`
|
|
111
|
+
|
|
112
|
+
Please proceed with querying the memory graph for information about "${query}".`;
|
|
113
|
+
return {
|
|
114
|
+
messages: [
|
|
115
|
+
{
|
|
116
|
+
role: "user",
|
|
117
|
+
content: {
|
|
118
|
+
type: "text",
|
|
119
|
+
text: memoryMessage
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function registerGraphReorganizationPrompt(server) {
|
|
127
|
+
server.registerPrompt("graph_reorganization", {
|
|
128
|
+
title: "Graph Reorganization",
|
|
129
|
+
description: "Analyze and reorganize the graph structure for optimal performance and usability"
|
|
130
|
+
}, async () => {
|
|
131
|
+
const reorganizationMessage = `# Graph Reorganization Task
|
|
132
|
+
|
|
133
|
+
You are working with a FalkorDB graph database to optimize its structure for better performance and usability.
|
|
134
|
+
|
|
135
|
+
**Your task is to:**
|
|
136
|
+
|
|
137
|
+
1. **Analyze Current Structure**: Examine the graph to identify structural issues:
|
|
138
|
+
- Find nodes with excessive connections (hubs with >50 relationships)
|
|
139
|
+
- Identify disconnected components or islands
|
|
140
|
+
- Locate duplicate or near-duplicate nodes
|
|
141
|
+
- Find outdated or stale connections
|
|
142
|
+
- Analyze relationship distribution and patterns
|
|
143
|
+
|
|
144
|
+
2. **Performance Optimization**: Based on the analysis, implement improvements:
|
|
145
|
+
- **Merge duplicate nodes**: Combine nodes with similar properties or content
|
|
146
|
+
- **Create strategic hubs**: Add intermediate nodes to reduce direct connections
|
|
147
|
+
- **Remove dead connections**: Delete relationships to non-existent or irrelevant nodes
|
|
148
|
+
- **Add semantic clustering**: Group related nodes under topic or category nodes
|
|
149
|
+
- **Optimize relationship types**: Ensure relationship labels are descriptive and indexed
|
|
150
|
+
|
|
151
|
+
3. **Usability Enhancement**: Improve graph navigation and querying:
|
|
152
|
+
- **Add metadata nodes**: Create nodes that summarize clusters or topics
|
|
153
|
+
- **Establish clear hierarchies**: Organize nodes in logical parent-child relationships
|
|
154
|
+
- **Create shortcut relationships**: Add direct paths for frequently accessed connections
|
|
155
|
+
- **Implement time-based organization**: Group nodes by temporal relevance
|
|
156
|
+
|
|
157
|
+
4. **Validation and Testing**: After reorganization:
|
|
158
|
+
- Verify all critical paths remain intact
|
|
159
|
+
- Test common query patterns for performance
|
|
160
|
+
- Ensure no data loss occurred during restructuring
|
|
161
|
+
- Document changes made for future reference
|
|
162
|
+
|
|
163
|
+
**Optimization Strategies by Goal:**
|
|
164
|
+
|
|
165
|
+
**Performance Focus:**
|
|
166
|
+
- Minimize query traversal depth
|
|
167
|
+
- Balance node degree distribution
|
|
168
|
+
- Create efficient index structures
|
|
169
|
+
- Reduce redundant relationships
|
|
170
|
+
|
|
171
|
+
**Usability Focus:**
|
|
172
|
+
- Improve semantic organization
|
|
173
|
+
- Add descriptive metadata
|
|
174
|
+
- Create intuitive navigation paths
|
|
175
|
+
- Enhance discoverability
|
|
176
|
+
|
|
177
|
+
**Balanced Approach:**
|
|
178
|
+
- Apply moderate optimizations from both areas
|
|
179
|
+
- Prioritize changes with highest impact/effort ratio
|
|
180
|
+
|
|
181
|
+
**Analysis Queries to Run:**
|
|
182
|
+
\`\`\`cypher
|
|
183
|
+
// Find high-degree nodes
|
|
184
|
+
MATCH (n)
|
|
185
|
+
WITH n, size((n)--()) as degree
|
|
186
|
+
WHERE degree > 20
|
|
187
|
+
RETURN n, degree ORDER BY degree DESC LIMIT 10
|
|
188
|
+
|
|
189
|
+
// Find disconnected components
|
|
190
|
+
MATCH (n)
|
|
191
|
+
WHERE NOT (n)--()
|
|
192
|
+
RETURN count(n) as isolated_nodes
|
|
193
|
+
|
|
194
|
+
// Identify potential duplicates
|
|
195
|
+
MATCH (n1), (n2)
|
|
196
|
+
WHERE id(n1) < id(n2)
|
|
197
|
+
AND n1.name = n2.name
|
|
198
|
+
AND n1.type = n2.type
|
|
199
|
+
RETURN n1, n2
|
|
200
|
+
|
|
201
|
+
// Find stale nodes (older than 30 days with no recent connections)
|
|
202
|
+
MATCH (n)
|
|
203
|
+
WHERE n.created_at < datetime() - duration({days: 30})
|
|
204
|
+
AND NOT (n)-[:UPDATED_AT|ACCESSED_AT]-()
|
|
205
|
+
RETURN n LIMIT 20
|
|
206
|
+
\`\`\`
|
|
207
|
+
|
|
208
|
+
**Reorganization Operations:**
|
|
209
|
+
Implement the most impactful optimizations. Prioritize operations that provide the best balance of performance improvement and structural clarity.
|
|
210
|
+
|
|
211
|
+
Begin with analyzing the current graph structure and proceed with the reorganization plan.`;
|
|
212
|
+
return {
|
|
213
|
+
messages: [
|
|
214
|
+
{
|
|
215
|
+
role: "user",
|
|
216
|
+
content: {
|
|
217
|
+
type: "text",
|
|
218
|
+
text: reorganizationMessage
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
]
|
|
222
|
+
};
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
export default function registerAllPrompts(server) {
|
|
226
|
+
registerUserSetupPrompt(server);
|
|
227
|
+
registerMemoryQueryPrompt(server);
|
|
228
|
+
registerGraphReorganizationPrompt(server);
|
|
229
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { falkorDBService } from '../services/falkordb.service.js';
|
|
2
|
+
import { logger } from '../services/logger.service.js';
|
|
3
|
+
export default function registerAllResources(server) {
|
|
4
|
+
// Register graph_list resource
|
|
5
|
+
server.registerResource("graph_list", "graph://listing", {
|
|
6
|
+
title: "List Graphs",
|
|
7
|
+
description: "List all graphs in the database",
|
|
8
|
+
mimeType: "text/plain",
|
|
9
|
+
}, async (uri) => {
|
|
10
|
+
try {
|
|
11
|
+
const graphNames = await falkorDBService.listGraphs();
|
|
12
|
+
const markdownList = graphNames.map(name => `- ${name}`).join('\n');
|
|
13
|
+
await logger.debug('Graph list resource accessed', { count: graphNames.length });
|
|
14
|
+
return {
|
|
15
|
+
contents: [{
|
|
16
|
+
uri: uri.href,
|
|
17
|
+
text: markdownList,
|
|
18
|
+
}]
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
await logger.error('Failed to fetch graph list resource', error instanceof Error ? error : new Error(String(error)));
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|