#!/usr/bin/env node
/**
 * Remote MCP Server for Memory Service
 * HTTP-based MCP server with Clerk OAuth authentication
 */
import { config } from 'dotenv';
import { resolve } from 'path';
import { existsSync } from 'fs';
import Fastify from 'fastify';
import cors from '@fastify/cors';
import { initDatabaseFromEnv } from './database/index.js';
import { MultiTenantMemoryCore } from './core/multi-tenant-memory-core.js';
import { MCPToolResultStatus } from './types/enums.js';
import { UsageTrackingDB } from './database/usage-tracking.js';
import { authenticateRequest, SessionManager, logAuthEvent, } from './middleware/mcp-auth.js';
// Load environment variables
const envLocalPath = resolve(process.cwd(), '.env.local');
const envPath = resolve(process.cwd(), '.env');
if (existsSync(envLocalPath)) {
    config({ path: envLocalPath });
}
else if (existsSync(envPath)) {
    config({ path: envPath });
}
/**
 * Remote MCP Server with HTTP transport and OAuth authentication
 */
export class RemoteMCPServer {
    fastify;
    memoryCore = null;
    db = null;
    sessionManager;
    debugEnabled = false;
    requestCounter = 0;
    port;
    host;
    constructor(port = 3001, host = '0.0.0.0') {
        this.port = port;
        this.host = host;
        this.debugEnabled = process.env.MCP_DEBUG === '1';
        this.sessionManager = new SessionManager();
        this.fastify = Fastify({
            logger: {
                level: process.env.LOG_LEVEL || 'info',
            },
        });
        this.setupMiddleware();
        this.setupRoutes();
    }
    logDebug(...args) {
        if (this.debugEnabled) {
            console.error('[Remote MCP DEBUG]', ...args);
        }
    }
    logError(...args) {
        console.error('[Remote MCP ERROR]', ...args);
    }
    ensureValidId(id) {
        if (id !== undefined && id !== null) {
            return id;
        }
        return `auto-${++this.requestCounter}`;
    }
    setupMiddleware() {
        // Register CORS
        this.fastify.register(cors, {
            origin: process.env.CORS_ORIGIN || true,
            credentials: true,
            methods: ['GET', 'POST', 'OPTIONS'],
        });
        // Authentication middleware for MCP endpoints
        this.fastify.addHook('preHandler', async (request, reply) => {
            // Skip auth for health check
            if (request.url === '/health' || request.url === '/') {
                return;
            }
            // All MCP endpoints require authentication
            const authResult = await authenticateRequest(request.headers.authorization, this.sessionManager);
            if (!authResult.authenticated) {
                logAuthEvent('failure', {
                    error: authResult.error,
                    url: request.url,
                    ip: request.ip,
                });
                reply.code(401).send({
                    jsonrpc: '2.0',
                    id: null,
                    error: {
                        code: -32001,
                        message: 'Authentication required',
                        data: { reason: authResult.error },
                    },
                });
                return;
            }
            // Attach user to request
            request.user = authResult.user;
            logAuthEvent('success', {
                email: authResult.user?.email,
                userId: authResult.user?.userId,
            });
        });
    }
    setupRoutes() {
        // Health check endpoint (no auth required)
        this.fastify.get('/health', async (request, reply) => {
            return {
                status: 'ok',
                timestamp: new Date().toISOString(),
                service: 'remote-mcp-server',
                activeSessions: this.sessionManager.getActiveSessions(),
            };
        });
        // Root endpoint (no auth required)
        this.fastify.get('/', async (request, reply) => {
            return {
                name: 'Remote MCP Memory Server',
                version: '1.0.0',
                description: 'HTTP-based MCP server with Clerk OAuth authentication',
                protocol: 'JSON-RPC 2.0',
            };
        });
        // Main MCP endpoint
        this.fastify.post('/mcp', async (request, reply) => {
            return this.handleMCPRequest(request, reply);
        });
        // MCP methods as individual endpoints (alternative to JSON-RPC)
        this.fastify.post('/mcp/initialize', async (request, reply) => {
            const rpcRequest = {
                jsonrpc: '2.0',
                id: 1,
                method: 'initialize',
                params: request.body,
            };
            return this.processRequest(rpcRequest, request.user);
        });
        this.fastify.get('/mcp/tools/list', async (request, reply) => {
            const rpcRequest = {
                jsonrpc: '2.0',
                id: 1,
                method: 'tools/list',
            };
            return this.processRequest(rpcRequest, request.user);
        });
        this.fastify.post('/mcp/tools/call', async (request, reply) => {
            const rpcRequest = {
                jsonrpc: '2.0',
                id: 1,
                method: 'tools/call',
                params: request.body,
            };
            return this.processRequest(rpcRequest, request.user);
        });
    }
    async handleMCPRequest(request, reply) {
        const rpcRequest = request.body;
        this.logDebug('MCP Request:', rpcRequest);
        const response = await this.processRequest(rpcRequest, request.user);
        this.logDebug('MCP Response:', response);
        return response;
    }
    async processRequest(request, user) {
        const { id, method, params } = request;
        try {
            const validId = this.ensureValidId(id);
            switch (method) {
                case 'initialize':
                    return this.handleInitialize(validId, params);
                case 'initialized':
                case 'notifications/initialized':
                    this.logDebug('Client initialized');
                    return { jsonrpc: '2.0', id: validId, result: {} };
                case 'tools/list':
                    return this.handleToolsList(validId);
                case 'tools/call':
                    return await this.handleToolsCall(validId, params, user);
                case 'prompts/list':
                    return { jsonrpc: '2.0', id: validId, result: { prompts: [] } };
                case 'resources/list':
                    return { jsonrpc: '2.0', id: validId, result: { resources: [] } };
                case 'ping':
                    return { jsonrpc: '2.0', id: validId, result: {} };
                default:
                    return {
                        jsonrpc: '2.0',
                        id: validId,
                        error: {
                            code: -32601,
                            message: `Method not found: ${method}`,
                        },
                    };
            }
        }
        catch (error) {
            this.logError('Error processing request:', error);
            return {
                jsonrpc: '2.0',
                id: this.ensureValidId(id),
                error: {
                    code: -32603,
                    message: 'Internal error',
                    data: String(error),
                },
            };
        }
    }
    handleInitialize(id, params) {
        this.logDebug('Initialize request:', params);
        const result = {
            protocolVersion: '2024-11-05',
            capabilities: {
                tools: {
                    listChanged: false,
                },
                logging: {},
            },
            serverInfo: {
                name: 'remote-mcp-memory-ts',
                version: '1.0.0',
            },
        };
        this.logDebug('Remote MCP server initialized');
        return {
            jsonrpc: '2.0',
            id,
            result,
        };
    }
    handleToolsList(id) {
        // Same tools as the stdio MCP server
        const tools = [
            {
                name: 'store_memory',
                description: 'Store a new memory',
                inputSchema: {
                    type: 'object',
                    properties: {
                        content: { type: 'string', description: 'The content to store in memory' },
                        type: {
                            type: 'string',
                            enum: ['episodic', 'semantic', 'procedural', 'working', 'sensory'],
                            description: 'Type of memory',
                            default: 'semantic',
                        },
                        importance: {
                            type: 'number',
                            minimum: 0,
                            maximum: 1,
                            description: 'Importance score (0-1)',
                            default: 0.5,
                        },
                        metadata: { type: 'object', description: 'Additional metadata' },
                    },
                    required: ['content'],
                },
            },
            {
                name: 'recall_memories',
                description: 'Retrieve memories based on a query',
                inputSchema: {
                    type: 'object',
                    properties: {
                        query: { type: 'string', description: 'Query string to search memories' },
                        limit: {
                            type: 'integer',
                            minimum: 1,
                            maximum: 100,
                            description: 'Maximum number of memories to retrieve',
                            default: 10,
                        },
                        strategy: {
                            type: 'string',
                            enum: ['recency', 'frequency', 'importance', 'similarity', 'composite'],
                            description: 'Recall strategy to use',
                            default: 'composite',
                        },
                        threshold: {
                            type: 'number',
                            minimum: 0,
                            maximum: 1,
                            description: 'Minimum relevance threshold',
                            default: 0.3,
                        },
                    },
                    required: ['query'],
                },
            },
            {
                name: 'get_memory',
                description: 'Retrieve a specific memory by ID',
                inputSchema: {
                    type: 'object',
                    properties: {
                        id: { type: 'string', description: 'Memory item ID' },
                    },
                    required: ['id'],
                },
            },
            {
                name: 'update_memory',
                description: 'Update an existing memory',
                inputSchema: {
                    type: 'object',
                    properties: {
                        id: { type: 'string', description: 'Memory item ID to update' },
                        title: { type: 'string', description: 'Updated title' },
                        content: { type: 'string', description: 'Updated content' },
                        importance: {
                            type: 'number',
                            minimum: 0,
                            maximum: 1,
                            description: 'Updated importance score',
                        },
                        tags: {
                            type: 'array',
                            items: { type: 'string' },
                            description: 'Updated tags',
                        },
                        metadata: { type: 'object', description: 'Updated metadata' },
                    },
                    required: ['id'],
                },
            },
            {
                name: 'delete_memory',
                description: 'Delete a memory by ID',
                inputSchema: {
                    type: 'object',
                    properties: {
                        id: { type: 'string', description: 'Memory item ID to delete' },
                    },
                    required: ['id'],
                },
            },
            {
                name: 'get_memory_stats',
                description: 'Get statistics about stored memories',
                inputSchema: {
                    type: 'object',
                    properties: {},
                },
            },
            {
                name: 'update_missing_embeddings',
                description: 'Manually trigger embedding generation for all memories without embeddings',
                inputSchema: {
                    type: 'object',
                    properties: {},
                },
            },
            {
                name: 'get_daily_costs',
                description: 'Get daily API usage costs for OpenAI embeddings. Returns token usage and costs for the current day or a specific date.',
                inputSchema: {
                    type: 'object',
                    properties: {
                        date: {
                            type: 'string',
                            description: 'Date in YYYY-MM-DD format (defaults to today)',
                        },
                    },
                },
            },
        ];
        return {
            jsonrpc: '2.0',
            id,
            result: { tools },
        };
    }
    async handleToolsCall(id, params, user) {
        const { name, arguments: args } = params;
        this.logDebug(`Tool call: ${name}`, args, `User: ${user.email}`);
        if (!this.memoryCore) {
            throw new Error('Memory core not initialized');
        }
        let resultText = '';
        try {
            switch (name) {
                case 'store_memory':
                    const inputType = args.type || 'semantic';
                    const importanceValue = args.importance !== undefined ? args.importance : 0.5;
                    const title = args.title || args.metadata?.title || 'Memory';
                    const tags = args.tags || args.metadata?.tags || [];
                    let metadataToStore;
                    if (args.metadata && typeof args.metadata === 'object') {
                        const { title: _title, tags: _tags, ...remainingMetadata } = args.metadata;
                        if (Object.keys(remainingMetadata).length > 0) {
                            metadataToStore = remainingMetadata;
                        }
                    }
                    const addResult = await this.memoryCore.addMemory(title, args.content, inputType, {
                        userId: user.email, // Use email as user identifier
                        tags: Array.isArray(tags) ? tags : [tags],
                        importance: importanceValue,
                        metadata: metadataToStore,
                        generateEmbedding: true,
                        useBuffer: false, // Direct write for remote MCP
                    });
                    if (addResult.status === MCPToolResultStatus.SUCCESS) {
                        const memoryData = addResult.data;
                        const embeddingStatus = memoryData?.hasEmbedding
                            ? '✅ with embedding'
                            : '⚠️  without embedding (will be generated)';
                        resultText = `✅ Memory stored successfully ${embeddingStatus}!\n\nID: ${memoryData?.id || 'unknown'}\nContent: ${args.content}\nType: ${inputType}\nImportance: ${importanceValue}\nUser: ${user.email}`;
                    }
                    else {
                        resultText = `❌ Failed to store memory: ${addResult.error || addResult.message}`;
                    }
                    break;
                case 'recall_memories':
                    const searchResult = await this.memoryCore.searchMemories(args.query, {
                        userId: user.email,
                        limit: args.limit || 10,
                        threshold: args.threshold || 0.3,
                        strategy: args.strategy || 'composite',
                    });
                    if (searchResult.status === MCPToolResultStatus.SUCCESS && searchResult.data) {
                        const memories = searchResult.data;
                        if (memories.length === 0) {
                            resultText = `🔍 No memories found for "${args.query}"`;
                        }
                        else {
                            resultText = `🔍 Found ${memories.length} memories for "${args.query}":\n\n`;
                            memories.forEach((memory, index) => {
                                const tagsDisplay = Array.isArray(memory.tags) && memory.tags.length > 0
                                    ? memory.tags.join(', ')
                                    : 'none';
                                const metadataDisplay = memory.metadata && Object.keys(memory.metadata).length > 0
                                    ? JSON.stringify(memory.metadata, null, 2)
                                    : 'none';
                                resultText += `${index + 1}. Memory ID: ${memory.id}\n   Content: ${memory.content.substring(0, 150)}${memory.content.length > 150 ? '...' : ''}\n   Type: ${memory.memoryType || 'semantic'}\n   Importance: ${memory.importance !== undefined ? memory.importance : 0.5}\n   Tags: ${tagsDisplay}\n   Metadata: ${metadataDisplay}\n   Created: ${new Date(memory.createdAt).toLocaleDateString()}\n\n`;
                            });
                        }
                    }
                    else {
                        resultText = `❌ Search failed: ${searchResult.error || searchResult.message}`;
                    }
                    break;
                case 'get_memory':
                    const getResult = await this.memoryCore.getMemory(args.id);
                    if (getResult.status === MCPToolResultStatus.SUCCESS && getResult.data) {
                        const memory = getResult.data;
                        // Verify user owns this memory
                        if (memory.userId !== user.email) {
                            resultText = `❌ Access denied: You do not own this memory`;
                            break;
                        }
                        const tagsDisplay = Array.isArray(memory.tags) && memory.tags.length > 0
                            ? memory.tags.join(', ')
                            : 'none';
                        const metadataDisplay = memory.metadata && Object.keys(memory.metadata).length > 0
                            ? JSON.stringify(memory.metadata, null, 2)
                            : 'none';
                        resultText = `📄 Memory Details:\n\nID: ${memory.id}\nContent: ${memory.content}\nType: ${memory.memoryType || 'semantic'}\nImportance: ${memory.importance !== undefined ? memory.importance : 0.5}\nCreated: ${new Date(memory.createdAt).toLocaleString()}\nTags: ${tagsDisplay}\nMetadata: ${metadataDisplay}`;
                    }
                    else {
                        resultText = `❌ Memory not found: ${getResult.error || getResult.message}`;
                    }
                    break;
                case 'update_memory':
                    const updates = {};
                    if (args.content !== undefined)
                        updates.content = args.content;
                    if (args.title !== undefined)
                        updates.title = args.title;
                    if (args.importance !== undefined)
                        updates.importance = args.importance;
                    if (args.tags !== undefined)
                        updates.tags = args.tags;
                    if (args.metadata !== undefined)
                        updates.metadata = args.metadata;
                    const memoryUpdateResult = await this.memoryCore.updateMemory(args.id, updates);
                    if (memoryUpdateResult.status === MCPToolResultStatus.SUCCESS) {
                        resultText = `✅ Memory ${args.id} updated successfully!`;
                    }
                    else {
                        resultText = `❌ Failed to update memory: ${memoryUpdateResult.error || memoryUpdateResult.message}`;
                    }
                    break;
                case 'delete_memory':
                    const deleteResult = await this.memoryCore.deleteMemory(args.id);
                    if (deleteResult.status === MCPToolResultStatus.SUCCESS) {
                        resultText = `✅ Memory ${args.id} deleted successfully!`;
                    }
                    else {
                        resultText = `❌ Failed to delete memory: ${deleteResult.error || deleteResult.message}`;
                    }
                    break;
                case 'get_memory_stats':
                    const statsResult = await this.memoryCore.getStatistics(user.email);
                    if (statsResult.status === MCPToolResultStatus.SUCCESS && statsResult.data) {
                        const stats = statsResult.data;
                        const memoryTypeBreakdown = stats.memoriesByType || {};
                        const typeLines = Object.entries(memoryTypeBreakdown)
                            .map(([type, count]) => `  - ${type}: ${count}`)
                            .join('\n');
                        const embeddingCoverage = Math.round(((stats.memoriesWithEmbeddings || 0) / Math.max(stats.totalMemories, 1)) * 100);
                        const embeddingStatus = embeddingCoverage === 100 ? '✅' : embeddingCoverage >= 90 ? '⚠️' : '❌';
                        resultText = `📊 Memory Statistics for ${user.email}:\n\n• Total Memories: ${stats.totalMemories}\n• Total Entities: ${stats.totalEntities}\n• Memories by Type:\n${typeLines || '  - None'}\n• Vector Embeddings: ${embeddingStatus} ${stats.memoriesWithEmbeddings || 0}/${stats.totalMemories} (${embeddingCoverage}%)\n\n${stats.vectorSearchHealth?.recommendation || ''}`;
                    }
                    else {
                        resultText = `❌ Failed to get statistics: ${statsResult.error || statsResult.message}`;
                    }
                    break;
                case 'update_missing_embeddings':
                    const embeddingUpdateResult = await this.memoryCore.updateMissingEmbeddings();
                    if (embeddingUpdateResult.status === MCPToolResultStatus.SUCCESS &&
                        embeddingUpdateResult.data) {
                        const updateStats = embeddingUpdateResult.data;
                        resultText = `✅ Embedding update completed!\n\n• Updated: ${updateStats.updated}\n• Failed: ${updateStats.failed}\n• Total processed: ${updateStats.updated + updateStats.failed}`;
                    }
                    else {
                        resultText = `❌ Failed to update embeddings: ${embeddingUpdateResult.error || embeddingUpdateResult.message}`;
                    }
                    break;
                case 'get_daily_costs':
                    try {
                        const date = args.date || new Date().toISOString().split('T')[0];
                        if (!this.db) {
                            resultText = '❌ Database not initialized';
                            break;
                        }
                        const usageTracker = new UsageTrackingDB(this.db);
                        const usage = await usageTracker.getDailyUsage(user.email, date);
                        const report = `📊 Daily API Cost Report - ${date} (${user.email})
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

OpenAI (Embeddings):
  • Requests: ${usage.openai.requests}
  • Tokens: ${usage.openai.tokens.toLocaleString()}
  • Cost: $${usage.openai.cost.toFixed(4)}

OpenRouter:
  • Requests: ${usage.openrouter.requests}
  • Tokens: ${usage.openrouter.tokens.toLocaleString()}
  • Cost: $${usage.openrouter.cost.toFixed(4)}

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Total Daily Cost: $${usage.total.cost.toFixed(4)}
Total Requests: ${usage.total.requests}
Total Tokens: ${usage.total.tokens.toLocaleString()}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`;
                        resultText = report;
                    }
                    catch (error) {
                        resultText = `❌ Failed to get daily costs: ${error instanceof Error ? error.message : String(error)}`;
                    }
                    break;
                default:
                    return {
                        jsonrpc: '2.0',
                        id,
                        error: {
                            code: -32601,
                            message: `Unknown tool: ${name}`,
                            data: { toolName: name },
                        },
                    };
            }
            const toolResult = {
                content: [
                    {
                        type: 'text',
                        text: resultText,
                    },
                ],
                isError: false,
            };
            return {
                jsonrpc: '2.0',
                id,
                result: toolResult,
            };
        }
        catch (error) {
            this.logError(`Tool call error for ${name}:`, error);
            return {
                jsonrpc: '2.0',
                id,
                error: {
                    code: -32603,
                    message: 'Tool execution failed',
                    data: { toolName: name, error: String(error) },
                },
            };
        }
    }
    async start() {
        try {
            this.logDebug('Starting Remote MCP Memory Server...');
            // Initialize database and memory core
            this.db = initDatabaseFromEnv();
            this.memoryCore = new MultiTenantMemoryCore(this.db, process.env.OPENAI_API_KEY);
            await this.memoryCore.initialize();
            this.logDebug('Multi-tenant memory core initialized successfully');
            // Start the server
            await this.fastify.listen({ port: this.port, host: this.host });
            this.fastify.log.info(`Remote MCP server listening on ${this.host}:${this.port}`);
        }
        catch (error) {
            this.logError('Failed to start Remote MCP server:', error);
            throw error;
        }
    }
    async stop() {
        if (this.memoryCore) {
            await this.memoryCore.close();
        }
        await this.fastify.close();
        this.logDebug('Remote MCP server stopped');
    }
    /**
     * Handle a single JSON-RPC request (for serverless/Vercel)
     */
    async handleRequest(rpcRequest, authHeader) {
        // Authenticate
        const authResult = await authenticateRequest(authHeader, this.sessionManager);
        if (!authResult.authenticated || !authResult.user) {
            return {
                jsonrpc: '2.0',
                id: this.ensureValidId(rpcRequest.id),
                error: {
                    code: -32001,
                    message: 'Authentication required',
                    data: { reason: authResult.error },
                },
            };
        }
        // Process request
        return this.processRequest(rpcRequest, authResult.user);
    }
}
// Start the server if this file is run directly
if (import.meta.url === `file://${process.argv[1]}`) {
    const port = process.env.REMOTE_MCP_PORT ? parseInt(process.env.REMOTE_MCP_PORT) : 3001;
    const host = process.env.REMOTE_MCP_HOST || '0.0.0.0';
    const server = new RemoteMCPServer(port, host);
    // Graceful shutdown
    process.on('SIGINT', async () => {
        console.log('Received SIGINT, shutting down gracefully...');
        await server.stop();
        process.exit(0);
    });
    process.on('SIGTERM', async () => {
        console.log('Received SIGTERM, shutting down gracefully...');
        await server.stop();
        process.exit(0);
    });
    server.start().catch((error) => {
        console.error('Failed to start Remote MCP server:', error);
        process.exit(1);
    });
}
//# sourceMappingURL=remote-mcp-server.js.map