vesper-memory 0.5.1 → 0.5.3

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.
Files changed (92) hide show
  1. package/README.md +85 -18
  2. package/config/claude-rules/memory-discipline.md +39 -0
  3. package/config/claude-rules/vesper.md +218 -0
  4. package/dist/cli.js +59 -0
  5. package/dist/cli.js.map +1 -1
  6. package/dist/router/smart-router.d.ts +9 -3
  7. package/dist/router/smart-router.d.ts.map +1 -1
  8. package/dist/router/smart-router.js +236 -57
  9. package/dist/router/smart-router.js.map +1 -1
  10. package/dist/server.js +127 -2
  11. package/dist/server.js.map +1 -1
  12. package/dist/utils/validation.d.ts +12 -0
  13. package/dist/utils/validation.d.ts.map +1 -1
  14. package/dist/utils/validation.js +13 -0
  15. package/dist/utils/validation.js.map +1 -1
  16. package/package.json +1 -2
  17. package/dist/cli 2.js +0 -583
  18. package/dist/cli.d 2.ts +0 -8
  19. package/dist/cli.d.ts 2.map +0 -1
  20. package/dist/cli.js 2.map +0 -1
  21. package/dist/config 2/rate-limits.d.ts +0 -76
  22. package/dist/config 2/rate-limits.d.ts.map +0 -1
  23. package/dist/config 2/rate-limits.js +0 -161
  24. package/dist/config 2/rate-limits.js.map +0 -1
  25. package/dist/consolidation 2/pipeline.d.ts +0 -52
  26. package/dist/consolidation 2/pipeline.d.ts.map +0 -1
  27. package/dist/consolidation 2/pipeline.js +0 -193
  28. package/dist/consolidation 2/pipeline.js.map +0 -1
  29. package/dist/embeddings 2/client.d.ts +0 -94
  30. package/dist/embeddings 2/client.d.ts.map +0 -1
  31. package/dist/embeddings 2/client.js +0 -144
  32. package/dist/embeddings 2/client.js.map +0 -1
  33. package/dist/memory-layers 2/semantic-memory.d.ts +0 -33
  34. package/dist/memory-layers 2/semantic-memory.d.ts.map +0 -1
  35. package/dist/memory-layers 2/semantic-memory.js +0 -114
  36. package/dist/memory-layers 2/semantic-memory.js.map +0 -1
  37. package/dist/memory-layers 2/skill-library.d.ts +0 -25
  38. package/dist/memory-layers 2/skill-library.d.ts.map +0 -1
  39. package/dist/memory-layers 2/skill-library.js +0 -54
  40. package/dist/memory-layers 2/skill-library.js.map +0 -1
  41. package/dist/memory-layers 2/working-memory.d.ts +0 -57
  42. package/dist/memory-layers 2/working-memory.d.ts.map +0 -1
  43. package/dist/memory-layers 2/working-memory.js +0 -153
  44. package/dist/memory-layers 2/working-memory.js.map +0 -1
  45. package/dist/middleware 2/auth.d.ts +0 -287
  46. package/dist/middleware 2/auth.d.ts.map +0 -1
  47. package/dist/middleware 2/auth.js +0 -610
  48. package/dist/middleware 2/auth.js.map +0 -1
  49. package/dist/monitoring 2/health.d.ts +0 -241
  50. package/dist/monitoring 2/health.d.ts.map +0 -1
  51. package/dist/monitoring 2/health.js +0 -649
  52. package/dist/monitoring 2/health.js.map +0 -1
  53. package/dist/monitoring 2/metrics.d.ts +0 -153
  54. package/dist/monitoring 2/metrics.d.ts.map +0 -1
  55. package/dist/monitoring 2/metrics.js +0 -349
  56. package/dist/monitoring 2/metrics.js.map +0 -1
  57. package/dist/retrieval 2/hybrid-search.d.ts +0 -186
  58. package/dist/retrieval 2/hybrid-search.d.ts.map +0 -1
  59. package/dist/retrieval 2/hybrid-search.js +0 -327
  60. package/dist/retrieval 2/hybrid-search.js.map +0 -1
  61. package/dist/router 2/smart-router.d.ts +0 -111
  62. package/dist/router 2/smart-router.d.ts.map +0 -1
  63. package/dist/router 2/smart-router.js +0 -383
  64. package/dist/router 2/smart-router.js.map +0 -1
  65. package/dist/security 2/rate-limit-middleware.d.ts +0 -119
  66. package/dist/security 2/rate-limit-middleware.d.ts.map +0 -1
  67. package/dist/security 2/rate-limit-middleware.js +0 -193
  68. package/dist/security 2/rate-limit-middleware.js.map +0 -1
  69. package/dist/server 2.js +0 -861
  70. package/dist/server-wrapper 2.js +0 -170
  71. package/dist/server-wrapper.d 2.ts +0 -12
  72. package/dist/server-wrapper.d.ts 2.map +0 -1
  73. package/dist/server-wrapper.js 2.map +0 -1
  74. package/dist/server.d 2.ts +0 -14
  75. package/dist/server.d.ts 2.map +0 -1
  76. package/dist/server.js 2.map +0 -1
  77. package/dist/synthesis 2/conflict-detector.d.ts +0 -43
  78. package/dist/synthesis 2/conflict-detector.d.ts.map +0 -1
  79. package/dist/synthesis 2/conflict-detector.js +0 -125
  80. package/dist/synthesis 2/conflict-detector.js.map +0 -1
  81. package/dist/utils 2/monitoring.d.ts +0 -25
  82. package/dist/utils 2/monitoring.d.ts.map +0 -1
  83. package/dist/utils 2/monitoring.js +0 -41
  84. package/dist/utils 2/monitoring.js.map +0 -1
  85. package/dist/utils 2/rate-limiter.d.ts +0 -97
  86. package/dist/utils 2/rate-limiter.d.ts.map +0 -1
  87. package/dist/utils 2/rate-limiter.js +0 -166
  88. package/dist/utils 2/rate-limiter.js.map +0 -1
  89. package/dist/utils 2/validation.d.ts +0 -139
  90. package/dist/utils 2/validation.d.ts.map +0 -1
  91. package/dist/utils 2/validation.js +0 -195
  92. package/dist/utils 2/validation.js.map +0 -1
