test.js
Rama main · réplica de lectura del repositorio original.
#!/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);
});