Skip to main content
The Cache Plugin provides transparent response caching for tools based on their input payloads, reducing redundant computation and improving response time.

Why Use Caching?

Faster Responses

Return cached results instantly without re-executing expensive operations

Reduce Load

Minimize API calls, database queries, and computational overhead

Cost Savings

Lower infrastructure costs by reducing redundant processing

Better UX

Improve perceived performance with instant responses for repeated queries

Installation

npm install @frontmcp/plugins

How It Works

1

Before Execution

The plugin hashes the tool’s validated input and checks the cache store
2

Cache Hit

If a cached result exists, it’s returned immediately, bypassing tool execution entirely
3

Cache Miss

The tool executes normally, and the result is stored with the configured TTL
4

Sliding Window (Optional)

When enabled, each cache read refreshes the TTL to keep hot entries alive longer
Cache entries are keyed using a deterministic hash of the tool’s validated input. The same input always produces the same cache key.

Quick Start

Basic Setup (In-Memory)

import { App } from '@frontmcp/sdk';
import CachePlugin from '@frontmcp/plugins/cache';

@App({
  id: 'my-app',
  name: 'My App',
  plugins: [CachePlugin], // Default: memory store, 1-day TTL
  tools: [
    /* your tools */
  ],
})
export default class MyApp {}

Enable Caching on Tools

Caching is opt-in per tool. Add the cache field to your tool metadata:
import { Tool, ToolContext } from '@frontmcp/sdk';
import { z } from 'zod';

@Tool({
  name: 'get-user',
  description: 'Get user by ID',
  inputSchema: { id: z.string() },
  cache: true, // Enable caching with defaults
})
export default class GetUserTool extends ToolContext {
  async execute(input: { id: string }) {
    // Expensive operation (e.g., database query)
    return await this.database.getUser(input.id);
  }
}

Storage Options

In-Memory (Default)

Best for: Single-instance deployments, development, non-critical caching
CachePlugin.init({
  type: 'memory',
  defaultTTL: 300, // 5 minutes (default: 86400 = 1 day)
});
Memory cache resets when the process restarts. Not shared across multiple instances.
Best for: Multi-instance deployments, persistent caching, production environments
CachePlugin.init({
  type: 'redis',
  defaultTTL: 600, // 10 minutes
  config: {
    host: '127.0.0.1',
    port: 6379,
    password: process.env.REDIS_PASSWORD, // optional
    db: 0, // optional
  },
});
Redis enables cache sharing across multiple server instances and persists cache across restarts.

Configuration Options

Plugin-Level Configuration

Configure default behavior when registering the plugin:
type
'memory' | 'redis' | 'redis-client'
default:"'memory'"
Cache store backend to use
defaultTTL
number
default:"86400"
Default time-to-live in seconds (applies to all cached tools unless overridden)
config
object
Redis connection configuration (required when type: 'redis')
client
Redis
Existing ioredis client instance (required when type: 'redis-client')

Tool-Level Configuration

Configure caching behavior per tool in the @Tool or tool() metadata:
@Tool({
  name: 'get-report',
  cache: true, // Uses plugin's defaultTTL
})
cache
boolean | object
Enable caching for this tool
  • true - Use plugin defaults
  • object - Custom configuration
cache.ttl
number
Time-to-live in seconds for this tool’s cache entries (overrides plugin default)
cache.slideWindow
boolean
default:"false"
When true, reading from cache refreshes the TTL, keeping frequently accessed entries alive longer

Advanced Usage

Multi-Tenant Caching

Include tenant or user identifiers in your tool inputs to ensure cache isolation:
@Tool({
  name: 'get-tenant-data',
  inputSchema: {
    tenantId: z.string(),
    dataType: z.string(),
  },
  cache: { ttl: 600 },
})
export default class GetTenantDataTool extends ToolContext {
  async execute(input: { tenantId: string; dataType: string }) {
    // Cache key includes tenantId automatically via input hash
    return await this.fetchTenantData(input.tenantId, input.dataType);
  }
}
The cache key is derived from the entire input object, so including tenant/user IDs ensures proper isolation.

Session-Scoped Caching

For user-specific data, include session or user identifiers:
export const GetUserDashboard = tool({
  name: 'get-user-dashboard',
  inputSchema: {
    userId: z.string(),
    dateRange: z.object({
      start: z.string(),
      end: z.string(),
    }),
  },
  cache: {
    ttl: 120, // 2 minutes
    slideWindow: true,
  },
})(async (input, ctx) => {
  // Cache key includes userId and dateRange
  return await generateDashboard(input.userId, input.dateRange);
});

