a fancy canvas mcp server!
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: cache the auth tokens

+71 -3
+1 -1
package.json
··· 6 6 "dev": "bun run --hot src/index.ts", 7 7 "build": "bun build --target=bun --production --outdir=dist ./src/index.ts", 8 8 "start": "bun run dist/index.js", 9 - "generate-key": "bun run scripts/generate-key.ts" 9 + "generate-key": "bun run scripts/generate-key.ts", 10 10 }, 11 11 "dependencies": { 12 12 "@modelcontextprotocol/sdk": "^1.26.0",
+1
src/index.ts
··· 63 63 timestamp: new Date().toISOString(), 64 64 version: "1.0.0", 65 65 uptime: process.uptime(), 66 + cache: DB.getApiKeyCacheStats(), 66 67 }); 67 68 } 68 69
+69 -2
src/lib/db.ts
··· 3 3 4 4 const db = new Database(process.env.DATABASE_PATH || "./canvas-mcp.db"); 5 5 6 + // In-memory cache for verified API keys 7 + interface ApiKeyCacheEntry { 8 + userId: number; 9 + verifiedAt: number; 10 + } 11 + 12 + const apiKeyCache = new Map<string, ApiKeyCacheEntry>(); 13 + const CACHE_TTL = parseInt(process.env.API_KEY_CACHE_TTL || "900000"); // 15 minutes default 14 + 15 + // Cache cleanup interval (runs every 5 minutes) 16 + setInterval(() => { 17 + const now = Date.now(); 18 + for (const [key, entry] of apiKeyCache.entries()) { 19 + if (now - entry.verifiedAt > CACHE_TTL) { 20 + apiKeyCache.delete(key); 21 + } 22 + } 23 + }, 5 * 60 * 1000); 24 + 6 25 // Initialize database schema 7 26 db.exec(` 8 27 CREATE TABLE IF NOT EXISTS users ( ··· 244 263 } 245 264 }, 246 265 247 - // Get user by API key 266 + // Get user by API key (with caching for performance) 248 267 async getUserByApiKey(apiKey: string): Promise<User | null> { 249 - const users = db.query("SELECT * FROM users").all() as User[]; 268 + // Check cache first for O(1) lookup 269 + const cached = apiKeyCache.get(apiKey); 270 + if (cached && Date.now() - cached.verifiedAt < CACHE_TTL) { 271 + // Cache hit - fast path 272 + const user = db 273 + .query("SELECT * FROM users WHERE id = ?") 274 + .get(cached.userId) as User | null; 275 + 276 + if (user) { 277 + return user; 278 + } 279 + // User was deleted - invalidate cache entry 280 + apiKeyCache.delete(apiKey); 281 + } 282 + 283 + // Cache miss - perform full verification (slow path) 284 + const users = db.query("SELECT * FROM users WHERE mcp_api_key IS NOT NULL").all() as User[]; 250 285 251 286 for (const user of users) { 252 287 if (await verifyApiKey(apiKey, user.mcp_api_key)) { 288 + // Cache the verified key for future requests 289 + apiKeyCache.set(apiKey, { 290 + userId: user.id, 291 + verifiedAt: Date.now(), 292 + }); 253 293 return user; 254 294 } 255 295 } ··· 304 344 305 345 // Regenerate API key 306 346 async regenerateApiKey(userId: number): Promise<string> { 347 + // Invalidate all cached entries for this user 348 + for (const [key, entry] of apiKeyCache.entries()) { 349 + if (entry.userId === userId) { 350 + apiKeyCache.delete(key); 351 + } 352 + } 353 + 307 354 const newApiKey = generateApiKey(); 308 355 const hashedApiKey = await hashApiKey(newApiKey); 309 356 ··· 512 559 } 513 560 514 561 return db.query("SELECT * FROM users WHERE id = ?").get(tokenData.user_id) as User | null; 562 + }, 563 + 564 + // Cache management utilities 565 + clearApiKeyCache() { 566 + apiKeyCache.clear(); 567 + }, 568 + 569 + invalidateUserCache(userId: number) { 570 + for (const [key, entry] of apiKeyCache.entries()) { 571 + if (entry.userId === userId) { 572 + apiKeyCache.delete(key); 573 + } 574 + } 575 + }, 576 + 577 + getApiKeyCacheStats() { 578 + return { 579 + size: apiKeyCache.size, 580 + ttl: CACHE_TTL, 581 + }; 515 582 }, 516 583 }; 517 584