/**
* MCP Server Smoke Test — Using MCP SDK Client
*/
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const serverPath = path.join(__dirname, '..', 'dist', 'index.js');
async function runTests(): Promise<void> {
console.log('=== Roku MCP Server Smoke Test ===\n');
const results: { test: string; pass: boolean; detail: string }[] = [];
// Start server via MCP Client
const transport = new StdioClientTransport({
command: 'node',
args: [serverPath],
env: { ...process.env, ROKU_DEV_HOST: '', ROKU_DEV_PASSWORD: '' } as Record<string, string>,
});
const client = new Client({ name: 'smoke-test', version: '1.0.0' });
// ── Test 1: Connect / Initialize ──
try {
await client.connect(transport);
results.push({ test: 'Server Initialize', pass: true, detail: 'Connected successfully' });
} catch (err) {
results.push({ test: 'Server Initialize', pass: false, detail: String(err) });
printResults(results);
process.exit(1);
}
// ── Test 2: List Tools ──
try {
const toolsList = await client.listTools();
const toolCount = toolsList.tools.length;
const toolNames = toolsList.tools.map(t => t.name);
results.push({
test: `List Tools (expect ≥23)`,
pass: toolCount >= 23,
detail: `${toolCount} tools: ${toolNames.join(', ')}`,
});
} catch (err) {
results.push({ test: 'List Tools', pass: false, detail: String(err) });
}
// ── Test 3: List Resources ──
try {
const resourcesList = await client.listResources();
const resCount = resourcesList.resources.length;
const resNames = resourcesList.resources.map(r => r.name);
results.push({
test: `List Resources (expect ≥2)`,
pass: resCount >= 2,
detail: `${resCount} resources: ${resNames.join(', ')}`,
});
} catch (err) {
results.push({ test: 'List Resources', pass: false, detail: String(err) });
}
// ── Test 4: roku_check_raf (local manifest analysis — no device needed) ──
try {
const rafResult = await client.callTool({ name: 'roku_check_raf', arguments: {} });
const text = (rafResult.content as Array<{ type: string; text?: string }>)[0]?.text || '';
const hasInfo = text.includes('RAF_ENABLED') || text.includes('Manifest');
results.push({
test: 'roku_check_raf (manifest analysis)',
pass: hasInfo,
detail: text.substring(0, 300),
});
} catch (err) {
results.push({ test: 'roku_check_raf', pass: false, detail: String(err) });
}
// ── Test 5: roku_check_resolution (partial — manifest + local files) ──
try {
const resResult = await client.callTool({ name: 'roku_check_resolution', arguments: {} });
const text = (resResult.content as Array<{ type: string; text?: string }>)[0]?.text || '';
// Even without device, it should at least check local manifest
const isError = resResult.isError === true;
results.push({
test: 'roku_check_resolution',
pass: !isError || text.length > 0,
detail: text.substring(0, 300),
});
} catch (err) {
results.push({ test: 'roku_check_resolution', pass: false, detail: String(err) });
}
// ── Test 6: roku_server_check (HTTP to app server) ──
try {
const serverResult = await client.callTool({ name: 'roku_server_check', arguments: {} });
const text = (serverResult.content as Array<{ type: string; text?: string }>)[0]?.text || '';
const hasResponse = text.includes('Status:') || text.includes('URL:');
results.push({
test: 'roku_server_check (HTTP)',
pass: hasResponse,
detail: text.substring(0, 300),
});
} catch (err) {
results.push({ test: 'roku_server_check', pass: false, detail: String(err) });
}
// ── Test 7: Tool descriptions contain Roku API references ──
try {
const toolsList = await client.listTools();
const toolsWithRef = toolsList.tools.filter(t =>
t.description?.includes('Roku API') || t.description?.includes('developer.roku.com')
);
results.push({
test: 'Self-documenting (API refs in descriptions)',
pass: toolsWithRef.length >= 15,
detail: `${toolsWithRef.length}/${toolsList.tools.length} tools have Roku API references`,
});
} catch (err) {
results.push({ test: 'Self-documenting', pass: false, detail: String(err) });
}
// ── Print Results ──
printResults(results);
// Cleanup
await client.close();
process.exit(results.some(r => !r.pass) ? 1 : 0);
}
function printResults(results: { test: string; pass: boolean; detail: string }[]): void {
console.log('\n=== Test Results ===\n');
let passed = 0, failed = 0;
for (const r of results) {
const icon = r.pass ? '✅' : '❌';
console.log(`${icon} ${r.test}`);
// Show first line of detail only
const detailLines = r.detail.split('\n');
console.log(` ${detailLines[0]}`);
if (detailLines.length > 1) console.log(` ${detailLines[1]}`);
console.log();
if (r.pass) passed++; else failed++;
}
console.log(`Total: ${passed} passed, ${failed} failed out of ${results.length}\n`);
}
runTests().catch((err) => {
console.error('Test runner failed:', err);
process.exit(1);
});