Time-Based Invalidation

Use short TTLs for frequently changing data:
@Tool({
  name: 'get-live-stock-price',
  inputSchema: { symbol: z.string() },
  cache: {
    ttl: 5, // Only cache for 5 seconds
  },
})

Best Practices

Cache tools whose outputs depend solely on their inputs. Don’t cache tools that:
  • Return random data
  • Depend on external time-sensitive state
  • Have side effects (mutations, API calls that change state)
  • Short TTLs (5-60s): Real-time data, frequently changing content - Medium TTLs (5-30min): User dashboards, reports, analytics - Long TTLs (hours-days): Static content, configuration, reference data
Redis provides: - Cache persistence across restarts - Sharing across multiple server instances - Better memory management with eviction policies
Always include tenant IDs, user IDs, or other scoping fields in your tool inputs:
// Good: includes tenantId for isolation
{ tenantId: "t-123", reportId: "r-456" }

// Bad: no scoping, shared across tenants
{ reportId: "r-456" }
Enable slideWindow for frequently accessed data to keep it cached longer:
cache: {
  ttl: 300,
  slideWindow: true, // Popular items stay cached
}

Cache Behavior Reference

BehaviorDescription
Key DerivationDeterministic hash from validated input. Same input = same cache key
Cache HitsBypasses tool execution entirely, returns cached result instantly
Default TTL86400 seconds (1 day) if not specified
Sliding WindowExtends TTL on reads when enabled
Store ChoiceMemory is node-local; Redis enables multi-instance sharing
InvalidationAutomatic after TTL expires, or manually by restarting (memory)

Troubleshooting

Possible causes:
  • Tool missing cache: true in metadata
  • Cache store offline or misconfigured
  • Input varies slightly (whitespace, order of fields)
Solutions:
  • Verify cache field is set in tool metadata
  • Check Redis connection if using Redis backend
  • Ensure input structure is consistent
Possible causes:
  • TTL too long for data freshness requirements
  • Data changed but cache not invalidated
Solutions:
  • Reduce TTL for the tool
  • Consider input-based cache busting (include timestamp or version in input)
  • Restart server to clear memory cache (or flush Redis)
Possible cause:
  • Using memory cache with multiple server instances
Solution:
  • Switch to Redis backend for multi-instance deployments
Solution:
  • Currently, manual invalidation requires custom implementation
  • For memory: restart the server
  • For Redis: use Redis CLI to delete keys manually
  • Consider shorter TTLs or input-based versioning instead

Complete Example

import { FrontMcp, App, Tool, ToolContext } from '@frontmcp/sdk';
import CachePlugin from '@frontmcp/plugins/cache';
import { z } from 'zod';

// Configure Redis cache
const cachePlugin = CachePlugin.init({
  type: 'redis',
  defaultTTL: 600, // 10 minutes default
  config: {
    host: process.env.REDIS_HOST || 'localhost',
    port: parseInt(process.env.REDIS_PORT || '6379'),
    password: process.env.REDIS_PASSWORD,
  },
});

// Expensive report generation tool
@Tool({
  name: 'generate-monthly-report',
  description: 'Generate monthly sales report for a tenant',
  inputSchema: {
    tenantId: z.string(),
    month: z.string(), // "2025-01"
  },
  cache: {
    ttl: 1800, // 30 minutes (reports don't change often)
  },
})
class GenerateMonthlyReportTool extends ToolContext {
  async execute(input: { tenantId: string; month: string }) {
    this.logger.info('Generating report', input);

    // Expensive operation: aggregate data, generate charts, etc.
    const report = await this.database.generateReport(input.tenantId, input.month);

    return report;
  }
}

// Hot data with sliding window
@Tool({
  name: 'get-trending-products',
  description: 'Get current trending products',
  inputSchema: {
    category: z.string(),
    limit: z.number().default(10),
  },
  cache: {
    ttl: 120, // 2 minutes
    slideWindow: true, // Keep popular queries cached
  },
})
class GetTrendingProductsTool extends ToolContext {
  async execute(input: { category: string; limit: number }) {
    return await this.analytics.getTrendingProducts(input.category, input.limit);
  }
}

@App({
  id: 'analytics',
  name: 'Analytics App',
  plugins: [cachePlugin],
  tools: [GenerateMonthlyReportTool, GetTrendingProductsTool],
})
class AnalyticsApp {}

@FrontMcp({
  info: { name: 'Analytics Server', version: '1.0.0' },
  apps: [AnalyticsApp],
  http: { port: 3000 },
})
export default class Server {}