@twelvehart/supermemory-runtime 1.0.0-next.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 +57 -0
- package/README.md +374 -0
- package/dist/index.js +189 -0
- package/dist/mcp/index.js +1132 -0
- package/docker-compose.prod.yml +91 -0
- package/docker-compose.yml +358 -0
- package/drizzle/0000_dapper_the_professor.sql +159 -0
- package/drizzle/0001_api_keys.sql +51 -0
- package/drizzle/meta/0000_snapshot.json +1532 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +20 -0
- package/package.json +114 -0
- package/scripts/add-extraction-job.ts +122 -0
- package/scripts/benchmark-pgvector.ts +122 -0
- package/scripts/bootstrap.sh +209 -0
- package/scripts/check-runtime-pack.ts +111 -0
- package/scripts/claude-mcp-config.ts +336 -0
- package/scripts/docker-entrypoint.sh +183 -0
- package/scripts/doctor.ts +377 -0
- package/scripts/init-db.sql +33 -0
- package/scripts/install.sh +1110 -0
- package/scripts/mcp-setup.ts +271 -0
- package/scripts/migrations/001_create_pgvector_extension.sql +31 -0
- package/scripts/migrations/002_create_memory_embeddings_table.sql +75 -0
- package/scripts/migrations/003_create_hnsw_index.sql +94 -0
- package/scripts/migrations/004_create_memory_embeddings_standalone.sql +70 -0
- package/scripts/migrations/005_create_chunks_table.sql +95 -0
- package/scripts/migrations/006_create_processing_queue.sql +45 -0
- package/scripts/migrations/generate_test_data.sql +42 -0
- package/scripts/migrations/phase1_comprehensive_test.sql +204 -0
- package/scripts/migrations/run_migrations.sh +286 -0
- package/scripts/migrations/test_hnsw_index.sql +255 -0
- package/scripts/pre-commit-secrets +282 -0
- package/scripts/run-extraction-worker.ts +46 -0
- package/scripts/run-phase1-tests.sh +291 -0
- package/scripts/setup.ts +222 -0
- package/scripts/smoke-install.sh +12 -0
- package/scripts/test-health-endpoint.sh +328 -0
- package/src/api/index.ts +2 -0
- package/src/api/middleware/auth.ts +80 -0
- package/src/api/middleware/csrf.ts +308 -0
- package/src/api/middleware/errorHandler.ts +166 -0
- package/src/api/middleware/rateLimit.ts +360 -0
- package/src/api/middleware/validation.ts +514 -0
- package/src/api/routes/documents.ts +286 -0
- package/src/api/routes/profiles.ts +237 -0
- package/src/api/routes/search.ts +71 -0
- package/src/api/stores/index.ts +58 -0
- package/src/config/bootstrap-env.ts +3 -0
- package/src/config/env.ts +71 -0
- package/src/config/feature-flags.ts +25 -0
- package/src/config/index.ts +140 -0
- package/src/config/secrets.config.ts +291 -0
- package/src/db/client.ts +92 -0
- package/src/db/index.ts +73 -0
- package/src/db/postgres.ts +72 -0
- package/src/db/schema/chunks.schema.ts +31 -0
- package/src/db/schema/containers.schema.ts +46 -0
- package/src/db/schema/documents.schema.ts +49 -0
- package/src/db/schema/embeddings.schema.ts +32 -0
- package/src/db/schema/index.ts +11 -0
- package/src/db/schema/memories.schema.ts +72 -0
- package/src/db/schema/profiles.schema.ts +34 -0
- package/src/db/schema/queue.schema.ts +59 -0
- package/src/db/schema/relationships.schema.ts +42 -0
- package/src/db/schema.ts +223 -0
- package/src/db/worker-connection.ts +47 -0
- package/src/index.ts +235 -0
- package/src/mcp/CLAUDE.md +1 -0
- package/src/mcp/index.ts +1380 -0
- package/src/mcp/legacyState.ts +22 -0
- package/src/mcp/rateLimit.ts +358 -0
- package/src/mcp/resources.ts +309 -0
- package/src/mcp/results.ts +104 -0
- package/src/mcp/tools.ts +401 -0
- package/src/queues/config.ts +119 -0
- package/src/queues/index.ts +289 -0
- package/src/sdk/client.ts +225 -0
- package/src/sdk/errors.ts +266 -0
- package/src/sdk/http.ts +560 -0
- package/src/sdk/index.ts +244 -0
- package/src/sdk/resources/base.ts +65 -0
- package/src/sdk/resources/connections.ts +204 -0
- package/src/sdk/resources/documents.ts +163 -0
- package/src/sdk/resources/index.ts +10 -0
- package/src/sdk/resources/memories.ts +150 -0
- package/src/sdk/resources/search.ts +60 -0
- package/src/sdk/resources/settings.ts +36 -0
- package/src/sdk/types.ts +674 -0
- package/src/services/chunking/index.ts +451 -0
- package/src/services/chunking.service.ts +650 -0
- package/src/services/csrf.service.ts +252 -0
- package/src/services/documents.repository.ts +219 -0
- package/src/services/documents.service.ts +191 -0
- package/src/services/embedding.service.ts +404 -0
- package/src/services/extraction.service.ts +300 -0
- package/src/services/extractors/code.extractor.ts +451 -0
- package/src/services/extractors/index.ts +9 -0
- package/src/services/extractors/markdown.extractor.ts +461 -0
- package/src/services/extractors/pdf.extractor.ts +315 -0
- package/src/services/extractors/text.extractor.ts +118 -0
- package/src/services/extractors/url.extractor.ts +243 -0
- package/src/services/index.ts +235 -0
- package/src/services/ingestion.service.ts +177 -0
- package/src/services/llm/anthropic.ts +400 -0
- package/src/services/llm/base.ts +460 -0
- package/src/services/llm/contradiction-detector.service.ts +526 -0
- package/src/services/llm/heuristics.ts +148 -0
- package/src/services/llm/index.ts +309 -0
- package/src/services/llm/memory-classifier.service.ts +383 -0
- package/src/services/llm/memory-extension-detector.service.ts +523 -0
- package/src/services/llm/mock.ts +470 -0
- package/src/services/llm/openai.ts +398 -0
- package/src/services/llm/prompts.ts +438 -0
- package/src/services/llm/types.ts +373 -0
- package/src/services/memory.repository.ts +1769 -0
- package/src/services/memory.service.ts +1338 -0
- package/src/services/memory.types.ts +234 -0
- package/src/services/persistence/index.ts +295 -0
- package/src/services/pipeline.service.ts +509 -0
- package/src/services/profile.repository.ts +436 -0
- package/src/services/profile.service.ts +560 -0
- package/src/services/profile.types.ts +270 -0
- package/src/services/relationships/detector.ts +1128 -0
- package/src/services/relationships/index.ts +268 -0
- package/src/services/relationships/memory-integration.ts +459 -0
- package/src/services/relationships/strategies.ts +132 -0
- package/src/services/relationships/types.ts +370 -0
- package/src/services/search.service.ts +761 -0
- package/src/services/search.types.ts +220 -0
- package/src/services/secrets.service.ts +384 -0
- package/src/services/vectorstore/base.ts +327 -0
- package/src/services/vectorstore/index.ts +444 -0
- package/src/services/vectorstore/memory.ts +286 -0
- package/src/services/vectorstore/migration.ts +295 -0
- package/src/services/vectorstore/mock.ts +403 -0
- package/src/services/vectorstore/pgvector.ts +695 -0
- package/src/services/vectorstore/types.ts +247 -0
- package/src/startup.ts +389 -0
- package/src/types/api.types.ts +193 -0
- package/src/types/document.types.ts +103 -0
- package/src/types/index.ts +241 -0
- package/src/types/profile.base.ts +133 -0
- package/src/utils/errors.ts +447 -0
- package/src/utils/id.ts +15 -0
- package/src/utils/index.ts +101 -0
- package/src/utils/logger.ts +313 -0
- package/src/utils/sanitization.ts +501 -0
- package/src/utils/secret-validation.ts +273 -0
- package/src/utils/synonyms.ts +188 -0
- package/src/utils/validation.ts +581 -0
- package/src/workers/chunking.worker.ts +242 -0
- package/src/workers/embedding.worker.ts +358 -0
- package/src/workers/extraction.worker.ts +346 -0
- package/src/workers/indexing.worker.ts +505 -0
- package/tsconfig.json +38 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { access, readFile, rename } from 'node:fs/promises'
|
|
2
|
+
|
|
3
|
+
export async function pathExists(filePath: string): Promise<boolean> {
|
|
4
|
+
try {
|
|
5
|
+
await access(filePath)
|
|
6
|
+
return true
|
|
7
|
+
} catch {
|
|
8
|
+
return false
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function readJsonFile<T>(filePath: string): Promise<T> {
|
|
13
|
+
const raw = await readFile(filePath, 'utf-8')
|
|
14
|
+
return JSON.parse(raw) as T
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function archiveFileWithSuffix(filePath: string, suffix: string = '.migrated'): Promise<string> {
|
|
18
|
+
const archiveBasePath = `${filePath}${suffix}`
|
|
19
|
+
const archivePath = (await pathExists(archiveBasePath)) ? `${archiveBasePath}.${Date.now()}` : archiveBasePath
|
|
20
|
+
await rename(filePath, archivePath)
|
|
21
|
+
return archivePath
|
|
22
|
+
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Rate Limiter
|
|
3
|
+
*
|
|
4
|
+
* Provides rate limiting for MCP tool calls using the existing
|
|
5
|
+
* RateLimitStore infrastructure. Supports both per-tool limits
|
|
6
|
+
* and global limits per containerTag.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { RateLimitStore, MemoryRateLimitStore, RedisRateLimitStore } from '../api/middleware/rateLimit.js'
|
|
10
|
+
import { createMcpEnvelopeError, createToolResponse } from './results.js'
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Types
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Result of a rate limit check
|
|
18
|
+
*/
|
|
19
|
+
export interface RateLimitResult {
|
|
20
|
+
/** Whether the request is allowed */
|
|
21
|
+
allowed: boolean
|
|
22
|
+
/** Remaining requests in the current window */
|
|
23
|
+
remaining: number
|
|
24
|
+
/** Seconds until the limit resets */
|
|
25
|
+
resetIn: number
|
|
26
|
+
/** The limit that applies */
|
|
27
|
+
limit: number
|
|
28
|
+
/** Which limit was hit (if any) */
|
|
29
|
+
limitType?: 'tool' | 'global'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Configuration for a rate limit
|
|
34
|
+
*/
|
|
35
|
+
export interface RateLimitConfig {
|
|
36
|
+
/** Maximum requests allowed */
|
|
37
|
+
maxRequests: number
|
|
38
|
+
/** Time window in milliseconds */
|
|
39
|
+
windowMs: number
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Tool-specific rate limit configurations
|
|
44
|
+
*/
|
|
45
|
+
export interface ToolLimits {
|
|
46
|
+
[toolName: string]: RateLimitConfig
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// Default Limits
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Tool-specific rate limits
|
|
55
|
+
*
|
|
56
|
+
* Limits are based on:
|
|
57
|
+
* - Computational cost (embedding generation is expensive)
|
|
58
|
+
* - Side effects (delete is destructive)
|
|
59
|
+
* - Read vs write operations
|
|
60
|
+
*/
|
|
61
|
+
const DEFAULT_TOOL_LIMITS: ToolLimits = {
|
|
62
|
+
// Write operations with embedding generation - most expensive
|
|
63
|
+
supermemory_add: {
|
|
64
|
+
maxRequests: 50,
|
|
65
|
+
windowMs: 60 * 1000, // 50 req/min
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
// Search operations - moderately expensive
|
|
69
|
+
supermemory_search: {
|
|
70
|
+
maxRequests: 100,
|
|
71
|
+
windowMs: 60 * 1000, // 100 req/min
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// Profile operations - moderately expensive
|
|
75
|
+
supermemory_profile: {
|
|
76
|
+
maxRequests: 50,
|
|
77
|
+
windowMs: 60 * 1000, // 50 req/min
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
// Remember - creates facts with potential embedding
|
|
81
|
+
supermemory_remember: {
|
|
82
|
+
maxRequests: 100,
|
|
83
|
+
windowMs: 60 * 1000, // 100 req/min
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
// Recall - semantic search over facts
|
|
87
|
+
supermemory_recall: {
|
|
88
|
+
maxRequests: 200,
|
|
89
|
+
windowMs: 60 * 1000, // 200 req/min
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
// List - lightweight read operation
|
|
93
|
+
supermemory_list: {
|
|
94
|
+
maxRequests: 200,
|
|
95
|
+
windowMs: 60 * 1000, // 200 req/min
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
// Delete - destructive, strict limit
|
|
99
|
+
supermemory_delete: {
|
|
100
|
+
maxRequests: 20,
|
|
101
|
+
windowMs: 60 * 1000, // 20 req/min
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Global rate limit per containerTag
|
|
107
|
+
* Applies across all tools to prevent abuse
|
|
108
|
+
*/
|
|
109
|
+
const DEFAULT_GLOBAL_LIMIT: RateLimitConfig = {
|
|
110
|
+
maxRequests: 1000,
|
|
111
|
+
windowMs: 15 * 60 * 1000, // 1000 req/15min
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ============================================================================
|
|
115
|
+
// MCPRateLimiter Class
|
|
116
|
+
// ============================================================================
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Rate limiter for MCP tool calls
|
|
120
|
+
*
|
|
121
|
+
* Provides two levels of rate limiting:
|
|
122
|
+
* 1. Per-tool limits: Different limits for each tool based on cost
|
|
123
|
+
* 2. Global limit: Overall limit per containerTag across all tools
|
|
124
|
+
*
|
|
125
|
+
* Uses the same store infrastructure as the API rate limiter,
|
|
126
|
+
* supporting both in-memory (single instance) and Redis (distributed).
|
|
127
|
+
*/
|
|
128
|
+
export class MCPRateLimiter {
|
|
129
|
+
private store: RateLimitStore
|
|
130
|
+
private toolLimits: ToolLimits
|
|
131
|
+
private globalLimit: RateLimitConfig
|
|
132
|
+
private readonly keyPrefix = 'mcp:'
|
|
133
|
+
|
|
134
|
+
constructor(options?: {
|
|
135
|
+
store?: RateLimitStore
|
|
136
|
+
toolLimits?: { [toolName: string]: RateLimitConfig }
|
|
137
|
+
globalLimit?: Partial<RateLimitConfig>
|
|
138
|
+
}) {
|
|
139
|
+
// Initialize store - use Redis if available, fallback to memory
|
|
140
|
+
this.store = options?.store ?? this.createDefaultStore()
|
|
141
|
+
|
|
142
|
+
// Merge tool limits with defaults
|
|
143
|
+
this.toolLimits = { ...DEFAULT_TOOL_LIMITS }
|
|
144
|
+
if (options?.toolLimits) {
|
|
145
|
+
for (const [key, value] of Object.entries(options.toolLimits)) {
|
|
146
|
+
this.toolLimits[key] = value
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Merge global limit with defaults
|
|
151
|
+
this.globalLimit = {
|
|
152
|
+
...DEFAULT_GLOBAL_LIMIT,
|
|
153
|
+
...options?.globalLimit,
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Create the default rate limit store
|
|
159
|
+
* Uses Redis if REDIS_URL is set, otherwise in-memory
|
|
160
|
+
*/
|
|
161
|
+
private createDefaultStore(): RateLimitStore {
|
|
162
|
+
if (process.env.REDIS_URL) {
|
|
163
|
+
return new RedisRateLimitStore()
|
|
164
|
+
}
|
|
165
|
+
return new MemoryRateLimitStore()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get the rate limit configuration for a tool
|
|
170
|
+
* Returns a default configuration for unknown tools
|
|
171
|
+
*/
|
|
172
|
+
getToolLimit(toolName: string): RateLimitConfig {
|
|
173
|
+
return (
|
|
174
|
+
this.toolLimits[toolName] ?? {
|
|
175
|
+
maxRequests: 100,
|
|
176
|
+
windowMs: 60 * 1000, // Default: 100 req/min
|
|
177
|
+
}
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get the global rate limit configuration
|
|
183
|
+
*/
|
|
184
|
+
getGlobalLimit(): RateLimitConfig {
|
|
185
|
+
return this.globalLimit
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Check if a request is allowed under rate limits
|
|
190
|
+
*
|
|
191
|
+
* @param containerTag - The container tag (user/context identifier)
|
|
192
|
+
* @param toolName - The MCP tool being called
|
|
193
|
+
* @returns Rate limit result with allowed status and metadata
|
|
194
|
+
*/
|
|
195
|
+
async checkLimit(containerTag: string, toolName: string): Promise<RateLimitResult> {
|
|
196
|
+
const now = Date.now()
|
|
197
|
+
const tag = containerTag || 'default'
|
|
198
|
+
|
|
199
|
+
// Check tool-specific limit first
|
|
200
|
+
const toolLimit = this.getToolLimit(toolName)
|
|
201
|
+
const toolKey = `${this.keyPrefix}tool:${tag}:${toolName}`
|
|
202
|
+
const toolEntry = await this.store.increment(toolKey, toolLimit.windowMs)
|
|
203
|
+
|
|
204
|
+
const toolRemaining = Math.max(0, toolLimit.maxRequests - toolEntry.count)
|
|
205
|
+
const toolResetIn = Math.ceil((toolEntry.resetTime - now) / 1000)
|
|
206
|
+
|
|
207
|
+
if (toolEntry.count > toolLimit.maxRequests) {
|
|
208
|
+
return {
|
|
209
|
+
allowed: false,
|
|
210
|
+
remaining: 0,
|
|
211
|
+
resetIn: toolResetIn,
|
|
212
|
+
limit: toolLimit.maxRequests,
|
|
213
|
+
limitType: 'tool',
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Check global limit
|
|
218
|
+
const globalKey = `${this.keyPrefix}global:${tag}`
|
|
219
|
+
const globalEntry = await this.store.increment(globalKey, this.globalLimit.windowMs)
|
|
220
|
+
|
|
221
|
+
const globalRemaining = Math.max(0, this.globalLimit.maxRequests - globalEntry.count)
|
|
222
|
+
const globalResetIn = Math.ceil((globalEntry.resetTime - now) / 1000)
|
|
223
|
+
|
|
224
|
+
if (globalEntry.count > this.globalLimit.maxRequests) {
|
|
225
|
+
return {
|
|
226
|
+
allowed: false,
|
|
227
|
+
remaining: 0,
|
|
228
|
+
resetIn: globalResetIn,
|
|
229
|
+
limit: this.globalLimit.maxRequests,
|
|
230
|
+
limitType: 'global',
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Request is allowed - return the more restrictive remaining count
|
|
235
|
+
return {
|
|
236
|
+
allowed: true,
|
|
237
|
+
remaining: Math.min(toolRemaining, globalRemaining),
|
|
238
|
+
resetIn: Math.min(toolResetIn, globalResetIn),
|
|
239
|
+
limit: toolLimit.maxRequests,
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get current rate limit status without incrementing counters
|
|
245
|
+
*
|
|
246
|
+
* @param containerTag - The container tag (user/context identifier)
|
|
247
|
+
* @param toolName - The MCP tool to check
|
|
248
|
+
* @returns Current rate limit status
|
|
249
|
+
*/
|
|
250
|
+
async getStatus(containerTag: string, toolName: string): Promise<RateLimitResult> {
|
|
251
|
+
const now = Date.now()
|
|
252
|
+
const tag = containerTag || 'default'
|
|
253
|
+
|
|
254
|
+
// Get tool-specific status
|
|
255
|
+
const toolLimit = this.getToolLimit(toolName)
|
|
256
|
+
const toolKey = `${this.keyPrefix}tool:${tag}:${toolName}`
|
|
257
|
+
const toolEntry = await this.store.get(toolKey)
|
|
258
|
+
|
|
259
|
+
let toolRemaining = toolLimit.maxRequests
|
|
260
|
+
let toolResetIn = 0
|
|
261
|
+
|
|
262
|
+
if (toolEntry && toolEntry.resetTime > now) {
|
|
263
|
+
toolRemaining = Math.max(0, toolLimit.maxRequests - toolEntry.count)
|
|
264
|
+
toolResetIn = Math.ceil((toolEntry.resetTime - now) / 1000)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Get global status
|
|
268
|
+
const globalKey = `${this.keyPrefix}global:${tag}`
|
|
269
|
+
const globalEntry = await this.store.get(globalKey)
|
|
270
|
+
|
|
271
|
+
let globalRemaining = this.globalLimit.maxRequests
|
|
272
|
+
let globalResetIn = 0
|
|
273
|
+
|
|
274
|
+
if (globalEntry && globalEntry.resetTime > now) {
|
|
275
|
+
globalRemaining = Math.max(0, this.globalLimit.maxRequests - globalEntry.count)
|
|
276
|
+
globalResetIn = Math.ceil((globalEntry.resetTime - now) / 1000)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Check if either limit is exceeded (or at capacity - next request would be blocked)
|
|
280
|
+
// Use >= because when count equals maxRequests, the next request would be blocked
|
|
281
|
+
if (toolEntry && toolEntry.count >= toolLimit.maxRequests && toolEntry.resetTime > now) {
|
|
282
|
+
return {
|
|
283
|
+
allowed: false,
|
|
284
|
+
remaining: 0,
|
|
285
|
+
resetIn: toolResetIn,
|
|
286
|
+
limit: toolLimit.maxRequests,
|
|
287
|
+
limitType: 'tool',
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (globalEntry && globalEntry.count >= this.globalLimit.maxRequests && globalEntry.resetTime > now) {
|
|
292
|
+
return {
|
|
293
|
+
allowed: false,
|
|
294
|
+
remaining: 0,
|
|
295
|
+
resetIn: globalResetIn,
|
|
296
|
+
limit: this.globalLimit.maxRequests,
|
|
297
|
+
limitType: 'global',
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
allowed: true,
|
|
303
|
+
remaining: Math.min(toolRemaining, globalRemaining),
|
|
304
|
+
resetIn: Math.max(toolResetIn, globalResetIn),
|
|
305
|
+
limit: toolLimit.maxRequests,
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ============================================================================
|
|
311
|
+
// Singleton Instance
|
|
312
|
+
// ============================================================================
|
|
313
|
+
|
|
314
|
+
let rateLimiterInstance: MCPRateLimiter | null = null
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Get or create the global MCP rate limiter instance
|
|
318
|
+
*/
|
|
319
|
+
export function getMCPRateLimiter(): MCPRateLimiter {
|
|
320
|
+
if (!rateLimiterInstance) {
|
|
321
|
+
rateLimiterInstance = new MCPRateLimiter()
|
|
322
|
+
}
|
|
323
|
+
return rateLimiterInstance
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Create a rate limit error response for MCP
|
|
328
|
+
*
|
|
329
|
+
* @param result - The rate limit result
|
|
330
|
+
* @param toolName - The tool that was rate limited
|
|
331
|
+
* @returns MCP-compatible error response
|
|
332
|
+
*/
|
|
333
|
+
export function createRateLimitErrorResponse(
|
|
334
|
+
result: RateLimitResult,
|
|
335
|
+
toolName: string
|
|
336
|
+
) {
|
|
337
|
+
const limitTypeMessage =
|
|
338
|
+
result.limitType === 'global' ? 'Global rate limit exceeded' : `Rate limit exceeded for ${toolName}`
|
|
339
|
+
|
|
340
|
+
return createToolResponse({
|
|
341
|
+
tool: toolName,
|
|
342
|
+
ok: false,
|
|
343
|
+
data: null,
|
|
344
|
+
errors: [
|
|
345
|
+
createMcpEnvelopeError(
|
|
346
|
+
'RATE_LIMIT_EXCEEDED',
|
|
347
|
+
`${limitTypeMessage}. Try again in ${result.resetIn} seconds. (Limit: ${result.limit} requests)`,
|
|
348
|
+
{
|
|
349
|
+
limit: result.limit,
|
|
350
|
+
limitType: result.limitType ?? 'tool',
|
|
351
|
+
remaining: result.remaining,
|
|
352
|
+
resetIn: result.resetIn,
|
|
353
|
+
},
|
|
354
|
+
true
|
|
355
|
+
),
|
|
356
|
+
],
|
|
357
|
+
})
|
|
358
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Resource Definitions for Supermemory
|
|
3
|
+
*
|
|
4
|
+
* Exposes supermemory data as MCP resources that can be read by clients.
|
|
5
|
+
* Resources use URI patterns to identify different data types.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ValidationError } from '../utils/errors.js'
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Resource URI Patterns
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resource URI patterns:
|
|
16
|
+
* - memory://profiles/{containerTag} - User profile for a container
|
|
17
|
+
* - memory://documents/{id} - Specific document by ID
|
|
18
|
+
* - memory://search?q={query}&container={containerTag} - Search results
|
|
19
|
+
* - memory://facts/{containerTag} - Facts for a container
|
|
20
|
+
* - memory://stats - Overall statistics
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export const RESOURCE_TEMPLATES = [
|
|
24
|
+
{
|
|
25
|
+
uriTemplate: 'memory://profiles/{containerTag}',
|
|
26
|
+
name: 'User Profile',
|
|
27
|
+
description: 'Get the user profile containing extracted facts for a specific container',
|
|
28
|
+
mimeType: 'application/json',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
uriTemplate: 'memory://documents/{id}',
|
|
32
|
+
name: 'Document',
|
|
33
|
+
description: 'Get a specific document by its ID',
|
|
34
|
+
mimeType: 'application/json',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
uriTemplate: 'memory://search',
|
|
38
|
+
name: 'Search Results',
|
|
39
|
+
description: 'Search for memories. Query params: q (query), container (containerTag), limit, mode',
|
|
40
|
+
mimeType: 'application/json',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
uriTemplate: 'memory://facts/{containerTag}',
|
|
44
|
+
name: 'Container Facts',
|
|
45
|
+
description: 'Get all facts for a specific container',
|
|
46
|
+
mimeType: 'application/json',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
uriTemplate: 'memory://stats',
|
|
50
|
+
name: 'Statistics',
|
|
51
|
+
description: 'Get overall supermemory statistics',
|
|
52
|
+
mimeType: 'application/json',
|
|
53
|
+
},
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
// ============================================================================
|
|
57
|
+
// Resource Parser
|
|
58
|
+
// ============================================================================
|
|
59
|
+
|
|
60
|
+
export interface ParsedResourceUri {
|
|
61
|
+
type: 'profile' | 'document' | 'search' | 'facts' | 'stats' | 'unknown'
|
|
62
|
+
params: Record<string, string>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Parse a resource URI into its components
|
|
67
|
+
*/
|
|
68
|
+
export function parseResourceUri(uri: string): ParsedResourceUri {
|
|
69
|
+
// Handle memory:// protocol
|
|
70
|
+
if (!uri.startsWith('memory://')) {
|
|
71
|
+
return { type: 'unknown', params: {} }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const path = uri.substring('memory://'.length)
|
|
75
|
+
const [pathPart, queryPart] = path.split('?')
|
|
76
|
+
|
|
77
|
+
// Parse query parameters
|
|
78
|
+
const params: Record<string, string> = {}
|
|
79
|
+
if (queryPart) {
|
|
80
|
+
const searchParams = new URLSearchParams(queryPart)
|
|
81
|
+
for (const [key, value] of searchParams.entries()) {
|
|
82
|
+
params[key] = value
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Parse path
|
|
87
|
+
if (!pathPart) {
|
|
88
|
+
return { type: 'unknown', params }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const segments = pathPart.split('/').filter(Boolean)
|
|
92
|
+
|
|
93
|
+
if (segments[0] === 'profiles' && segments[1]) {
|
|
94
|
+
return { type: 'profile', params: { containerTag: segments[1], ...params } }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (segments[0] === 'documents' && segments[1]) {
|
|
98
|
+
return { type: 'document', params: { id: segments[1], ...params } }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (segments[0] === 'search') {
|
|
102
|
+
return { type: 'search', params }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (segments[0] === 'facts' && segments[1]) {
|
|
106
|
+
return { type: 'facts', params: { containerTag: segments[1], ...params } }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (segments[0] === 'stats') {
|
|
110
|
+
return { type: 'stats', params }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { type: 'unknown', params }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Build a resource URI from components
|
|
118
|
+
*/
|
|
119
|
+
export function buildResourceUri(
|
|
120
|
+
type: 'profile' | 'document' | 'search' | 'facts' | 'stats',
|
|
121
|
+
params?: Record<string, string>
|
|
122
|
+
): string {
|
|
123
|
+
switch (type) {
|
|
124
|
+
case 'profile':
|
|
125
|
+
if (!params?.containerTag) {
|
|
126
|
+
throw new ValidationError('containerTag required for profile URI', {
|
|
127
|
+
containerTag: ['containerTag parameter is required for profile URIs'],
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
return `memory://profiles/${encodeURIComponent(params.containerTag)}`
|
|
131
|
+
|
|
132
|
+
case 'document':
|
|
133
|
+
if (!params?.id) {
|
|
134
|
+
throw new ValidationError('id required for document URI', {
|
|
135
|
+
id: ['id parameter is required for document URIs'],
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
return `memory://documents/${encodeURIComponent(params.id)}`
|
|
139
|
+
|
|
140
|
+
case 'search': {
|
|
141
|
+
const searchParams = new URLSearchParams()
|
|
142
|
+
if (params?.q) searchParams.set('q', params.q)
|
|
143
|
+
if (params?.container) searchParams.set('container', params.container)
|
|
144
|
+
if (params?.limit) searchParams.set('limit', params.limit)
|
|
145
|
+
if (params?.mode) searchParams.set('mode', params.mode)
|
|
146
|
+
const queryString = searchParams.toString()
|
|
147
|
+
return queryString ? `memory://search?${queryString}` : 'memory://search'
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
case 'facts':
|
|
151
|
+
if (!params?.containerTag) {
|
|
152
|
+
throw new ValidationError('containerTag required for facts URI', {
|
|
153
|
+
containerTag: ['containerTag parameter is required for facts URIs'],
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
return `memory://facts/${encodeURIComponent(params.containerTag)}`
|
|
157
|
+
|
|
158
|
+
case 'stats':
|
|
159
|
+
return 'memory://stats'
|
|
160
|
+
|
|
161
|
+
default:
|
|
162
|
+
throw new ValidationError(`Unknown resource type: ${type}`, {
|
|
163
|
+
type: [`Invalid resource type '${type}'. Valid types: profiles, documents, search, facts, stats`],
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ============================================================================
|
|
169
|
+
// Resource Response Types
|
|
170
|
+
// ============================================================================
|
|
171
|
+
|
|
172
|
+
export interface ProfileResource {
|
|
173
|
+
uri: string
|
|
174
|
+
containerTag: string
|
|
175
|
+
staticFacts: Array<{
|
|
176
|
+
id: string
|
|
177
|
+
content: string
|
|
178
|
+
category?: string
|
|
179
|
+
confidence: number
|
|
180
|
+
extractedAt: string
|
|
181
|
+
}>
|
|
182
|
+
dynamicFacts: Array<{
|
|
183
|
+
id: string
|
|
184
|
+
content: string
|
|
185
|
+
category?: string
|
|
186
|
+
expiresAt?: string
|
|
187
|
+
extractedAt: string
|
|
188
|
+
}>
|
|
189
|
+
createdAt: string
|
|
190
|
+
updatedAt: string
|
|
191
|
+
version: number
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface DocumentResource {
|
|
195
|
+
uri: string
|
|
196
|
+
id: string
|
|
197
|
+
title?: string
|
|
198
|
+
content: string
|
|
199
|
+
contentType: string
|
|
200
|
+
containerTag?: string
|
|
201
|
+
sourceUrl?: string
|
|
202
|
+
metadata?: Record<string, unknown>
|
|
203
|
+
createdAt: string
|
|
204
|
+
updatedAt: string
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface SearchResource {
|
|
208
|
+
uri: string
|
|
209
|
+
query: string
|
|
210
|
+
results: Array<{
|
|
211
|
+
id: string
|
|
212
|
+
content: string
|
|
213
|
+
similarity: number
|
|
214
|
+
containerTag?: string
|
|
215
|
+
metadata?: Record<string, unknown>
|
|
216
|
+
}>
|
|
217
|
+
totalCount: number
|
|
218
|
+
searchTimeMs: number
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export interface FactsResource {
|
|
222
|
+
uri: string
|
|
223
|
+
containerTag: string
|
|
224
|
+
facts: Array<{
|
|
225
|
+
id: string
|
|
226
|
+
content: string
|
|
227
|
+
type: 'static' | 'dynamic'
|
|
228
|
+
category?: string
|
|
229
|
+
confidence: number
|
|
230
|
+
createdAt: string
|
|
231
|
+
expiresAt?: string
|
|
232
|
+
}>
|
|
233
|
+
totalCount: number
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export interface StatsResource {
|
|
237
|
+
uri: string
|
|
238
|
+
totalDocuments: number
|
|
239
|
+
totalMemories: number
|
|
240
|
+
totalFacts: number
|
|
241
|
+
containerTags: string[]
|
|
242
|
+
indexedVectors: number
|
|
243
|
+
lastUpdated: string
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ============================================================================
|
|
247
|
+
// Resource List Response
|
|
248
|
+
// ============================================================================
|
|
249
|
+
|
|
250
|
+
export interface ResourceListItem {
|
|
251
|
+
uri: string
|
|
252
|
+
name: string
|
|
253
|
+
description?: string
|
|
254
|
+
mimeType?: string
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export const MAX_LISTED_RESOURCE_CONTAINERS = 5
|
|
258
|
+
export const MAX_LISTED_RESOURCE_DOCUMENTS = 5
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Generate a list of available resources for a given state
|
|
262
|
+
*/
|
|
263
|
+
export function generateResourceList(containerTags: string[], documentIds: string[]): ResourceListItem[] {
|
|
264
|
+
const resources: ResourceListItem[] = []
|
|
265
|
+
|
|
266
|
+
// Add stats resource
|
|
267
|
+
resources.push({
|
|
268
|
+
uri: 'memory://stats',
|
|
269
|
+
name: 'Supermemory Statistics',
|
|
270
|
+
description: 'Overall statistics and health of the memory system',
|
|
271
|
+
mimeType: 'application/json',
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
// Add search resource
|
|
275
|
+
resources.push({
|
|
276
|
+
uri: 'memory://search',
|
|
277
|
+
name: 'Search Memories',
|
|
278
|
+
description: 'Search through stored memories (add ?q=query to search)',
|
|
279
|
+
mimeType: 'application/json',
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
// Add profile resources for each container
|
|
283
|
+
for (const tag of containerTags.slice(0, MAX_LISTED_RESOURCE_CONTAINERS)) {
|
|
284
|
+
resources.push({
|
|
285
|
+
uri: buildResourceUri('profile', { containerTag: tag }),
|
|
286
|
+
name: `Profile: ${tag}`,
|
|
287
|
+
description: `User profile and facts for container "${tag}"`,
|
|
288
|
+
mimeType: 'application/json',
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
resources.push({
|
|
292
|
+
uri: buildResourceUri('facts', { containerTag: tag }),
|
|
293
|
+
name: `Facts: ${tag}`,
|
|
294
|
+
description: `All facts for container "${tag}"`,
|
|
295
|
+
mimeType: 'application/json',
|
|
296
|
+
})
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Add only a few recent documents to keep resource discovery bounded.
|
|
300
|
+
for (const id of documentIds.slice(0, MAX_LISTED_RESOURCE_DOCUMENTS)) {
|
|
301
|
+
resources.push({
|
|
302
|
+
uri: buildResourceUri('document', { id }),
|
|
303
|
+
name: `Document: ${id}`,
|
|
304
|
+
mimeType: 'application/json',
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return resources
|
|
309
|
+
}
|