#!/usr/bin/env node
/**
 * VLCode MCP Server — exposes VLCode tools to Claude CLI via MCP protocol.
 *
 * This is a lightweight proxy that:
 *   1. Fetches available tool schemas from VLCode's HTTP API on startup
 *   2. Exposes them via MCP protocol (stdio JSON-RPC transport)
 *   3. Forwards tool/call requests to VLCode's /api/tools/execute endpoint
 *   4. Returns results in MCP format
 *
 * Usage:
 *   node mcp-server.js --port <vlcode-port>
 *
 * The VLCode instance must already be running on the specified port.
 * Tool execution happens locally (same machine), so latency is minimal.
 */
import http from 'http';
import { createInterface } from 'readline';

// ─── Parse CLI args ──────────────────────────────────────────────────────────
const args = process.argv.slice(2);
const portIdx = args.indexOf('--port');
const PORT = portIdx >= 0 ? parseInt(args[portIdx + 1]) : 3200;
const BASE_URL = `http://localhost:${PORT}`;

// Suppress proxy for localhost
process.env.NO_PROXY = (process.env.NO_PROXY || '') + ',localhost,127.0.0.1,::1';

// ─── HTTP Helpers ────────────────────────────────────────────────────────────

function httpGet(urlPath) {
  return new Promise((resolve, reject) => {
    const req = http.get(`${BASE_URL}${urlPath}`, {
      headers: { Accept: 'application/json' },
    }, res => {
      let data = '';
      res.on('data', chunk => { data += chunk; });
      res.on('end', () => {
        try { resolve(JSON.parse(data)); }
        catch { reject(new Error(`Invalid JSON from ${urlPath}: ${data.slice(0, 200)}`)); }
      });
    });
    req.on('error', reject);
    req.setTimeout(10000, () => { req.destroy(); reject(new Error(`Timeout: ${urlPath}`)); });
  });
}

function httpPost(urlPath, body) {
  return new Promise((resolve, reject) => {
    const payload = JSON.stringify(body);
    const req = http.request(`${BASE_URL}${urlPath}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Content-Length': Buffer.byteLength(payload),
      },
    }, res => {
      let data = '';
      res.on('data', chunk => { data += chunk; });
      res.on('end', () => {
        try { resolve(JSON.parse(data)); }
        catch { reject(new Error(`Invalid JSON from POST ${urlPath}: ${data.slice(0, 200)}`)); }
      });
    });
    req.on('error', reject);
    req.setTimeout(120000, () => { req.destroy(); reject(new Error(`Timeout: POST ${urlPath}`)); });
    req.write(payload);
    req.end();
  });
}

// ─── MCP Protocol Handler ────────────────────────────────────────────────────

let toolSchemas = []; // Cached from VLCode API
let toolsFetchPromise = null; // For awaiting in tools/list if fetch is in progress

function send(obj) {
  process.stdout.write(JSON.stringify(obj) + '\n');
}

function sendResult(id, result) {
  send({ jsonrpc: '2.0', id, result });
}

function sendError(id, code, message) {
  send({ jsonrpc: '2.0', id, error: { code, message } });
}

async function handleMessage(msg) {
  const { id, method, params } = msg;

  switch (method) {
    // ── Lifecycle ──
    case 'initialize':
      sendResult(id, {
        protocolVersion: '2024-11-05',
        capabilities: { tools: {} },
        serverInfo: { name: 'vlcode', version: '1.20.0' },
      });
      break;

    case 'notifications/initialized':
      // Notification (no response). Start fetching tool schemas from VLCode.
      // Store the promise so tools/list can await it if needed (race condition fix).
      toolsFetchPromise = (async () => {
        try {
          toolSchemas = await httpGet('/api/tools/list');
          if (!Array.isArray(toolSchemas)) toolSchemas = [];
          process.stderr.write(`[vlcode-mcp] Loaded ${toolSchemas.length} tools from VLCode\n`);
        } catch (e) {
          process.stderr.write(`[vlcode-mcp] Failed to fetch tools from VLCode: ${e.message}\n`);
          toolSchemas = [];
        }
      })();
      break;

    // ── Tools ──
    case 'tools/list':
      // Wait for tool fetch to complete if still in progress
      if (toolsFetchPromise) await toolsFetchPromise;
      sendResult(id, {
        tools: toolSchemas.map(t => ({
          name: t.name,
          description: t.description || '',
          inputSchema: t.input_schema || t.inputSchema || { type: 'object', properties: {} },
        })),
      });
      break;

    case 'tools/call': {
      const toolName = params?.name;
      const toolArgs = params?.arguments || {};

      if (!toolName) {
        sendResult(id, { content: [{ type: 'text', text: 'Error: missing tool name' }], isError: true });
        break;
      }

      try {
        const result = await httpPost('/api/tools/execute', { name: toolName, input: toolArgs });

        if (result.error) {
          sendResult(id, {
            content: [{ type: 'text', text: `Error: ${result.error}` }],
            isError: true,
          });
        } else {
          const text = typeof result.result === 'string'
            ? result.result
            : JSON.stringify(result.result, null, 2);
          sendResult(id, {
            content: [{ type: 'text', text }],
          });
        }
      } catch (e) {
        sendResult(id, {
          content: [{ type: 'text', text: `Tool execution failed: ${e.message}` }],
          isError: true,
        });
      }
      break;
    }

    // ── Unknown ──
    default:
      if (id !== undefined) {
        sendError(id, -32601, `Method not found: ${method}`);
      }
      // Notifications (no id) for unknown methods are silently ignored
      break;
  }
}

// ─── Main: stdio JSON-RPC transport ──────────────────────────────────────────

const rl = createInterface({ input: process.stdin, terminal: false });

rl.on('line', async (line) => {
  const trimmed = line.trim();
  if (!trimmed) return;

  try {
    const msg = JSON.parse(trimmed);
    await handleMessage(msg);
  } catch (e) {
    process.stderr.write(`[vlcode-mcp] Parse error: ${e.message} | line: ${trimmed.slice(0, 100)}\n`);
  }
});

rl.on('close', () => {
  process.exit(0);
});

// Graceful shutdown
process.on('SIGTERM', () => process.exit(0));
process.on('SIGINT', () => process.exit(0));
