claude-recall 0.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.
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RateLimiter = void 0;
4
+ class RateLimiter {
5
+ constructor(logger, config = {}) {
6
+ this.logger = logger;
7
+ this.requests = new Map();
8
+ this.cleanupInterval = null;
9
+ this.windowMs = config.windowMs || 60000; // 1 minute default
10
+ this.maxRequests = config.maxRequests || 100; // 100 requests default
11
+ this.skipSuccessfulRequests = config.skipSuccessfulRequests || false;
12
+ // Clean up old entries periodically
13
+ this.cleanupInterval = setInterval(() => {
14
+ this.cleanup();
15
+ }, this.windowMs);
16
+ this.logger.info('RateLimiter', 'Rate limiter initialized', {
17
+ windowMs: this.windowMs,
18
+ maxRequests: this.maxRequests,
19
+ skipSuccessfulRequests: this.skipSuccessfulRequests
20
+ });
21
+ }
22
+ async checkLimit(sessionId) {
23
+ const now = Date.now();
24
+ const requests = this.requests.get(sessionId) || [];
25
+ // Remove old requests outside window
26
+ const validRequests = requests.filter(time => now - time < this.windowMs);
27
+ if (validRequests.length >= this.maxRequests) {
28
+ this.logger.warn('RateLimiter', 'Rate limit exceeded', {
29
+ sessionId,
30
+ requests: validRequests.length,
31
+ window: this.windowMs,
32
+ maxRequests: this.maxRequests
33
+ });
34
+ return false;
35
+ }
36
+ // Don't add to count yet - wait for recordRequest
37
+ this.requests.set(sessionId, validRequests);
38
+ return true;
39
+ }
40
+ recordRequest(sessionId, successful = true) {
41
+ if (this.skipSuccessfulRequests && successful) {
42
+ return; // Don't count successful requests if configured
43
+ }
44
+ const now = Date.now();
45
+ const requests = this.requests.get(sessionId) || [];
46
+ // Add new request
47
+ requests.push(now);
48
+ // Keep only requests within window
49
+ const validRequests = requests.filter(time => now - time < this.windowMs);
50
+ this.requests.set(sessionId, validRequests);
51
+ this.logger.debug('RateLimiter', 'Request recorded', {
52
+ sessionId,
53
+ requestCount: validRequests.length,
54
+ successful
55
+ });
56
+ }
57
+ getRemainingRequests(sessionId) {
58
+ const now = Date.now();
59
+ const requests = this.requests.get(sessionId) || [];
60
+ const validRequests = requests.filter(time => now - time < this.windowMs);
61
+ return Math.max(0, this.maxRequests - validRequests.length);
62
+ }
63
+ resetLimit(sessionId) {
64
+ this.requests.delete(sessionId);
65
+ this.logger.info('RateLimiter', 'Rate limit reset', { sessionId });
66
+ }
67
+ cleanup() {
68
+ const now = Date.now();
69
+ let cleaned = 0;
70
+ for (const [sessionId, requests] of this.requests) {
71
+ const validRequests = requests.filter(time => now - time < this.windowMs);
72
+ if (validRequests.length === 0) {
73
+ this.requests.delete(sessionId);
74
+ cleaned++;
75
+ }
76
+ else if (validRequests.length < requests.length) {
77
+ this.requests.set(sessionId, validRequests);
78
+ }
79
+ }
80
+ if (cleaned > 0) {
81
+ this.logger.debug('RateLimiter', `Cleaned up ${cleaned} expired session limits`);
82
+ }
83
+ }
84
+ getStats() {
85
+ const now = Date.now();
86
+ const stats = {
87
+ activeSessions: this.requests.size,
88
+ totalRequests: 0,
89
+ topSessions: []
90
+ };
91
+ const sessionRequests = [];
92
+ for (const [sessionId, requests] of this.requests) {
93
+ const validRequests = requests.filter(time => now - time < this.windowMs);
94
+ const count = validRequests.length;
95
+ stats.totalRequests += count;
96
+ sessionRequests.push({ sessionId, requests: count });
97
+ }
98
+ // Get top 5 sessions by request count
99
+ stats.topSessions = sessionRequests
100
+ .sort((a, b) => b.requests - a.requests)
101
+ .slice(0, 5);
102
+ return stats;
103
+ }
104
+ shutdown() {
105
+ if (this.cleanupInterval) {
106
+ clearInterval(this.cleanupInterval);
107
+ this.cleanupInterval = null;
108
+ }
109
+ this.logger.info('RateLimiter', 'Rate limiter shut down', {
110
+ activeSessions: this.requests.size
111
+ });
112
+ }
113
+ }
114
+ exports.RateLimiter = RateLimiter;
@@ -0,0 +1,283 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MCPServer = void 0;
4
+ const stdio_1 = require("./transports/stdio");
5
+ const memory_tools_1 = require("./tools/memory-tools");
6
+ const memory_1 = require("../services/memory");
7
+ const logging_1 = require("../services/logging");
8
+ const session_manager_1 = require("./session-manager");
9
+ const rate_limiter_1 = require("./rate-limiter");
10
+ class MCPServer {
11
+ constructor() {
12
+ this.tools = new Map();
13
+ this.sessions = new Map();
14
+ this.isInitialized = false;
15
+ this.transport = new stdio_1.StdioTransport();
16
+ this.memoryService = memory_1.MemoryService.getInstance();
17
+ this.logger = logging_1.LoggingService.getInstance();
18
+ this.sessionManager = new session_manager_1.SessionManager(this.logger);
19
+ this.rateLimiter = new rate_limiter_1.RateLimiter(this.logger, {
20
+ windowMs: 60000, // 1 minute
21
+ maxRequests: 100, // 100 requests per minute
22
+ skipSuccessfulRequests: false
23
+ });
24
+ this.setupRequestHandlers();
25
+ this.registerTools();
26
+ }
27
+ setupRequestHandlers() {
28
+ this.transport.onRequest(async (request) => {
29
+ try {
30
+ switch (request.method) {
31
+ case 'initialize':
32
+ return this.handleInitialize(request);
33
+ case 'tools/list':
34
+ return this.handleToolsList(request);
35
+ case 'tools/call':
36
+ return this.handleToolCall(request);
37
+ case 'notifications/initialized':
38
+ return this.handleInitialized(request);
39
+ case 'health/check':
40
+ return this.handleHealthCheck(request);
41
+ default:
42
+ return this.createErrorResponse(request.id, -32601, `Method not found: ${request.method}`);
43
+ }
44
+ }
45
+ catch (error) {
46
+ this.logger.logServiceError('MCPServer', 'handleRequest', error, { method: request.method });
47
+ return this.createErrorResponse(request.id, -32603, 'Internal error', { message: error.message });
48
+ }
49
+ });
50
+ }
51
+ registerTools() {
52
+ const memoryTools = new memory_tools_1.MemoryTools(this.memoryService, this.logger);
53
+ const tools = memoryTools.getTools();
54
+ for (const tool of tools) {
55
+ this.tools.set(tool.name, tool);
56
+ }
57
+ this.logger.info('MCPServer', `Registered ${this.tools.size} tools`, {
58
+ tools: Array.from(this.tools.keys())
59
+ });
60
+ }
61
+ async handleInitialize(request) {
62
+ const params = request.params || {};
63
+ this.logger.info('MCPServer', 'Initializing MCP server', params);
64
+ return {
65
+ jsonrpc: "2.0",
66
+ id: request.id,
67
+ result: {
68
+ protocolVersion: "2024-11-05",
69
+ capabilities: {
70
+ tools: {},
71
+ logging: {}
72
+ },
73
+ serverInfo: {
74
+ name: "claude-recall",
75
+ version: "0.2.0"
76
+ }
77
+ }
78
+ };
79
+ }
80
+ async handleInitialized(request) {
81
+ this.isInitialized = true;
82
+ this.logger.info('MCPServer', 'MCP server initialized successfully');
83
+ // Note: initialized is a notification, no response required
84
+ return {
85
+ jsonrpc: "2.0",
86
+ id: request.id,
87
+ result: null
88
+ };
89
+ }
90
+ async handleToolsList(request) {
91
+ const toolList = Array.from(this.tools.values()).map(tool => ({
92
+ name: tool.name,
93
+ description: tool.description,
94
+ inputSchema: tool.inputSchema
95
+ }));
96
+ this.logger.debug('MCPServer', `Listing ${toolList.length} tools`);
97
+ return {
98
+ jsonrpc: "2.0",
99
+ id: request.id,
100
+ result: {
101
+ tools: toolList
102
+ }
103
+ };
104
+ }
105
+ async handleToolCall(request) {
106
+ const { name, arguments: toolArgs } = request.params || {};
107
+ if (!name || typeof name !== 'string') {
108
+ return this.createErrorResponse(request.id, -32602, 'Invalid params: tool name required');
109
+ }
110
+ const tool = this.tools.get(name);
111
+ if (!tool) {
112
+ return this.createErrorResponse(request.id, -32601, `Tool not found: ${name}`);
113
+ }
114
+ const startTime = Date.now();
115
+ try {
116
+ // Create or get session context
117
+ const sessionId = toolArgs?.sessionId || this.generateSessionId();
118
+ // Get or create session
119
+ let session = this.sessionManager.getSession(sessionId);
120
+ if (!session) {
121
+ session = this.sessionManager.createSession(sessionId);
122
+ }
123
+ // Check rate limit
124
+ const withinLimit = await this.rateLimiter.checkLimit(sessionId);
125
+ if (!withinLimit) {
126
+ const remaining = this.rateLimiter.getRemainingRequests(sessionId);
127
+ return this.createErrorResponse(request.id, -32000, // Custom error code for rate limit
128
+ 'Rate limit exceeded', {
129
+ sessionId,
130
+ remainingRequests: remaining,
131
+ windowMs: 60000,
132
+ message: 'Too many requests. Please wait before trying again.'
133
+ });
134
+ }
135
+ // Update session activity
136
+ this.sessionManager.incrementToolCalls(sessionId);
137
+ const context = {
138
+ sessionId,
139
+ timestamp: Date.now(),
140
+ projectId: toolArgs?.projectId
141
+ };
142
+ this.logger.info('MCPServer', `Executing tool: ${name}`, {
143
+ sessionId,
144
+ args: toolArgs,
145
+ toolCallCount: session.toolCalls
146
+ });
147
+ const result = await tool.handler(toolArgs || {}, context);
148
+ // Record successful request for rate limiting
149
+ this.rateLimiter.recordRequest(sessionId, true);
150
+ // Claude-flow pattern: Enhanced response format
151
+ return {
152
+ jsonrpc: "2.0",
153
+ id: request.id,
154
+ result: {
155
+ content: [
156
+ {
157
+ type: "text",
158
+ text: typeof result === 'string' ? result : JSON.stringify(result, null, 2)
159
+ }
160
+ ],
161
+ isError: false,
162
+ metadata: {
163
+ toolName: name,
164
+ duration: Date.now() - startTime,
165
+ sessionId: context.sessionId
166
+ }
167
+ }
168
+ };
169
+ }
170
+ catch (error) {
171
+ this.logger.logServiceError('MCPServer', `tool:${name}`, error, toolArgs);
172
+ // Record failed request for rate limiting
173
+ const sessionId = toolArgs?.sessionId || this.generateSessionId();
174
+ this.rateLimiter.recordRequest(sessionId, false);
175
+ // Claude-flow pattern: Enhanced error response
176
+ return {
177
+ jsonrpc: "2.0",
178
+ id: request.id,
179
+ result: {
180
+ content: [
181
+ {
182
+ type: "text",
183
+ text: `Tool execution failed: ${error.message}`
184
+ }
185
+ ],
186
+ isError: true,
187
+ metadata: {
188
+ toolName: name,
189
+ duration: Date.now() - startTime,
190
+ sessionId: this.generateSessionId(),
191
+ error: {
192
+ message: error.message,
193
+ stack: error.stack
194
+ }
195
+ }
196
+ }
197
+ };
198
+ }
199
+ }
200
+ createErrorResponse(id, code, message, data) {
201
+ return {
202
+ jsonrpc: "2.0",
203
+ id,
204
+ error: {
205
+ code,
206
+ message,
207
+ ...(data && { data })
208
+ }
209
+ };
210
+ }
211
+ generateSessionId() {
212
+ return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
213
+ }
214
+ async handleHealthCheck(request) {
215
+ const rateLimiterStats = this.rateLimiter.getStats();
216
+ const health = {
217
+ status: 'healthy',
218
+ version: '0.2.0',
219
+ uptime: process.uptime(),
220
+ memory: process.memoryUsage(),
221
+ sessions: {
222
+ total: this.sessionManager.getAllSessions().length,
223
+ active: this.sessionManager.getActiveSessionCount()
224
+ },
225
+ toolsRegistered: this.tools.size,
226
+ database: this.memoryService.isConnected() ? 'connected' : 'disconnected',
227
+ rateLimiter: {
228
+ activeSessions: rateLimiterStats.activeSessions,
229
+ totalRequests: rateLimiterStats.totalRequests,
230
+ topSessions: rateLimiterStats.topSessions
231
+ }
232
+ };
233
+ this.logger.info('MCPServer', 'Health check performed', health);
234
+ return {
235
+ jsonrpc: "2.0",
236
+ id: request.id,
237
+ result: health
238
+ };
239
+ }
240
+ async start() {
241
+ try {
242
+ this.logger.info('MCPServer', 'Starting Claude Recall MCP server...');
243
+ await this.transport.start();
244
+ this.logger.info('MCPServer', 'MCP server started successfully');
245
+ }
246
+ catch (error) {
247
+ this.logger.logServiceError('MCPServer', 'start', error);
248
+ throw error;
249
+ }
250
+ }
251
+ async stop() {
252
+ try {
253
+ this.logger.info('MCPServer', 'Stopping MCP server...');
254
+ // Clean up old sessions before shutdown
255
+ this.sessionManager.cleanupOldSessions();
256
+ // Shutdown session manager (persists sessions)
257
+ this.sessionManager.shutdown();
258
+ // Shutdown rate limiter
259
+ this.rateLimiter.shutdown();
260
+ await this.transport.stop();
261
+ this.memoryService.close();
262
+ this.logger.info('MCPServer', 'MCP server stopped');
263
+ }
264
+ catch (error) {
265
+ this.logger.logServiceError('MCPServer', 'stop', error);
266
+ throw error;
267
+ }
268
+ }
269
+ // Graceful shutdown handling
270
+ setupSignalHandlers() {
271
+ process.on('SIGINT', async () => {
272
+ this.logger.info('MCPServer', 'Received SIGINT, shutting down gracefully...');
273
+ await this.stop();
274
+ process.exit(0);
275
+ });
276
+ process.on('SIGTERM', async () => {
277
+ this.logger.info('MCPServer', 'Received SIGTERM, shutting down gracefully...');
278
+ await this.stop();
279
+ process.exit(0);
280
+ });
281
+ }
282
+ }
283
+ exports.MCPServer = MCPServer;
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.SessionManager = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const os = __importStar(require("os"));
40
+ class SessionManager {
41
+ constructor(logger) {
42
+ this.sessions = new Map();
43
+ this.persistInterval = null;
44
+ this.logger = logger;
45
+ this.sessionFile = path.join(os.homedir(), '.claude-recall', 'sessions.json');
46
+ this.ensureDirectoryExists();
47
+ this.loadSessions();
48
+ // Persist sessions periodically
49
+ this.persistInterval = setInterval(() => {
50
+ this.persistSessions();
51
+ }, 30000); // Every 30 seconds
52
+ }
53
+ ensureDirectoryExists() {
54
+ const dir = path.dirname(this.sessionFile);
55
+ if (!fs.existsSync(dir)) {
56
+ fs.mkdirSync(dir, { recursive: true });
57
+ }
58
+ }
59
+ createSession(id) {
60
+ const session = {
61
+ id,
62
+ startTime: Date.now(),
63
+ lastActivity: Date.now(),
64
+ toolCalls: 0,
65
+ memories: []
66
+ };
67
+ this.sessions.set(id, session);
68
+ this.persistSessions();
69
+ this.logger.info('SessionManager', 'Session created', { sessionId: id });
70
+ return session;
71
+ }
72
+ getSession(id) {
73
+ return this.sessions.get(id);
74
+ }
75
+ updateSession(id, update) {
76
+ const session = this.sessions.get(id);
77
+ if (session) {
78
+ Object.assign(session, update, { lastActivity: Date.now() });
79
+ this.persistSessions();
80
+ this.logger.debug('SessionManager', 'Session updated', { sessionId: id, update });
81
+ }
82
+ }
83
+ incrementToolCalls(id) {
84
+ const session = this.sessions.get(id);
85
+ if (session) {
86
+ session.toolCalls++;
87
+ session.lastActivity = Date.now();
88
+ this.persistSessions();
89
+ }
90
+ }
91
+ addMemory(id, memoryId) {
92
+ const session = this.sessions.get(id);
93
+ if (session) {
94
+ session.memories.push(memoryId);
95
+ session.lastActivity = Date.now();
96
+ this.persistSessions();
97
+ }
98
+ }
99
+ loadSessions() {
100
+ try {
101
+ if (fs.existsSync(this.sessionFile)) {
102
+ const data = fs.readFileSync(this.sessionFile, 'utf-8');
103
+ const sessions = JSON.parse(data);
104
+ // Convert array back to Map
105
+ if (Array.isArray(sessions)) {
106
+ sessions.forEach(([id, session]) => {
107
+ this.sessions.set(id, session);
108
+ });
109
+ }
110
+ this.logger.info('SessionManager', `Loaded ${this.sessions.size} sessions from disk`);
111
+ }
112
+ }
113
+ catch (error) {
114
+ this.logger.error('SessionManager', 'Failed to load sessions', error);
115
+ }
116
+ }
117
+ persistSessions() {
118
+ try {
119
+ // Save to disk like claude-flow does
120
+ const data = JSON.stringify(Array.from(this.sessions.entries()), null, 2);
121
+ fs.writeFileSync(this.sessionFile, data);
122
+ this.logger.debug('SessionManager', `Persisted ${this.sessions.size} sessions to disk`);
123
+ }
124
+ catch (error) {
125
+ this.logger.error('SessionManager', 'Failed to persist sessions', error);
126
+ }
127
+ }
128
+ // Clean up old sessions (sessions older than 24 hours with no activity)
129
+ cleanupOldSessions() {
130
+ const now = Date.now();
131
+ const maxAge = 24 * 60 * 60 * 1000; // 24 hours
132
+ let removed = 0;
133
+ for (const [id, session] of this.sessions) {
134
+ if (now - session.lastActivity > maxAge) {
135
+ this.sessions.delete(id);
136
+ removed++;
137
+ }
138
+ }
139
+ if (removed > 0) {
140
+ this.logger.info('SessionManager', `Cleaned up ${removed} old sessions`);
141
+ this.persistSessions();
142
+ }
143
+ }
144
+ getAllSessions() {
145
+ return Array.from(this.sessions.values());
146
+ }
147
+ getActiveSessionCount() {
148
+ const now = Date.now();
149
+ const activeThreshold = 5 * 60 * 1000; // 5 minutes
150
+ return Array.from(this.sessions.values()).filter(session => now - session.lastActivity < activeThreshold).length;
151
+ }
152
+ shutdown() {
153
+ if (this.persistInterval) {
154
+ clearInterval(this.persistInterval);
155
+ this.persistInterval = null;
156
+ }
157
+ this.persistSessions();
158
+ this.logger.info('SessionManager', 'Session manager shut down');
159
+ }
160
+ }
161
+ exports.SessionManager = SessionManager;