Ctrl K
ComunidadGuías…

test.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env node

/**
 * Quick test -- verifies the MCP server starts, responds to protocol
 * handshake, and can make a read-only API call to Follow Up Boss.
 *
 * Usage: npm test
 */

import { Client } from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';
import { readFileSync } from 'fs';

const __dirname = dirname(fileURLToPath(import.meta.url));
const serverPath = resolve(__dirname, 'index.js');

// Preload .env so a stale parent-shell FUB_API_KEY (e.g. "YOUR_KEY_HERE"
// from a setup wizard) can't shadow the local key.
function loadDotEnv() {
  const out = {};
  try {
    for (const line of readFileSync(resolve(__dirname, '.env'), 'utf8').split('\n')) {
      const t = line.trim();
      if (!t || t.startsWith('#')) continue;
      const eq = t.indexOf('=');
      if (eq > -1) out[t.slice(0, eq).trim()] = t.slice(eq + 1).trim();
    }
  } catch {}
  return out;
}
const DOTENV = loadDotEnv();

let passed = 0;
let failed = 0;

function ok(label) {
  passed++;
  console.log(`  PASS  ${label}`);
}

function fail(label, err) {
  failed++;
  console.error(`  FAIL  ${label}: ${err}`);
}

async function run() {
  console.log('\nFollow Up Boss MCP Server -- Quick Test\n');

  // 1. Start server and connect
  let client;
  try {
    const transport = new StdioClientTransport({
      command: 'node',
      args: [serverPath],
      env: { ...process.env, ...DOTENV },
    });
    client = new Client({ name: 'fub-test', version: '1.0.0' });
    await client.connect(transport);
    ok('Server starts and MCP handshake succeeds');
  } catch (err) {
    fail('Server starts and MCP handshake succeeds', err.message);
    console.error('\nCannot continue without a connection. Check your .env or FUB_API_KEY.\n');
    process.exit(1);
  }

  // 2. List tools
  let tools;
  try {
    const result = await client.listTools();
    tools = result.tools;
    if (tools.length >= 130) {
      ok(`Lists ${tools.length} tools`);
    } else {
      fail(`Lists tools`, `Expected 130+, got ${tools.length}`);
    }
  } catch (err) {
    fail('Lists tools', err.message);
  }

  // 3. Read-only API call -- get account identity
  try {
    const result = await client.callTool({ name: 'getIdentity', arguments: {} });
    const content = result.content?.[0]?.text;
    if (content) {
      const data = JSON.parse(content);
      // An error payload is still a non-empty object, so check for it explicitly
      // before looking at the identity fields.
      if (result.isError || data.error || data.status >= 400) {
        fail('Read-only API call', data.error || `HTTP ${data.status}`);
      } else {
        // FUB identity can return nested or flat -- require a real identifying field.
        // Shapes seen in the wild: flat, { identity: {...} }, and { account: { id, domain, owner } }.
        const flat = data.userId || data.id || data.email || data.accountId;
        const nested = data.identity?.userId || data.identity?.email || data.account?.id || data.account?.owner?.email;
        const label = data.email || data.identity?.email || data.name || data.identity?.name || data.account?.name || 'ok';
        if (flat || nested) {
          ok(`Read-only API call works (account: ${label})`);
        } else {
          fail('Read-only API call', `No identity fields in response: ${JSON.stringify(data).slice(0, 120)}`);
        }
      }
    } else {
      fail('Read-only API call', 'No content returned');
    }
  } catch (err) {
    fail('Read-only API call', err.message);
  }

  // Done
  console.log(`\n${passed} passed, ${failed} failed\n`);
  await client.close();
  process.exit(failed > 0 ? 1 : 0);
}

run().catch((err) => {
  console.error('Test crashed:', err);
  process.exit(1);
});