jokes-mcp.js•4.21 kB
#!/usr/bin/env node
import readline from 'node:readline';
import { performance } from 'node:perf_hooks';
import { loadConfig } from './config.js';
import { validateGetJokeInput } from './validation.js';
import { logEvent, logError } from './logger.js';
import * as jokeApiProvider from './providers/jokeapi.js';
import * as officialProvider from './providers/official.js';
import * as localProvider from './providers/local.js';
const config = loadConfig(process.env);
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false });
const PROVIDER_CHAIN = buildProviderChain(config.provider);
rl.on('line', async (line) => {
const trimmed = line.trim();
if (!trimmed) return;
if (trimmed === 'health') {
respond({ ok: true });
return;
}
if (trimmed.startsWith('getJoke')) {
const payloadText = extractPayload(trimmed);
try {
const payload = payloadText ? JSON.parse(payloadText) : undefined;
const preferences = validateGetJokeInput(payload, {
defaultCategory: config.defaultCategory,
lang: config.lang,
});
const result = await resolveJoke(preferences, config);
respond(result);
} catch (error) {
const printable = toResponseError(error);
respond(printable);
}
return;
}
respond({ ok: false, error: `Unknown command: ${trimmed}` });
});
rl.on('close', () => {
process.exit(0);
});
process.on('SIGINT', () => {
rl.close();
});
async function resolveJoke(preferences, cfg) {
const verbose = Boolean(cfg.verbose);
const infoLog = verbose ? logEvent : noop;
const errorLog = verbose ? logError : noop;
const attempts = [];
for (const providerName of PROVIDER_CHAIN) {
const start = performance.now();
if (!cfg.allowNet && providerName !== 'local') {
const latency = Math.round(performance.now() - start);
const message = 'Network access disabled';
attempts.push({ provider: providerName, message, latency });
infoLog({
event: 'provider_skipped',
provider: providerName,
reason: message,
latency_ms: latency,
fallback_depth: attempts.length,
});
continue;
}
try {
const provider = getProvider(providerName);
const result = await provider.getJoke(preferences, {
timeoutMs: cfg.timeoutMs,
retries: cfg.retries,
allowNet: cfg.allowNet,
});
const latency = Math.round(performance.now() - start);
const response = {
text: result.text,
source: result.source ?? providerName,
category: result.category ?? preferences.category,
latency_ms: latency,
};
infoLog({
event: 'getJoke',
provider: response.source,
latency_ms: latency,
fallback_depth: attempts.length,
});
return response;
} catch (error) {
const latency = Math.round(performance.now() - start);
attempts.push({ provider: providerName, message: error.message, latency });
errorLog(error, { event: 'provider_failure', provider: providerName, latency_ms: latency });
}
}
const failure = new Error('All joke providers failed');
failure.details = attempts;
throw failure;
}
function buildProviderChain(primary) {
if (primary === 'jokeapi') {
return ['jokeapi', 'official', 'local'];
}
return ['official', 'jokeapi', 'local'];
}
function getProvider(name) {
switch (name) {
case 'jokeapi':
return jokeApiProvider;
case 'official':
return officialProvider;
case 'local':
return localProvider;
default:
throw new Error(`Unknown provider ${name}`);
}
}
function extractPayload(commandLine) {
const firstSpace = commandLine.indexOf(' ');
if (firstSpace === -1) return '';
return commandLine.slice(firstSpace + 1).trim();
}
function respond(payload) {
process.stdout.write(`${JSON.stringify(payload)}\n`);
}
function toResponseError(error) {
const info = {
ok: false,
error: error?.message ?? 'Unknown error',
};
if (error?.details) {
info.details = error.details;
}
return info;
}
export { resolveJoke, buildProviderChain };
function noop() {}