@kya-os/mcp-i
Version:
The TypeScript MCP framework with identity features built-in
235 lines (234 loc) • 10.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.detectCacheType = detectCacheType;
exports.createNonceCache = createNonceCache;
exports.createNonceCacheWithConfig = createNonceCacheWithConfig;
const memory_nonce_cache_1 = require("./memory-nonce-cache");
const redis_nonce_cache_1 = require("./redis-nonce-cache");
const dynamodb_nonce_cache_1 = require("./dynamodb-nonce-cache");
const cloudflare_kv_nonce_cache_1 = require("./cloudflare-kv-nonce-cache");
/**
* Get environment variable with fallback support for old naming
* Supports migration from XMCPI_ to MCPI_ prefixes
*/
function getEnvWithFallback(newKey, oldKey) {
const newValue = process.env[newKey];
if (newValue !== undefined) {
return newValue;
}
if (oldKey) {
const oldValue = process.env[oldKey];
if (oldValue !== undefined) {
console.warn(`⚠️ Environment variable ${oldKey} is deprecated. Please use ${newKey} instead.`);
return oldValue;
}
}
return undefined;
}
/**
* Environment-based cache type detection
*/
function detectCacheType() {
// Check for explicit configuration (with fallback support)
const explicitType = getEnvWithFallback("MCPI_NONCE_CACHE_TYPE", "XMCPI_NONCE_CACHE_TYPE");
if (explicitType &&
["memory", "redis", "dynamodb", "cloudflare-kv"].includes(explicitType)) {
return explicitType;
}
// Auto-detect based on environment (most specific first)
// Redis detection
if (process.env.REDIS_URL ||
getEnvWithFallback("MCPI_REDIS_URL", "XMCPI_REDIS_URL")) {
return "redis";
}
// DynamoDB detection (AWS Lambda environment)
if ((process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION) &&
getEnvWithFallback("MCPI_DYNAMODB_TABLE", "XMCPI_DYNAMODB_TABLE")) {
return "dynamodb";
}
// Cloudflare Workers detection (more robust)
if (typeof globalThis !== "undefined") {
// Check for Cloudflare Workers specific globals
const globalScope = globalThis;
if ("caches" in globalThis &&
typeof globalScope.fetch === "function" &&
(typeof globalScope.addEventListener === "function" ||
typeof globalScope.Response === "function")) {
return "cloudflare-kv";
}
}
// Warn about memory cache in production
if (process.env.NODE_ENV === "production" ||
getEnvWithFallback("MCPI_ENV", "XMCPI_ENV") === "production") {
console.warn("mcpi: Using memory cache in production environment. " +
"For multi-instance deployments, configure Redis (REDIS_URL), " +
"DynamoDB (AWS_REGION + MCPI_DYNAMODB_TABLE), or " +
"Cloudflare KV (MCPI_KV_NAMESPACE).");
}
return "memory";
}
/**
* Create a nonce cache instance based on configuration or environment detection
*/
async function createNonceCache(config, options) {
const cacheType = config?.type || detectCacheType();
switch (cacheType) {
case "redis": {
const redisUrl = config?.redis?.url ||
process.env.REDIS_URL ||
getEnvWithFallback("MCPI_REDIS_URL", "XMCPI_REDIS_URL");
if (!redisUrl) {
const error = new Error("Redis URL not found");
if (options?.throwOnError) {
throw error;
}
console.warn("Redis URL not found, falling back to memory cache");
return new memory_nonce_cache_1.MemoryNonceCache();
}
try {
// Dynamic import to avoid bundling Redis in environments that don't need it
const { createClient } = await import("redis");
const redis = createClient({
url: redisUrl,
socket: {
connectTimeout: 5000, // 5 second timeout
},
});
// Test connection
await redis.connect();
// Verify basic operations work
await redis.ping();
return new redis_nonce_cache_1.RedisNonceCache(redis, config?.redis?.keyPrefix);
}
catch (error) {
if (options?.throwOnError) {
throw error;
}
const errorMessage = error instanceof Error ? error.message : String(error);
console.warn("Failed to connect to Redis, falling back to memory cache:", errorMessage);
return new memory_nonce_cache_1.MemoryNonceCache();
}
}
case "dynamodb": {
const tableName = config?.dynamodb?.tableName ||
getEnvWithFallback("MCPI_DYNAMODB_TABLE", "XMCPI_DYNAMODB_TABLE");
if (!tableName) {
const error = new Error("DynamoDB table name not found");
if (options?.throwOnError) {
throw error;
}
console.warn("DynamoDB table name not found, falling back to memory cache");
return new memory_nonce_cache_1.MemoryNonceCache();
}
try {
// Dynamic import to avoid bundling AWS SDK in environments that don't need it
const awsSdk = await import("@aws-sdk/client-dynamodb");
const sdkModule = awsSdk;
const DynamoDBClient = "DynamoDBClient" in sdkModule
? sdkModule.DynamoDBClient
: sdkModule.default.DynamoDBClient;
const DescribeTableCommand = "DescribeTableCommand" in sdkModule
? sdkModule.DescribeTableCommand
: sdkModule.default.DescribeTableCommand;
const region = config?.dynamodb?.region ||
process.env.AWS_REGION ||
process.env.AWS_DEFAULT_REGION ||
"us-east-1";
const dynamodb = new DynamoDBClient({
region,
maxAttempts: 3,
});
// Verify table exists and is accessible
try {
await dynamodb.send(new DescribeTableCommand({ TableName: tableName }));
}
catch (tableError) {
const error = tableError;
if (error.name === "ResourceNotFoundException") {
throw new Error(`DynamoDB table '${tableName}' not found in region '${region}'`);
}
throw tableError;
}
return new dynamodb_nonce_cache_1.DynamoNonceCache(dynamodb, tableName, config?.dynamodb?.keyAttribute, config?.dynamodb?.ttlAttribute);
}
catch (error) {
if (options?.throwOnError) {
throw error;
}
const errorMessage = error instanceof Error ? error.message : String(error);
console.warn("Failed to initialize DynamoDB, falling back to memory cache:", errorMessage);
return new memory_nonce_cache_1.MemoryNonceCache();
}
}
case "cloudflare-kv": {
const namespace = config?.cloudflareKv?.namespace ||
getEnvWithFallback("MCPI_KV_NAMESPACE", "XMCPI_KV_NAMESPACE");
if (!namespace) {
const error = new Error("Cloudflare KV namespace not found");
if (options?.throwOnError) {
throw error;
}
console.warn("Cloudflare KV namespace not found, falling back to memory cache");
return new memory_nonce_cache_1.MemoryNonceCache();
}
try {
// In Cloudflare Workers, KV namespaces are available as globals
const globalScope = globalThis;
const kv = globalScope[namespace];
if (!kv) {
throw new Error(`KV namespace '${namespace}' not found in global scope`);
}
// Verify KV namespace has required methods
const kvInterface = kv;
if (typeof kvInterface.get !== "function" ||
typeof kvInterface.put !== "function" ||
typeof kvInterface.delete !== "function") {
throw new Error(`KV namespace '${namespace}' does not have required methods`);
}
// Test basic KV operation (this will be very fast in Workers)
try {
const kvTest = kv;
await kvTest.get("__mcpi_test_key__");
}
catch (kvError) {
const error = kvError;
// Some KV errors are expected (like key not found), but connection errors are not
if (error.message?.includes("network") ||
error.message?.includes("timeout")) {
throw kvError;
}
// Other errors (like key not found) are fine
}
return new cloudflare_kv_nonce_cache_1.CloudflareKVNonceCache(kv, config?.cloudflareKv?.keyPrefix);
}
catch (error) {
if (options?.throwOnError) {
throw error;
}
const errorMessage = error instanceof Error ? error.message : String(error);
console.warn("Failed to initialize Cloudflare KV, falling back to memory cache:", errorMessage);
return new memory_nonce_cache_1.MemoryNonceCache();
}
}
case "memory":
default:
return new memory_nonce_cache_1.MemoryNonceCache();
}
}
/**
* Create nonce cache with explicit configuration override
*/
async function createNonceCacheWithConfig(options = {}) {
try {
return await createNonceCache(options.config, {
throwOnError: options.fallbackToMemory === false,
});
}
catch (error) {
if (options.fallbackToMemory !== false) {
console.warn("Failed to create configured nonce cache, falling back to memory:", error);
return new memory_nonce_cache_1.MemoryNonceCache();
}
throw error;
}
}