vesper-memory 0.3.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.
- package/.env.example +28 -0
- package/CHANGELOG.md +137 -0
- package/LICENSE +21 -0
- package/README.md +665 -0
- package/config/sqlite-schema.sql +413 -0
- package/dist/benchmark/index.d.ts +18 -0
- package/dist/benchmark/index.d.ts.map +1 -0
- package/dist/benchmark/index.js +19 -0
- package/dist/benchmark/index.js.map +1 -0
- package/dist/benchmark/metrics-collector.d.ts +90 -0
- package/dist/benchmark/metrics-collector.d.ts.map +1 -0
- package/dist/benchmark/metrics-collector.js +196 -0
- package/dist/benchmark/metrics-collector.js.map +1 -0
- package/dist/benchmark/report-generator.d.ts +35 -0
- package/dist/benchmark/report-generator.d.ts.map +1 -0
- package/dist/benchmark/report-generator.js +221 -0
- package/dist/benchmark/report-generator.js.map +1 -0
- package/dist/benchmark/runner.d.ts +30 -0
- package/dist/benchmark/runner.d.ts.map +1 -0
- package/dist/benchmark/runner.js +308 -0
- package/dist/benchmark/runner.js.map +1 -0
- package/dist/benchmark/scenarios/context-recall.d.ts +65 -0
- package/dist/benchmark/scenarios/context-recall.d.ts.map +1 -0
- package/dist/benchmark/scenarios/context-recall.js +180 -0
- package/dist/benchmark/scenarios/context-recall.js.map +1 -0
- package/dist/benchmark/scenarios/index.d.ts +34 -0
- package/dist/benchmark/scenarios/index.d.ts.map +1 -0
- package/dist/benchmark/scenarios/index.js +230 -0
- package/dist/benchmark/scenarios/index.js.map +1 -0
- package/dist/benchmark/statistics.d.ts +125 -0
- package/dist/benchmark/statistics.d.ts.map +1 -0
- package/dist/benchmark/statistics.js +520 -0
- package/dist/benchmark/statistics.js.map +1 -0
- package/dist/benchmark/types.d.ts +458 -0
- package/dist/benchmark/types.d.ts.map +1 -0
- package/dist/benchmark/types.js +297 -0
- package/dist/benchmark/types.js.map +1 -0
- package/dist/cli 2.js +583 -0
- package/dist/cli.d 2.ts +8 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.d.ts 2.map +1 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +583 -0
- package/dist/cli.js 2.map +1 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/rate-limits.d.ts +76 -0
- package/dist/config/rate-limits.d.ts.map +1 -0
- package/dist/config/rate-limits.js +161 -0
- package/dist/config/rate-limits.js.map +1 -0
- package/dist/config 2/rate-limits.d.ts +76 -0
- package/dist/config 2/rate-limits.d.ts.map +1 -0
- package/dist/config 2/rate-limits.js +161 -0
- package/dist/config 2/rate-limits.js.map +1 -0
- package/dist/consolidation/pipeline.d.ts +52 -0
- package/dist/consolidation/pipeline.d.ts.map +1 -0
- package/dist/consolidation/pipeline.js +193 -0
- package/dist/consolidation/pipeline.js.map +1 -0
- package/dist/consolidation 2/pipeline.d.ts +52 -0
- package/dist/consolidation 2/pipeline.d.ts.map +1 -0
- package/dist/consolidation 2/pipeline.js +193 -0
- package/dist/consolidation 2/pipeline.js.map +1 -0
- package/dist/embeddings/client.d.ts +95 -0
- package/dist/embeddings/client.d.ts.map +1 -0
- package/dist/embeddings/client.js +162 -0
- package/dist/embeddings/client.js.map +1 -0
- package/dist/embeddings 2/client.d.ts +94 -0
- package/dist/embeddings 2/client.d.ts.map +1 -0
- package/dist/embeddings 2/client.js +144 -0
- package/dist/embeddings 2/client.js.map +1 -0
- package/dist/memory-layers/semantic-memory.d.ts +76 -0
- package/dist/memory-layers/semantic-memory.d.ts.map +1 -0
- package/dist/memory-layers/semantic-memory.js +231 -0
- package/dist/memory-layers/semantic-memory.js.map +1 -0
- package/dist/memory-layers/skill-library.d.ts +25 -0
- package/dist/memory-layers/skill-library.d.ts.map +1 -0
- package/dist/memory-layers/skill-library.js +54 -0
- package/dist/memory-layers/skill-library.js.map +1 -0
- package/dist/memory-layers/working-memory.d.ts +57 -0
- package/dist/memory-layers/working-memory.d.ts.map +1 -0
- package/dist/memory-layers/working-memory.js +153 -0
- package/dist/memory-layers/working-memory.js.map +1 -0
- package/dist/memory-layers 2/semantic-memory.d.ts +33 -0
- package/dist/memory-layers 2/semantic-memory.d.ts.map +1 -0
- package/dist/memory-layers 2/semantic-memory.js +114 -0
- package/dist/memory-layers 2/semantic-memory.js.map +1 -0
- package/dist/memory-layers 2/skill-library.d.ts +25 -0
- package/dist/memory-layers 2/skill-library.d.ts.map +1 -0
- package/dist/memory-layers 2/skill-library.js +54 -0
- package/dist/memory-layers 2/skill-library.js.map +1 -0
- package/dist/memory-layers 2/working-memory.d.ts +57 -0
- package/dist/memory-layers 2/working-memory.d.ts.map +1 -0
- package/dist/memory-layers 2/working-memory.js +153 -0
- package/dist/memory-layers 2/working-memory.js.map +1 -0
- package/dist/middleware/auth.d.ts +287 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +610 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware 2/auth.d.ts +287 -0
- package/dist/middleware 2/auth.d.ts.map +1 -0
- package/dist/middleware 2/auth.js +610 -0
- package/dist/middleware 2/auth.js.map +1 -0
- package/dist/monitoring/health.d.ts +241 -0
- package/dist/monitoring/health.d.ts.map +1 -0
- package/dist/monitoring/health.js +649 -0
- package/dist/monitoring/health.js.map +1 -0
- package/dist/monitoring/metrics.d.ts +153 -0
- package/dist/monitoring/metrics.d.ts.map +1 -0
- package/dist/monitoring/metrics.js +349 -0
- package/dist/monitoring/metrics.js.map +1 -0
- package/dist/monitoring 2/health.d.ts +241 -0
- package/dist/monitoring 2/health.d.ts.map +1 -0
- package/dist/monitoring 2/health.js +649 -0
- package/dist/monitoring 2/health.js.map +1 -0
- package/dist/monitoring 2/metrics.d.ts +153 -0
- package/dist/monitoring 2/metrics.d.ts.map +1 -0
- package/dist/monitoring 2/metrics.js +349 -0
- package/dist/monitoring 2/metrics.js.map +1 -0
- package/dist/retrieval/hybrid-search.d.ts +186 -0
- package/dist/retrieval/hybrid-search.d.ts.map +1 -0
- package/dist/retrieval/hybrid-search.js +330 -0
- package/dist/retrieval/hybrid-search.js.map +1 -0
- package/dist/retrieval 2/hybrid-search.d.ts +186 -0
- package/dist/retrieval 2/hybrid-search.d.ts.map +1 -0
- package/dist/retrieval 2/hybrid-search.js +327 -0
- package/dist/retrieval 2/hybrid-search.js.map +1 -0
- package/dist/router/smart-router.d.ts +157 -0
- package/dist/router/smart-router.d.ts.map +1 -0
- package/dist/router/smart-router.js +524 -0
- package/dist/router/smart-router.js.map +1 -0
- package/dist/router 2/smart-router.d.ts +111 -0
- package/dist/router 2/smart-router.d.ts.map +1 -0
- package/dist/router 2/smart-router.js +383 -0
- package/dist/router 2/smart-router.js.map +1 -0
- package/dist/security/rate-limit-middleware.d.ts +119 -0
- package/dist/security/rate-limit-middleware.d.ts.map +1 -0
- package/dist/security/rate-limit-middleware.js +193 -0
- package/dist/security/rate-limit-middleware.js.map +1 -0
- package/dist/security 2/rate-limit-middleware.d.ts +119 -0
- package/dist/security 2/rate-limit-middleware.d.ts.map +1 -0
- package/dist/security 2/rate-limit-middleware.js +193 -0
- package/dist/security 2/rate-limit-middleware.js.map +1 -0
- package/dist/server 2.js +861 -0
- package/dist/server-wrapper 2.js +170 -0
- package/dist/server-wrapper.d 2.ts +12 -0
- package/dist/server-wrapper.d.ts +12 -0
- package/dist/server-wrapper.d.ts 2.map +1 -0
- package/dist/server-wrapper.d.ts.map +1 -0
- package/dist/server-wrapper.js +170 -0
- package/dist/server-wrapper.js 2.map +1 -0
- package/dist/server-wrapper.js.map +1 -0
- package/dist/server.d 2.ts +14 -0
- package/dist/server.d.ts +14 -0
- package/dist/server.d.ts 2.map +1 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +656 -0
- package/dist/server.js 2.map +1 -0
- package/dist/server.js.map +1 -0
- package/dist/synthesis/conflict-detector.d.ts +43 -0
- package/dist/synthesis/conflict-detector.d.ts.map +1 -0
- package/dist/synthesis/conflict-detector.js +125 -0
- package/dist/synthesis/conflict-detector.js.map +1 -0
- package/dist/synthesis 2/conflict-detector.d.ts +43 -0
- package/dist/synthesis 2/conflict-detector.d.ts.map +1 -0
- package/dist/synthesis 2/conflict-detector.js +125 -0
- package/dist/synthesis 2/conflict-detector.js.map +1 -0
- package/dist/utils/monitoring.d.ts +25 -0
- package/dist/utils/monitoring.d.ts.map +1 -0
- package/dist/utils/monitoring.js +41 -0
- package/dist/utils/monitoring.js.map +1 -0
- package/dist/utils/rate-limiter.d.ts +97 -0
- package/dist/utils/rate-limiter.d.ts.map +1 -0
- package/dist/utils/rate-limiter.js +166 -0
- package/dist/utils/rate-limiter.js.map +1 -0
- package/dist/utils/validation.d.ts +139 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +195 -0
- package/dist/utils/validation.js.map +1 -0
- package/dist/utils 2/monitoring.d.ts +25 -0
- package/dist/utils 2/monitoring.d.ts.map +1 -0
- package/dist/utils 2/monitoring.js +41 -0
- package/dist/utils 2/monitoring.js.map +1 -0
- package/dist/utils 2/rate-limiter.d.ts +97 -0
- package/dist/utils 2/rate-limiter.d.ts.map +1 -0
- package/dist/utils 2/rate-limiter.js +166 -0
- package/dist/utils 2/rate-limiter.js.map +1 -0
- package/dist/utils 2/validation.d.ts +139 -0
- package/dist/utils 2/validation.d.ts.map +1 -0
- package/dist/utils 2/validation.js +195 -0
- package/dist/utils 2/validation.js.map +1 -0
- package/docker-compose.yml +65 -0
- package/embedding-service/Dockerfile +29 -0
- package/embedding-service/README.md +68 -0
- package/embedding-service/requirements.txt +6 -0
- package/embedding-service/server.py +110 -0
- package/package.json +92 -0
- package/scripts/FRESH_INSTALL.md +280 -0
- package/scripts/README.md +101 -0
- package/scripts/ensure-infrastructure.sh +38 -0
- package/scripts/fresh-install-auto.sh +96 -0
- package/scripts/fresh-install.sh +167 -0
- package/scripts/health-check.sh +72 -0
- package/scripts/init-qdrant.ts +201 -0
- package/scripts/postinstall.js +94 -0
- package/scripts/run-accuracy-benchmark.ts +1237 -0
- package/scripts/run-real-benchmark.ts +581 -0
- package/scripts/test-mcp-server.sh +38 -0
package/dist/server 2.js
ADDED
|
@@ -0,0 +1,861 @@
|
|
|
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
|