cf-memory-mcp
Version:
Best-in-class MCP (Model Context Protocol) server for AI memory storage with MIRIX-Inspired Specialized Memory Types (Core, Episodic, Semantic, Procedural, Resource, Knowledge Vault), Progressive Disclosure, AI-Powered Summaries, Context Window Optimizati
333 lines (290 loc) • 11.2 kB
JavaScript
/**
* CF Memory MCP - Portable MCP Server
*
* A portable MCP (Model Context Protocol) server for AI memory storage
* using Cloudflare infrastructure. This executable connects to a deployed
* Cloudflare Worker to provide memory storage capabilities for AI agents.
*
* Usage: npx cf-memory-mcp
*
* @author John Lam <johnlam90@gmail.com>
* @license MIT
*/
const https = require('https');
const { URL } = require('url');
const os = require('os');
const process = require('process');
// Configuration
const STREAMABLE_HTTP_URL = 'https://cf-memory-mcp.johnlam90.workers.dev/mcp';
const LEGACY_SERVER_URL = 'https://cf-memory-mcp.johnlam90.workers.dev/mcp/message';
const PACKAGE_VERSION = require('../package.json').version;
const TIMEOUT_MS = 30000;
const CONNECT_TIMEOUT_MS = 10000;
// Get API key from environment variable (will be checked later)
const API_KEY = process.env.CF_MEMORY_API_KEY;
/**
* Cross-platform MCP stdio bridge
* Handles communication between MCP clients and the Cloudflare Worker
*/
class CFMemoryMCP {
constructor() {
this.streamableHttpUrl = STREAMABLE_HTTP_URL;
this.legacyServerUrl = LEGACY_SERVER_URL;
this.userAgent = `cf-memory-mcp/${PACKAGE_VERSION} (${os.platform()} ${os.arch()}; Node.js ${process.version})`;
this.useStreamableHttp = true; // Try Streamable HTTP first
// Handle process termination gracefully
process.on('SIGINT', () => this.shutdown('SIGINT'));
process.on('SIGTERM', () => this.shutdown('SIGTERM'));
process.on('uncaughtException', (error) => {
this.logError('Uncaught exception:', error);
this.shutdown('ERROR');
});
// Set up stdio encoding
process.stdin.setEncoding('utf8');
process.stdout.setEncoding('utf8');
this.logDebug('CF Memory MCP server starting...');
this.logDebug(`Streamable HTTP URL: ${this.streamableHttpUrl}`);
this.logDebug(`Legacy Server URL: ${this.legacyServerUrl}`);
this.logDebug(`User Agent: ${this.userAgent}`);
}
/**
* Log debug messages to stderr (won't interfere with MCP communication)
*/
logDebug(message) {
if (process.env.DEBUG || process.env.MCP_DEBUG) {
process.stderr.write(`[DEBUG] ${new Date().toISOString()} ${message}\n`);
}
}
/**
* Log error messages to stderr
*/
logError(message, error = null) {
const timestamp = new Date().toISOString();
process.stderr.write(`[ERROR] ${timestamp} ${message}\n`);
if (error && error.stack) {
process.stderr.write(`[ERROR] ${timestamp} ${error.stack}\n`);
}
}
/**
* Start listening for MCP messages on stdin
*/
async start() {
try {
// Skip connectivity test in MCP mode - it will be tested when first request is made
this.logDebug('Starting MCP message processing...');
await this.processStdio();
} catch (error) {
this.logError('Failed to start MCP server:', error);
process.exit(1);
}
}
/**
* Test connectivity to the Cloudflare Worker
*/
async testConnectivity() {
this.logDebug('Testing connectivity to Cloudflare Worker...');
const testMessage = {
jsonrpc: '2.0',
id: 'connectivity-test',
method: 'initialize',
params: {
protocolVersion: '2025-03-26',
capabilities: { tools: {} },
clientInfo: {
name: 'cf-memory-mcp',
version: PACKAGE_VERSION
}
}
};
try {
// Try Streamable HTTP first
const response = await this.makeRequest(testMessage);
if (response.error) {
throw new Error(`Server responded with error: ${response.error.message}`);
}
this.logDebug('Connectivity test successful with Streamable HTTP');
} catch (error) {
// If Streamable HTTP fails, try legacy endpoint
this.logDebug('Streamable HTTP failed, trying legacy endpoint...');
this.useStreamableHttp = false;
try {
const response = await this.makeRequest(testMessage);
if (response.error) {
throw new Error(`Server responded with error: ${response.error.message}`);
}
this.logDebug('Connectivity test successful with legacy endpoint');
} catch (legacyError) {
throw new Error(`Cannot connect to CF Memory server: ${error.message} (Legacy also failed: ${legacyError.message})`);
}
}
}
/**
* Process stdio input/output for MCP communication
*/
async processStdio() {
let buffer = '';
// Handle stdin data
for await (const chunk of process.stdin) {
buffer += chunk;
// Process complete JSON-RPC messages (one per line)
let newlineIndex;
while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
const line = buffer.slice(0, newlineIndex).trim();
buffer = buffer.slice(newlineIndex + 1);
if (line) {
await this.handleMessage(line);
}
}
}
// Process any remaining buffer content
if (buffer.trim()) {
await this.handleMessage(buffer.trim());
}
this.logDebug('Stdin closed, shutting down...');
}
/**
* Handle a single MCP message
*/
async handleMessage(messageStr) {
try {
const message = JSON.parse(messageStr);
this.logDebug(`Processing message: ${message.method} (id: ${message.id})`);
const response = await this.makeRequest(message);
// Send response to stdout
process.stdout.write(JSON.stringify(response) + '\n');
} catch (error) {
this.logError('Error handling message:', error);
// Send error response
const errorResponse = {
jsonrpc: '2.0',
id: null,
error: {
code: -32700,
message: 'Parse error',
data: error.message
}
};
process.stdout.write(JSON.stringify(errorResponse) + '\n');
}
}
/**
* Make HTTP request to the Cloudflare Worker
*/
async makeRequest(message) {
return new Promise((resolve) => {
const serverUrl = this.useStreamableHttp ? this.streamableHttpUrl : this.legacyServerUrl;
const url = new URL(serverUrl);
const postData = JSON.stringify(message);
const options = {
hostname: url.hostname,
port: url.port || 443,
path: url.pathname,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Accept-Encoding': 'identity',
'User-Agent': this.userAgent,
'X-API-Key': API_KEY,
'Content-Length': Buffer.byteLength(postData)
},
timeout: TIMEOUT_MS
};
const req = https.request(options, (res) => {
let body = '';
res.on('data', (chunk) => {
body += chunk;
});
res.on('end', () => {
try {
const response = JSON.parse(body);
resolve(response);
} catch (error) {
resolve({
jsonrpc: '2.0',
id: message.id || null,
error: {
code: -32603,
message: 'Invalid JSON response from server',
data: error.message
}
});
}
});
});
req.on('error', (error) => {
resolve({
jsonrpc: '2.0',
id: message.id || null,
error: {
code: -32603,
message: 'Network error',
data: `Failed to connect to ${serverUrl}: ${error.message}`
}
});
});
req.on('timeout', () => {
req.destroy();
resolve({
jsonrpc: '2.0',
id: message.id || null,
error: {
code: -32603,
message: 'Request timeout',
data: `Server did not respond within ${TIMEOUT_MS}ms`
}
});
});
req.write(postData);
req.end();
});
}
/**
* Graceful shutdown
*/
shutdown(reason = 'UNKNOWN') {
this.logDebug(`Shutting down (reason: ${reason})`);
process.exit(0);
}
}
// Handle command line arguments
if (process.argv.includes('--version') || process.argv.includes('-v')) {
console.log(`cf-memory-mcp v${PACKAGE_VERSION}`);
process.exit(0);
}
if (process.argv.includes('--help') || process.argv.includes('-h')) {
console.log(`
CF Memory MCP v${PACKAGE_VERSION}
A portable MCP (Model Context Protocol) server for AI memory storage using Cloudflare infrastructure.
Usage:
npx cf-memory-mcp Start the MCP server
npx cf-memory-mcp --version Show version
npx cf-memory-mcp --help Show this help
Environment Variables:
CF_MEMORY_API_KEY=<key> Your CF Memory API key (required)
DEBUG=1 Enable debug logging
MCP_DEBUG=1 Enable MCP debug logging
For more information, visit: https://github.com/johnlam90/cf-memory-mcp
`);
process.exit(0);
}
// Check API key before starting server
if (!API_KEY) {
console.error('Error: CF_MEMORY_API_KEY environment variable is required');
console.error('');
console.error('Please set your API key:');
console.error(' export CF_MEMORY_API_KEY="your-api-key-here"');
console.error('');
console.error('Or run with:');
console.error(' CF_MEMORY_API_KEY="your-api-key-here" npx cf-memory-mcp');
console.error('');
console.error('Get your API key from: https://cf-memory-mcp.johnlam90.workers.dev');
process.exit(1);
}
// Start the MCP server
const server = new CFMemoryMCP();
server.start().catch(error => {
console.error('Failed to start CF Memory MCP server:', error.message);
process.exit(1);
});