limitless-mcp
Version:
MCP server for Limitless API - Connect your Pendant data to Claude and other LLMs
177 lines (176 loc) • 9.7 kB
JavaScript
import { McpError, ErrorCode } from '../utils/errors.js';
import { z } from "zod";
import cache from "../cache/index.js";
import config from "../config.js";
/**
* Register cache management tools on the MCP server
*/
export function registerCacheTools(server) {
// Tool to manage cache settings and data
server.tool("manage_cache", {
action: z.enum(["stats", "clear", "clear_type", "config"]).default("stats").describe("Action to perform on the cache"),
type: z.string().optional().describe("Cache type to clear (for clear_type action)")
}, async ({ action, type }) => {
// Handle different actions
switch (action) {
case "clear":
const keysCount = cache.keys().length;
cache.flushAll();
return {
content: [{
type: "text",
text: `Cache cleared successfully. ${keysCount} entries removed.`
}]
};
case "clear_type":
if (!type) {
throw new McpError(ErrorCode.InvalidParams, "Type parameter is required for clear_type action", { action, type });
}
// Get all keys
const allKeys = cache.keys();
// Filter keys that match the specified type
const keysToDelete = allKeys.filter(key => {
if (type === "full_lifelog" && key.includes('/lifelogs/') && key.includes('includeMarkdown=true')) {
return true;
}
else if (type === "metadata" && key.includes('/lifelogs/') && !key.includes('includeMarkdown=true')) {
return true;
}
else if (type === "listings" && key.startsWith('/lifelogs') && !key.includes('/lifelogs/')) {
return true;
}
else if (type === "search" && key.includes('query=')) {
return true;
}
else if (key.includes(type)) {
// Generic matching based on substring
return true;
}
return false;
});
// Delete the matched keys
keysToDelete.forEach(key => cache.del(key));
return {
content: [{
type: "text",
text: `Selectively cleared ${keysToDelete.length} cache entries of type '${type}'.\n\n` +
`Valid types include: full_lifelog, metadata, listings, search, or any custom substring.`
}]
};
case "config":
// Show current cache configuration
return {
content: [{
type: "text",
text: `# Cache Configuration\n\n` +
`## API Configuration\n` +
`- **API Base URL**: ${config.API_BASE_URL}\n` +
`- **API Timeout**: ${config.API_TIMEOUT_MS}ms (${config.API_TIMEOUT_MS / 1000} seconds)\n` +
`- **Max Retries**: ${config.API_MAX_RETRIES}\n\n` +
`## Pagination & Limits\n` +
`- **Max Results**: ${config.MAX_LIFELOG_LIMIT}\n` +
`- **Default Page Size**: ${config.DEFAULT_PAGE_SIZE}\n` +
`- **Search Multiplier**: ${config.MAX_SEARCH_MULTIPLIER}x\n\n` +
`## Cache Settings\n` +
`- **TTL**: ${config.CACHE_TTL}s (${config.CACHE_TTL / 60} minutes)\n` +
`- **Check Period**: ${config.CACHE_CHECK_PERIOD}s (${config.CACHE_CHECK_PERIOD / 60} minutes)\n` +
`- **Max Keys**: ${config.CACHE_MAX_KEYS}\n\n` +
`## TTL Multipliers\n` +
`- **Metadata**: ${config.CACHE_TTL_MULTIPLIERS.METADATA}x (${config.CACHE_TTL * config.CACHE_TTL_MULTIPLIERS.METADATA}s)\n` +
`- **Listings**: ${config.CACHE_TTL_MULTIPLIERS.LISTINGS}x (${config.CACHE_TTL * config.CACHE_TTL_MULTIPLIERS.LISTINGS}s)\n` +
`- **Search**: ${config.CACHE_TTL_MULTIPLIERS.SEARCH}x (${config.CACHE_TTL * config.CACHE_TTL_MULTIPLIERS.SEARCH}s)\n` +
`- **Summaries**: ${config.CACHE_TTL_MULTIPLIERS.SUMMARIES}x (${config.CACHE_TTL * config.CACHE_TTL_MULTIPLIERS.SUMMARIES}s)\n\n` +
`These settings can be configured via the following environment variables:\n` +
`- LIMITLESS_API_KEY (required)\n` +
`- LIMITLESS_API_BASE_URL\n` +
`- LIMITLESS_API_TIMEOUT_MS\n` +
`- LIMITLESS_API_MAX_RETRIES\n` +
`- LIMITLESS_MAX_LIFELOG_LIMIT\n` +
`- LIMITLESS_DEFAULT_PAGE_SIZE\n` +
`- LIMITLESS_SEARCH_MULTIPLIER\n` +
`- LIMITLESS_CACHE_TTL\n` +
`- LIMITLESS_CACHE_CHECK_PERIOD\n` +
`- LIMITLESS_CACHE_MAX_KEYS\n` +
`- CACHE_TTL_METADATA\n` +
`- CACHE_TTL_LISTINGS\n` +
`- CACHE_TTL_SEARCH\n` +
`- CACHE_TTL_SUMMARIES`
}]
};
case "stats":
default:
const stats = cache.getStats();
const keys = cache.keys();
// Enhanced type detection
const keysByType = keys.reduce((acc, key) => {
let type;
if (key.includes('/lifelogs/')) {
if (key.includes('includeMarkdown=true')) {
type = 'full_lifelog';
}
else {
type = 'lifelog_metadata';
}
}
else if (key.startsWith('/lifelogs')) {
if (key.includes('date=')) {
type = 'date_filtered_listings';
}
else {
type = 'lifelog_listings';
}
}
else if (key.includes('query=')) {
type = 'search_results';
}
else {
type = 'other';
}
acc[type] = (acc[type] || 0) + 1;
return acc;
}, {});
// Calculate hit ratio and cache efficiency metrics
const hitRatio = stats.hits + stats.misses > 0
? ((stats.hits / (stats.hits + stats.misses)) * 100).toFixed(2)
: "0.00";
// Calculate average key age if possible (not directly supported by NodeCache)
const keysSample = keys.slice(0, Math.min(keys.length, 10));
const keyAges = keysSample.map(key => {
const ttl = cache.getTtl(key);
if (ttl) {
return Math.round((ttl - Date.now()) / 1000);
}
return 0;
}).filter(age => age > 0);
const avgAge = keyAges.length > 0
? (keyAges.reduce((sum, age) => sum + age, 0) / keyAges.length).toFixed(0)
: "unknown";
// Enhanced stats output
return {
content: [{
type: "text",
text: `# Cache Statistics\n\n` +
`## Performance Metrics\n` +
`- **Total Keys**: ${stats.keys}\n` +
`- **Hits**: ${stats.hits}\n` +
`- **Misses**: ${stats.misses}\n` +
`- **Hit Ratio**: ${hitRatio}%\n` +
`- **Avg. TTL Remaining**: ${avgAge !== "unknown" ? `~${avgAge}s` : "unknown"}\n\n` +
`## Cache Composition\n` +
Object.entries(keysByType)
.sort(([_, a], [__, b]) => b - a) // Sort by count (highest first)
.map(([type, count]) => {
const percentage = ((count / stats.keys) * 100).toFixed(1);
return `- **${type}**: ${count} (${percentage}%)`;
})
.join('\n') +
`\n\n## Available Actions\n` +
`- **stats**: Show these statistics\n` +
`- **clear**: Clear entire cache\n` +
`- **clear_type**: Clear specific type of cached data (requires 'type' parameter)\n` +
`- **config**: Show cache configuration settings`
}]
};
}
});
}