#!/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); });