@@ -1,861 +0,0 @@
1
- /**
2
- * Vesper - AI Memory System
3
- *
4
- * Main entry point for the Model Context Protocol server.
5
- * Implements a neuroscience-inspired memory system for Claude Code agents.
6
- *
7
- * This server provides:
8
- * - store_memory: Add memories to working memory
9
- * - retrieve_memory: Query with smart routing
10
- * - list_recent: Get last 5 conversations
11
- * - get_stats: Return system metrics
12
- */
13
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
14
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
- import { CallToolRequestSchema, ListToolsRequestSchema, McpError, ErrorCode, } from "@modelcontextprotocol/sdk/types.js";
16
- import Redis from "ioredis";
17
- import { createRequire } from "module";
18
- import { randomUUID } from "crypto";
19
- import { validateInput, StoreMemoryInputSchema, RetrieveMemoryInputSchema, ListRecentInputSchema, GetStatsInputSchema, } from "./utils/validation.js";
20
- import { createEmbeddingClient } from "./embeddings/client.js";
21
- import { HybridSearchEngine } from "./retrieval/hybrid-search.js";
22
- import { RateLimitError, createRateLimitMiddleware, } from "./security/rate-limit-middleware.js";
23
- import { getRateLimitConfigFromEnv } from "./config/rate-limits.js";
24
- import { isAuthEnabled, validateApiKeyFormat, extractKeyPrefix, verifyApiKey, hasScope, createAuthCache, } from "./middleware/auth.js";
25
- import { MetricsCollector } from "./monitoring/metrics.js";
26
- // Import better-sqlite3 constructor using createRequire
27
- const require = createRequire(import.meta.url);
28
- const DatabaseConstructor = require("better-sqlite3");
29
- const connections = {};
30
- /**
31
- * Auth cache for performance (avoids repeated bcrypt comparisons)
32
- */
33
- const authCache = createAuthCache({ maxSize: 1000, ttlSeconds: 60 });
34
- /**
35
- * Default user ID for MCP requests when auth is disabled
36
- * SEC-CRIT-001: When AUTH_ENABLED=true, this is replaced with authenticated user ID
37
- */
38
- const DEFAULT_USER_ID = "mcp-default-user";
39
- /**
40
- * Tool definitions for the MCP server
41
- */
42
- const TOOLS = [
43
- {
44
- name: "store_memory",
45
- description: "Add a memory entry to the working memory store. Accepts text, embeddings, and metadata.",
46
- inputSchema: {
47
- type: "object",
48
- properties: {
49
- content: {
50
- type: "string",
51
- description: "The memory content to store",
52
- },
53
- memory_type: {
54
- type: "string",
55
- enum: ["episodic", "semantic", "procedural"],
56
- description: "Type of memory being stored",
57
- },
58
- metadata: {
59
- type: "object",
60
- description: "Additional metadata (timestamp, source, tags, etc.)",
61
- additionalProperties: true,
62
- },
63
- },
64
- required: ["content", "memory_type"],
65
- },
66
- },
67
- {
68
- name: "retrieve_memory",
69
- description: "Query memories using smart routing (semantic search, full-text, graph traversal).",
70
- inputSchema: {
71
- type: "object",
72
- properties: {
73
- query: {
74
- type: "string",
75
- description: "The search query or memory retrieval prompt",
76
- },
77
- memory_types: {
78
- type: "array",
79
- items: { type: "string" },
80
- description: "Filter by memory types (episodic, semantic, procedural). Omit for all types.",
81
- },
82
- max_results: {
83
- type: "number",
84
- description: "Maximum number of results to return (default: 5)",
85
- },
86
- routing_strategy: {
87
- type: "string",
88
- enum: ["fast_path", "semantic", "full_text", "graph"],
89
- description: "Retrieval strategy to use (default: auto-select)",
90
- },
91
- },
92
- required: ["query"],
93
- },
94
- },
95
- {
96
- name: "list_recent",
97
- description: "Get the last N memory entries or conversations for context.",
98
- inputSchema: {
99
- type: "object",
100
- properties: {
101
- limit: {
102
- type: "number",
103
- description: "Number of recent entries to return (default: 5)",
104
- },
105
- memory_type: {
106
- type: "string",
107
- description: "Optional filter by memory type",
108
- },
109
- },
110
- },
111
- },
112
- {
113
- name: "get_stats",
114
- description: "Return system metrics: memory count, storage usage, retrieval latency, etc.",
115
- inputSchema: {
116
- type: "object",
117
- properties: {
118
- detailed: {
119
- type: "boolean",
120
- description: "Include detailed per-layer statistics (default: false)",
121
- },
122
- },
123
- },
124
- },
125
- ];
126
- /**
127
- * Initialize connections to Redis, Qdrant, and SQLite
128
- */
129
- async function initializeConnections() {
130
- try {
131
- console.error("[INFO] Initializing connections to external services...");
132
- // Initialize Redis (optional - used for caching and rate limiting)
133
- try {
134
- connections.redis = new Redis({
135
- host: process.env.REDIS_HOST || "localhost",
136
- port: parseInt(process.env.REDIS_PORT || "6379"),
137
- password: process.env.REDIS_PASSWORD,
138
- retryStrategy: (times) => Math.min(times * 50, 2000),
139
- maxRetriesPerRequest: 3,
140
- connectTimeout: 5000,
141
- });
142
- connections.redis.on("connect", () => {
143
- console.error("[INFO] ✅ Redis connected");
144
- });
145
- connections.redis.on("error", (err) => {
146
- console.error("[WARN] Redis connection error:", err.message);
147
- });
148
- // Test connection with timeout
149
- const pingTimeout = new Promise((_, reject) => setTimeout(() => reject(new Error("Connection timeout")), 5000));
150
- await Promise.race([connections.redis.ping(), pingTimeout]);
151
- console.error("[INFO] ✅ Redis ping successful");
152
- }
153
- catch (err) {
154
- const errorMsg = err instanceof Error ? err.message : String(err);
155
- console.error("\x1b[33m⚠️ Redis not available:\x1b[0m", errorMsg);
156
- console.error("\x1b[34mℹ️ Impact:\x1b[0m Working memory and rate limiting disabled");
157
- console.error("\x1b[32m💡 Solution:\x1b[0m Start Redis with: docker-compose up -d redis");
158
- connections.redis = undefined;
159
- }
160
- // Initialize SQLite (metadata store)
161
- try {
162
- const dbPath = process.env.SQLITE_DB || ":memory:";
163
- connections.sqlite = new DatabaseConstructor(dbPath);
164
- connections.sqlite.pragma("journal_mode = WAL");
165
- // Initialize schema
166
- initializeSqliteSchema();
167
- console.error("[INFO] SQLite database initialized at", dbPath);
168
- }
169
- catch (err) {
170
- console.error("[WARN] SQLite initialization failed:", err instanceof Error ? err.message : String(err));
171
- throw err;
172
- }
173
- // Initialize Embedding Service (optional - graceful degradation)
174
- try {
175
- connections.embeddingClient = createEmbeddingClient({
176
- serviceUrl: process.env.EMBEDDING_SERVICE_URL || "http://localhost:8000",
177
- timeout: 10000,
178
- maxRetries: 2,
179
- });
180
- // Test connection with timeout
181
- const healthTimeout = new Promise((_, reject) => setTimeout(() => reject(new Error("Health check timeout")), 10000));
182
- const health = await Promise.race([
183
- connections.embeddingClient.health(),
184
- healthTimeout,
185
- ]);
186
- console.error(`[INFO] ✅ Embedding service connected: ${health.model} (${health.dimensions}-dim)`);
187
- }
188
- catch (err) {
189
- const errorMsg = err instanceof Error ? err.message : String(err);
190
- console.error("\x1b[33m⚠️ Embedding service not available:\x1b[0m", errorMsg);
191
- console.error("\x1b[34mℹ️ Impact:\x1b[0m Semantic search disabled, text-only search available");
192
- console.error("\x1b[32m💡 Solution:\x1b[0m Start embedding service with: docker-compose up -d embedding");
193
- connections.embeddingClient = undefined;
194
- }
195
- // Initialize Hybrid Search Engine (optional - requires Qdrant)
196
- try {
197
- const qdrantUrl = process.env.QDRANT_URL || "http://localhost:6333";
198
- const qdrantApiKey = process.env.QDRANT_API_KEY;
199
- connections.hybridSearch = new HybridSearchEngine(qdrantUrl, "memory-vectors", 1024, qdrantApiKey);
200
- // Initialize collection with timeout
201
- const initTimeout = new Promise((_, reject) => setTimeout(() => reject(new Error("Initialization timeout")), 10000));
202
- await Promise.race([
203
- connections.hybridSearch.initializeCollection(),
204
- initTimeout,
205
- ]);
206
- console.error("[INFO] ✅ Qdrant hybrid search initialized");
207
- }
208
- catch (err) {
209
- const errorMsg = err instanceof Error ? err.message : String(err);
210
- console.error("\x1b[33m⚠️ Qdrant not available:\x1b[0m", errorMsg);
211
- console.error("\x1b[34mℹ️ Impact:\x1b[0m Vector storage disabled, semantic search limited");
212
- console.error("\x1b[32m💡 Solution:\x1b[0m Start Qdrant with: docker-compose up -d qdrant");
213
- connections.hybridSearch = undefined;
214
- }
215
- // Initialize Rate Limiter (SEC-HIGH-001)
216
- try {
217
- if (connections.redis) {
218
- const rateLimitConfig = getRateLimitConfigFromEnv();
219
- connections.rateLimiter = createRateLimitMiddleware(connections.redis, {
220
- failOpen: rateLimitConfig.failOpen,
221
- tier: rateLimitConfig.defaultTier,
222
- });
223
- console.error(`[INFO] Rate limiter initialized (tier: ${rateLimitConfig.defaultTier}, failOpen: ${rateLimitConfig.failOpen})`);
224
- }
225
- else {
226
- console.error("[WARN] Rate limiter not initialized (Redis required for rate limiting)");
227
- }
228
- }
229
- catch (err) {
230
- console.error("[WARN] Rate limiter initialization failed:", err instanceof Error ? err.message : String(err));
231
- connections.rateLimiter = undefined;
232
- }
233
- // Initialize Metrics Collector (HIGH-001)
234
- try {
235
- connections.metricsCollector = new MetricsCollector({
236
- prefix: "mcp",
237
- collectDefaultMetrics: true,
238
- requireAuth: process.env.METRICS_AUTH_ENABLED === "true",
239
- authToken: process.env.METRICS_AUTH_TOKEN,
240
- });
241
- console.error("[INFO] ✅ Metrics collector initialized");
242
- }
243
- catch (err) {
244
- console.error("[WARN] Metrics collector initialization failed:", err instanceof Error ? err.message : String(err));
245
- connections.metricsCollector = undefined;
246
- }
247
- // Print service status summary
248
- console.error("\n" + "=".repeat(60));
249
- console.error("📊 Vesper Service Status:");
250
- console.error("=".repeat(60));
251
- console.error(`SQLite: ${connections.sqlite ? '\x1b[32m✓ Ready\x1b[0m' : '\x1b[31m✗ Failed\x1b[0m'} (required)`);
252
- console.error(`Redis: ${connections.redis ? '\x1b[32m✓ Ready\x1b[0m' : '\x1b[33m⚠ Disabled\x1b[0m'} (working memory, rate limiting)`);
253
- console.error(`Embedding: ${connections.embeddingClient ? '\x1b[32m✓ Ready\x1b[0m' : '\x1b[33m⚠ Disabled\x1b[0m'} (semantic search)`);
254
- console.error(`Qdrant: ${connections.hybridSearch ? '\x1b[32m✓ Ready\x1b[0m' : '\x1b[33m⚠ Disabled\x1b[0m'} (vector storage)`);
255
- console.error(`Rate Limiter: ${connections.rateLimiter ? '\x1b[32m✓ Ready\x1b[0m' : '\x1b[33m⚠ Disabled\x1b[0m'} (requires Redis)`);
256
- console.error(`Metrics: ${connections.metricsCollector ? '\x1b[32m✓ Ready\x1b[0m' : '\x1b[33m⚠ Disabled\x1b[0m'} (monitoring)`);
257
- console.error("=".repeat(60));
258
- // Show degraded mode warning if any optional services are down
259
- const optionalServicesDown = !connections.redis || !connections.embeddingClient || !connections.hybridSearch;
260
- if (optionalServicesDown) {
261
- console.error("\x1b[33m⚠️ Running in degraded mode\x1b[0m");
262
- console.error("\x1b[32m💡 Start all services:\x1b[0m docker-compose up -d redis qdrant embedding");
263
- console.error("=".repeat(60));
264
- }
265
- console.error("");
266
- }
267
- catch (err) {
268
- console.error("[ERROR] Failed to initialize connections:", err instanceof Error ? err.message : String(err));
269
- throw err;
270
- }
271
- }
272
- /**
273
- * Initialize SQLite schema for memory storage
274
- */
275
- function initializeSqliteSchema() {
276
- if (!connections.sqlite) {
277
- throw new Error("SQLite not initialized");
278
- }
279
- // Create memories table
280
- connections.sqlite.exec(`
281
- CREATE TABLE IF NOT EXISTS memories (
282
- id TEXT PRIMARY KEY,
283
- content TEXT NOT NULL,
284
- memory_type TEXT NOT NULL,
285
- created_at INTEGER NOT NULL,
286
- updated_at INTEGER NOT NULL,
287
- metadata TEXT
288
- );
289
-
290
- CREATE INDEX IF NOT EXISTS idx_memory_type ON memories(memory_type);
291
- CREATE INDEX IF NOT EXISTS idx_created_at ON memories(created_at DESC);
292
-
293
- CREATE TABLE IF NOT EXISTS stats (
294
- key TEXT PRIMARY KEY,
295
- value TEXT,
296
- updated_at INTEGER
297
- );
298
- `);
299
- }
300
- /**
301
- * Handle store_memory tool call
302
- */
303
- async function handleStoreMemory(input) {
304
- if (!connections.sqlite) {
305
- throw new McpError(ErrorCode.InternalError, "SQLite database not available");
306
- }
307
- try {
308
- // Validate input (SEC-006)
309
- const validatedInput = validateInput(StoreMemoryInputSchema, input);
310
- // Use cryptographically secure random UUID instead of Math.random()
311
- const id = `mem_${randomUUID()}`;
312
- const now = Date.now();
313
- // Generate embedding if service is available
314
- let embedding;
315
- if (connections.embeddingClient) {
316
- try {
317
- embedding = await connections.embeddingClient.embed(validatedInput.content);
318
- console.error(`[INFO] Generated embedding for memory ${id}`);
319
- }
320
- catch (err) {
321
- console.error("[WARN] Failed to generate embedding (continuing without):", err instanceof Error ? err.message : String(err));
322
- }
323
- }
324
- // Store in SQLite
325
- const stmt = connections.sqlite.prepare(`
326
- INSERT INTO memories (id, content, memory_type, created_at, updated_at, metadata)
327
- VALUES (?, ?, ?, ?, ?, ?)
328
- `);
329
- stmt.run(id, validatedInput.content, validatedInput.memory_type, now, now, JSON.stringify(validatedInput.metadata || {}));
330
- // Store embedding in Qdrant if available
331
- if (embedding && connections.hybridSearch) {
332
- try {
333
- await connections.hybridSearch.upsertMemory(id, embedding, {
334
- content: validatedInput.content,
335
- memory_type: validatedInput.memory_type,
336
- created_at: now,
337
- ...validatedInput.metadata,
338
- });
339
- console.error(`[INFO] Stored embedding in Qdrant for ${id}`);
340
- }
341
- catch (err) {
342
- console.error("[WARN] Failed to store embedding in Qdrant:", err instanceof Error ? err.message : String(err));
343
- }
344
- }
345
- console.error(`[INFO] Memory stored: ${id} (type: ${validatedInput.memory_type}, length: ${validatedInput.content.length}, embedding: ${embedding ? "yes" : "no"})`);
346
- return {
347
- success: true,
348
- memory_id: id,
349
- timestamp: now,
350
- message: `Memory stored successfully (${validatedInput.memory_type})`,
351
- has_embedding: !!embedding,
352
- };
353
- }
354
- catch (err) {
355
- throw new McpError(ErrorCode.InternalError, `Failed to store memory: ${err instanceof Error ? err.message : String(err)}`);
356
- }
357
- }
358
- /**
359
- * Handle retrieve_memory tool call
360
- */
361
- async function handleRetrieveMemory(input) {
362
- if (!connections.sqlite) {
363
- throw new McpError(ErrorCode.InternalError, "SQLite database not available");
364
- }
365
- try {
366
- // Validate input (SEC-006)
367
- const validatedInput = validateInput(RetrieveMemoryInputSchema, input);
368
- const maxResults = validatedInput.max_results || 5;
369
- // Try semantic search if embedding service is available
370
- if (connections.embeddingClient && connections.hybridSearch) {
371
- try {
372
- // Generate query embedding
373
- const queryEmbedding = await connections.embeddingClient.embed(validatedInput.query);
374
- // Perform hybrid search
375
- const searchResults = await connections.hybridSearch.hybridSearch(queryEmbedding, maxResults);
376
- console.error(`[INFO] Semantic search: query="${validatedInput.query}", results=${searchResults.length}`);
377
- return {
378
- success: true,
379
- query: validatedInput.query,
380
- routing_strategy: "semantic",
381
- results: searchResults.map((r) => ({
382
- id: r.id,
383
- content: r.payload?.content,
384
- memory_type: r.payload?.memory_type,
385
- created_at: r.payload?.created_at,
386
- similarity_score: r.fusedScore,
387
- rank: r.rank,
388
- metadata: r.payload,
389
- })),
390
- count: searchResults.length,
391
- };
392
- }
393
- catch (err) {
394
- console.error("[WARN] Semantic search failed, falling back to text search:", err instanceof Error ? err.message : String(err));
395
- }
396
- }
397
- // Fallback: SQL-based text search
398
- let query = "SELECT id, content, memory_type, created_at, metadata FROM memories";
399
- const params = [];
400
- // Add type filter if specified
401
- if (validatedInput.memory_types && validatedInput.memory_types.length > 0) {
402
- const placeholders = validatedInput.memory_types.map(() => "?").join(",");
403
- query += ` WHERE memory_type IN (${placeholders})`;
404
- params.push(...validatedInput.memory_types);
405
- }
406
- query += " ORDER BY created_at DESC LIMIT ?";
407
- params.push(maxResults);
408
- const stmt = connections.sqlite.prepare(query);
409
- const results = stmt.all(...params);
410
- console.error(`[INFO] Text search (fallback): query="${validatedInput.query}", results=${results.length}`);
411
- return {
412
- success: true,
413
- query: validatedInput.query,
414
- routing_strategy: "text_fallback",
415
- results: results.map((r) => ({
416
- id: r.id,
417
- content: r.content,
418
- memory_type: r.memory_type,
419
- created_at: r.created_at,
420
- metadata: JSON.parse(r.metadata || "{}"),
421
- })),
422
- count: results.length,
423
- };
424
- }
425
- catch (err) {
426
- throw new McpError(ErrorCode.InternalError, `Failed to retrieve memory: ${err instanceof Error ? err.message : String(err)}`);
427
- }
428
- }
429
- /**
430
- * Handle list_recent tool call
431
- */
432
- async function handleListRecent(input) {
433
- if (!connections.sqlite) {
434
- throw new McpError(ErrorCode.InternalError, "SQLite database not available");
435
- }
436
- try {
437
- // Validate input (SEC-006)
438
- const validatedInput = validateInput(ListRecentInputSchema, input);
439
- const limit = validatedInput.limit || 5;
440
- let query = "SELECT id, content, memory_type, created_at FROM memories";
441
- const params = [];
442
- if (validatedInput.memory_type) {
443
- query += " WHERE memory_type = ?";
444
- params.push(validatedInput.memory_type);
445
- }
446
- query += " ORDER BY created_at DESC LIMIT ?";
447
- params.push(limit);
448
- const stmt = connections.sqlite.prepare(query);
449
- const results = stmt.all(...params);
450
- console.error(`[INFO] Listed ${results.length} recent memories (limit=${limit}, type=${validatedInput.memory_type || "all"})`);
451
- return {
452
- success: true,
453
- limit,
454
- memory_type_filter: validatedInput.memory_type || null,
455
- entries: results.map((r) => ({
456
- id: r.id,
457
- content: r.content,
458
- memory_type: r.memory_type,
459
- created_at: r.created_at,
460
- })),
461
- count: results.length,
462
- };
463
- }
464
- catch (err) {
465
- throw new McpError(ErrorCode.InternalError, `Failed to list recent memories: ${err instanceof Error ? err.message : String(err)}`);
466
- }
467
- }
468
- /**
469
- * Handle get_stats tool call
470
- */
471
- async function handleGetStats(input) {
472
- if (!connections.sqlite) {
473
- throw new McpError(ErrorCode.InternalError, "SQLite database not available");
474
- }
475
- try {
476
- // Validate input (SEC-006)
477
- const validatedInput = validateInput(GetStatsInputSchema, input);
478
- const countStmt = connections.sqlite.prepare("SELECT COUNT(*) as count FROM memories");
479
- const countResult = countStmt.get();
480
- const totalMemories = countResult.count;
481
- const typeStmt = connections.sqlite.prepare(`
482
- SELECT memory_type, COUNT(*) as count FROM memories GROUP BY memory_type
483
- `);
484
- const typeResults = typeStmt.all();
485
- const stats = {
486
- success: true,
487
- timestamp: Date.now(),
488
- total_memories: totalMemories,
489
- redis_connected: connections.redis ? true : false,
490
- sqlite_connected: connections.sqlite ? true : false,
491
- };
492
- // Add memory type breakdown
493
- const memoryTypeBreakdown = {};
494
- for (const row of typeResults) {
495
- memoryTypeBreakdown[row.memory_type] = row.count;
496
- }
497
- stats.memory_type_breakdown = memoryTypeBreakdown;
498
- if (validatedInput.detailed) {
499
- // Add detailed statistics
500
- stats.estimated_storage_bytes = totalMemories * 500; // Rough estimate
501
- stats.retrieval_latency_ms = {
502
- p50: 15,
503
- p95: 45,
504
- p99: 120,
505
- };
506
- stats.cache_hit_rate = connections.redis ? 0.72 : null;
507
- }
508
- console.error(`[INFO] Stats retrieved: ${totalMemories} total memories`);
509
- return stats;
510
- }
511
- catch (err) {
512
- throw new McpError(ErrorCode.InternalError, `Failed to retrieve stats: ${err instanceof Error ? err.message : String(err)}`);
513
- }
514
- }
515
- /**
516
- * Authenticate an MCP request
517
- *
518
- * SEC-CRIT-001: Implements API key authentication
519
- *
520
- * @param apiKey - API key from request metadata
521
- * @param toolName - Name of the tool being called
522
- * @param ip - Client IP address (if available)
523
- * @returns Authentication context
524
- */
525
- async function authenticateMcpRequest(apiKey, toolName, _ip = "127.0.0.1") {
526
- // Check if auth is enabled
527
- if (!isAuthEnabled()) {
528
- console.error("[INFO] Auth disabled, using default user");
529
- return {
530
- userId: DEFAULT_USER_ID,
531
- keyId: "none",
532
- tier: "standard",
533
- scopes: ["*"],
534
- authenticated: false,
535
- };
536
- }
537
- // Require API key when auth is enabled
538
- if (!apiKey) {
539
- console.error("[WARN] Auth failed: No API key provided");
540
- // Track failed auth attempt (HIGH-001)
541
- if (connections.metricsCollector) {
542
- connections.metricsCollector.incrementAuthAttempts("failed");
543
- }
544
- throw new McpError(ErrorCode.InvalidRequest, "API key required");
545
- }
546
- // Validate format
547
- if (!validateApiKeyFormat(apiKey)) {
548
- console.error("[WARN] Auth failed: Invalid API key format");
549
- // Track failed auth attempt (HIGH-001)
550
- if (connections.metricsCollector) {
551
- connections.metricsCollector.incrementAuthAttempts("failed");
552
- }
553
- throw new McpError(ErrorCode.InvalidRequest, "Invalid API key format");
554
- }
555
- // Extract prefix for lookup
556
- const prefix = extractKeyPrefix(apiKey);
557
- if (!prefix) {
558
- // Track failed auth attempt (HIGH-001)
559
- if (connections.metricsCollector) {
560
- connections.metricsCollector.incrementAuthAttempts("failed");
561
- }
562
- throw new McpError(ErrorCode.InvalidRequest, "Invalid API key");
563
- }
564
- // Check cache first
565
- const cached = authCache.get(prefix);
566
- if (cached) {
567
- console.error(`[INFO] Auth cache hit for key prefix ${prefix.slice(0, 4)}...`);
568
- // Track successful auth (cached) (HIGH-001)
569
- if (connections.metricsCollector) {
570
- connections.metricsCollector.incrementAuthAttempts("success");
571
- }
572
- return {
573
- userId: cached.userId,
574
- keyId: prefix,
575
- tier: cached.tier,
576
- scopes: ["*"], // Cache doesn't store scopes, assume full access
577
- authenticated: true,
578
- };
579
- }
580
- // In production, this would query PostgreSQL
581
- // For now, we use an environment variable to validate a single key
582
- const validKeyHash = process.env.MCP_API_KEY_HASH;
583
- if (!validKeyHash) {
584
- console.error("[WARN] No MCP_API_KEY_HASH configured, rejecting request");
585
- // Track failed auth attempt (HIGH-001)
586
- if (connections.metricsCollector) {
587
- connections.metricsCollector.incrementAuthAttempts("failed");
588
- }
589
- throw new McpError(ErrorCode.InternalError, "Authentication not configured");
590
- }
591
- // Verify the key
592
- const isValid = await verifyApiKey(apiKey, validKeyHash);
593
- if (!isValid) {
594
- console.error(`[WARN] Auth failed: Invalid API key (prefix: ${prefix.slice(0, 4)}...)`);
595
- // Track failed auth attempt (HIGH-001)
596
- if (connections.metricsCollector) {
597
- connections.metricsCollector.incrementAuthAttempts("failed");
598
- }
599
- throw new McpError(ErrorCode.InvalidRequest, "Invalid API key");
600
- }
601
- // Key is valid - cache the result
602
- const authContext = {
603
- userId: process.env.MCP_API_KEY_USER_ID || "authenticated-user",
604
- keyId: prefix,
605
- tier: process.env.MCP_API_KEY_TIER || "standard",
606
- scopes: (process.env.MCP_API_KEY_SCOPES || "*").split(","),
607
- authenticated: true,
608
- };
609
- // Check scope
610
- if (!hasScope(authContext.scopes, toolName)) {
611
- console.error(`[WARN] Auth failed: Missing scope ${toolName}`);
612
- // Track failed auth attempt (HIGH-001)
613
- if (connections.metricsCollector) {
614
- connections.metricsCollector.incrementAuthAttempts("failed");
615
- }
616
- throw new McpError(ErrorCode.InvalidRequest, "Insufficient permissions");
617
- }
618
- // Cache the result
619
- authCache.set(prefix, { userId: authContext.userId, tier: authContext.tier });
620
- console.error(`[INFO] Auth success: user=${authContext.userId}, tier=${authContext.tier}`);
621
- // Track successful auth attempt (HIGH-001)
622
- if (connections.metricsCollector) {
623
- connections.metricsCollector.incrementAuthAttempts("success");
624
- }
625
- return authContext;
626
- }
627
- /**
628
- * Process tool calls with authentication and rate limiting
629
- *
630
- * SEC-CRIT-001: Integrates API key authentication
631
- * SEC-HIGH-001: Integrates rate limiting into all MCP tool handlers
632
- * SEC-HIGH-002: Implements fail-closed behavior when Redis unavailable
633
- */
634
- async function processTool(name, input, authContext) {
635
- const userId = authContext.userId;
636
- console.error(`[INFO] Processing tool: ${name} for user: ${userId}`);
637
- // Check if Vesper is disabled (pass-through mode)
638
- const isEnabled = process.env.VESPER_ENABLED !== 'false';
639
- if (!isEnabled) {
640
- console.error('[INFO] Vesper is disabled (pass-through mode)');
641
- return {
642
- result: {
643
- success: true,
644
- disabled: true,
645
- message: "Vesper memory system is currently disabled (pass-through mode). Enable with 'vesper enable'.",
646
- },
647
- };
648
- }
649
- // Start metrics timer (HIGH-001)
650
- const startTime = Date.now();
651
- try {
652
- // Apply rate limiting if available (SEC-HIGH-001)
653
- let rateLimitResult;
654
- if (connections.rateLimiter) {
655
- try {
656
- rateLimitResult = await connections.rateLimiter.checkRateLimit(userId, name);
657
- if (!rateLimitResult.allowed) {
658
- // Track rate limit hit in metrics (HIGH-001)
659
- if (connections.metricsCollector) {
660
- connections.metricsCollector.incrementRateLimitHits(userId, name);
661
- }
662
- // Return 429 rate limit error
663
- const retryAfter = rateLimitResult.headers["Retry-After"] || "60";
664
- throw new RateLimitError(`Rate limit exceeded for ${name}. Limit: ${rateLimitResult.headers["X-RateLimit-Limit"]}/min. ` +
665
- `Reset in ${retryAfter} seconds.`, 429, parseInt(retryAfter));
666
- }
667
- console.error(`[INFO] Rate limit check passed for ${name}: ${rateLimitResult.remaining} remaining`);
668
- }
669
- catch (err) {
670
- if (err instanceof RateLimitError) {
671
- throw err; // Re-throw rate limit errors
672
- }
673
- // SEC-HIGH-002: Fail closed when Redis unavailable
674
- console.error("[WARN] Rate limit check failed:", err instanceof Error ? err.message : String(err));
675
- throw new McpError(ErrorCode.InternalError, "Service temporarily unavailable. Please try again in a few minutes.");
676
- }
677
- }
678
- // Process the tool
679
- let result;
680
- switch (name) {
681
- case "store_memory":
682
- result = await handleStoreMemory(input);
683
- break;
684
- case "retrieve_memory":
685
- result = await handleRetrieveMemory(input);
686
- break;
687
- case "list_recent":
688
- result = await handleListRecent(input);
689
- break;
690
- case "get_stats":
691
- result = await handleGetStats(input);
692
- break;
693
- default:
694
- throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
695
- }
696
- // Track successful request in metrics (HIGH-001)
697
- if (connections.metricsCollector) {
698
- const duration = (Date.now() - startTime) / 1000; // Convert to seconds
699
- connections.metricsCollector.incrementRequests(name, "success");
700
- connections.metricsCollector.recordLatency(name, duration);
701
- }
702
- return {
703
- result,
704
- rateLimitHeaders: rateLimitResult?.headers,
705
- };
706
- }
707
- catch (err) {
708
- // Track failed request in metrics (HIGH-001)
709
- if (connections.metricsCollector) {
710
- const duration = (Date.now() - startTime) / 1000; // Convert to seconds
711
- connections.metricsCollector.incrementRequests(name, "error");
712
- connections.metricsCollector.recordLatency(name, duration);
713
- // Track error type
714
- if (err instanceof RateLimitError) {
715
- connections.metricsCollector.incrementErrors("rate_limit");
716
- }
717
- else if (err instanceof McpError) {
718
- connections.metricsCollector.incrementErrors("mcp_error");
719
- }
720
- else {
721
- connections.metricsCollector.incrementErrors("unknown");
722
- }
723
- }
724
- // Re-throw the error
725
- throw err;
726
- }
727
- }
728
- /**
729
- * Main server function
730
- */
731
- async function main() {
732
- console.error("[INFO] Starting Vesper...");
733
- // Initialize connections
734
- await initializeConnections();
735
- // Create MCP server
736
- const server = new Server({
737
- name: "vesper",
738
- version: "0.1.0",
739
- }, {
740
- capabilities: {
741
- tools: {},
742
- },
743
- });
744
- // Register tools listing
745
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
746
- tools: TOOLS,
747
- }));
748
- // Register tool call handler
749
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
750
- const { name, arguments: args } = request.params;
751
- const requestId = randomUUID();
752
- console.error(`[INFO] Tool call received: ${name} (requestId: ${requestId})`);
753
- try {
754
- // Extract API key from request metadata (SEC-CRIT-001)
755
- // MCP clients can pass API key in _meta.apiKey or via environment
756
- const meta = request.params._meta || {};
757
- const apiKey = meta.apiKey || process.env.MCP_API_KEY;
758
- // Authenticate the request
759
- const authContext = await authenticateMcpRequest(apiKey, name);
760
- // Process the tool with auth context
761
- const { result, rateLimitHeaders } = await processTool(name, args, authContext);
762
- // Include rate limit headers in the response metadata
763
- const responseData = {
764
- ...result,
765
- // Include rate limit info for transparency
766
- _rateLimit: rateLimitHeaders
767
- ? {
768
- limit: rateLimitHeaders["X-RateLimit-Limit"],
769
- remaining: rateLimitHeaders["X-RateLimit-Remaining"],
770
- reset: rateLimitHeaders["X-RateLimit-Reset"],
771
- }
772
- : undefined,
773
- };
774
- return {
775
- content: [
776
- {
777
- type: "text",
778
- text: JSON.stringify(responseData, null, 2),
779
- },
780
- ],
781
- };
782
- }
783
- catch (err) {
784
- // Handle rate limit errors specially (SEC-HIGH-001)
785
- if (err instanceof RateLimitError) {
786
- console.error(`[WARN] Rate limit error for ${name}: ${err.message}`);
787
- return {
788
- content: [
789
- {
790
- type: "text",
791
- text: JSON.stringify({
792
- error: err.message,
793
- code: "RATE_LIMITED",
794
- statusCode: err.statusCode,
795
- retryAfter: err.retryAfter,
796
- tool: name,
797
- requestId,
798
- }, null, 2),
799
- },
800
- ],
801
- isError: true,
802
- };
803
- }
804
- // Handle auth errors (SEC-CRIT-001)
805
- if (err instanceof McpError) {
806
- const isAuthError = err.message.includes("API key") ||
807
- err.message.includes("permission") ||
808
- err.message.includes("Authentication");
809
- if (isAuthError) {
810
- console.error(`[WARN] Auth error for ${name}: ${err.message}`);
811
- return {
812
- content: [
813
- {
814
- type: "text",
815
- text: JSON.stringify({
816
- error: err.message,
817
- code: "UNAUTHENTICATED",
818
- statusCode: 401,
819
- tool: name,
820
- requestId,
821
- }, null, 2),
822
- },
823
- ],
824
- isError: true,
825
- };
826
- }
827
- }
828
- const errorMessage = err instanceof McpError
829
- ? err.message
830
- : err instanceof Error
831
- ? err.message
832
- : String(err);
833
- console.error(`[ERROR] Tool error for ${name}: ${errorMessage}`);
834
- return {
835
- content: [
836
- {
837
- type: "text",
838
- text: JSON.stringify({
839
- error: errorMessage,
840
- tool: name,
841
- requestId,
842
- }, null, 2),
843
- },
844
- ],
845
- isError: true,
846
- };
847
- }
848
- });
849
- // Start stdio-based server
850
- const transport = new StdioServerTransport();
851
- console.error("[INFO] Starting stdio transport...");
852
- await server.connect(transport);
853
- console.error("[INFO] Vesper running on stdio transport");
854
- console.error("[INFO] Available tools: store_memory, retrieve_memory, list_recent, get_stats");
855
- }
856
- // Run the server
857
- main().catch((err) => {
858
- console.error("[FATAL] Server error:", err instanceof Error ? err.message : String(err));
859
- process.exit(1);
860
- });
861
- //# sourceMappingURL=server.js.map