/**
* MCP Server Integration Tests for Reddit MCP
*
* These tests verify the MCP server protocol compliance and tool schemas.
* They spawn the actual MCP server and test via JSON-RPC.
*/
import { describe, test, expect } from 'bun:test';
import { spawn } from 'child_process';
import * as path from 'path';
describe('Reddit MCP Server Integration Tests', () => {
const sendRequest = (request: object): Promise<any> => {
return new Promise((resolve, reject) => {
const proc = spawn('node', [path.join(__dirname, '../build/index.js')], {
env: { ...process.env },
stdio: ['pipe', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
proc.stdout?.on('data', (data) => {
stdout += data.toString();
});
proc.stderr?.on('data', (data) => {
stderr += data.toString();
});
proc.on('close', () => {
try {
const lines = stdout.trim().split('\n');
for (const line of lines) {
if (line.startsWith('{')) {
const response = JSON.parse(line);
resolve(response);
return;
}
}
resolve({ stdout, stderr });
} catch (e) {
resolve({ stdout, stderr, error: e });
}
});
proc.on('error', reject);
proc.stdin?.write(JSON.stringify(request) + '\n');
proc.stdin?.end();
setTimeout(() => {
proc.kill();
reject(new Error('Request timeout'));
}, 30000);
});
};
describe('tools/list endpoint', () => {
test('returns all 9 tools', async () => {
const response = await sendRequest({
jsonrpc: '2.0',
id: 1,
method: 'tools/list',
params: {},
});
expect(response.result).toBeDefined();
expect(response.result.tools).toBeInstanceOf(Array);
expect(response.result.tools.length).toBe(9);
});
test('includes get_subreddit_hot tool with correct schema', async () => {
const response = await sendRequest({
jsonrpc: '2.0',
id: 1,
method: 'tools/list',
params: {},
});
const tools = response.result.tools;
const tool = tools.find((t: any) => t.name === 'get_subreddit_hot');
expect(tool).toBeDefined();
expect(tool.inputSchema.required).toContain('subreddit');
expect(tool.inputSchema.properties).toHaveProperty('subreddit');
expect(tool.inputSchema.properties).toHaveProperty('limit');
expect(tool.inputSchema.properties).toHaveProperty('after');
expect(tool.inputSchema.properties).toHaveProperty('save_dir');
expect(tool.inputSchema.properties).toHaveProperty('max_items');
});
test('includes get_subreddit_new tool', async () => {
const response = await sendRequest({
jsonrpc: '2.0',
id: 1,
method: 'tools/list',
params: {},
});
const tools = response.result.tools;
const tool = tools.find((t: any) => t.name === 'get_subreddit_new');
expect(tool).toBeDefined();
expect(tool.inputSchema.required).toContain('subreddit');
});
test('includes get_subreddit_top tool with time filter', async () => {
const response = await sendRequest({
jsonrpc: '2.0',
id: 1,
method: 'tools/list',
params: {},
});
const tools = response.result.tools;
const tool = tools.find((t: any) => t.name === 'get_subreddit_top');
expect(tool).toBeDefined();
expect(tool.inputSchema.required).toContain('subreddit');
expect(tool.inputSchema.properties).toHaveProperty('time');
expect(tool.inputSchema.properties.time.enum).toContain('hour');
expect(tool.inputSchema.properties.time.enum).toContain('day');
expect(tool.inputSchema.properties.time.enum).toContain('week');
expect(tool.inputSchema.properties.time.enum).toContain('month');
expect(tool.inputSchema.properties.time.enum).toContain('year');
expect(tool.inputSchema.properties.time.enum).toContain('all');
});
test('includes get_post_content tool with comment options', async () => {
const response = await sendRequest({
jsonrpc: '2.0',
id: 1,
method: 'tools/list',
params: {},
});
const tools = response.result.tools;
const tool = tools.find((t: any) => t.name === 'get_post_content');
expect(tool).toBeDefined();
expect(tool.inputSchema.required).toContain('post_id');
expect(tool.inputSchema.properties).toHaveProperty('subreddit');
expect(tool.inputSchema.properties).toHaveProperty('comment_limit');
expect(tool.inputSchema.properties).toHaveProperty('comment_depth');
expect(tool.inputSchema.properties).toHaveProperty('sort');
expect(tool.inputSchema.properties.sort.enum).toContain('confidence');
expect(tool.inputSchema.properties.sort.enum).toContain('top');
expect(tool.inputSchema.properties.sort.enum).toContain('new');
});
test('includes search_posts tool with filters', async () => {
const response = await sendRequest({
jsonrpc: '2.0',
id: 1,
method: 'tools/list',
params: {},
});
const tools = response.result.tools;
const tool = tools.find((t: any) => t.name === 'search_posts');
expect(tool).toBeDefined();
expect(tool.inputSchema.required).toContain('query');
expect(tool.inputSchema.properties).toHaveProperty('subreddit');
expect(tool.inputSchema.properties).toHaveProperty('sort');
expect(tool.inputSchema.properties).toHaveProperty('time');
expect(tool.inputSchema.properties).toHaveProperty('limit');
expect(tool.inputSchema.properties.sort.enum).toContain('relevance');
expect(tool.inputSchema.properties.sort.enum).toContain('hot');
expect(tool.inputSchema.properties.sort.enum).toContain('top');
expect(tool.inputSchema.properties.sort.enum).toContain('new');
expect(tool.inputSchema.properties.sort.enum).toContain('comments');
});
test('includes get_user_info tool', async () => {
const response = await sendRequest({
jsonrpc: '2.0',
id: 1,
method: 'tools/list',
params: {},
});
const tools = response.result.tools;
const tool = tools.find((t: any) => t.name === 'get_user_info');
expect(tool).toBeDefined();
expect(tool.inputSchema.required).toContain('username');
});
test('includes get_user_posts tool with sort options', async () => {
const response = await sendRequest({
jsonrpc: '2.0',
id: 1,
method: 'tools/list',
params: {},
});
const tools = response.result.tools;
const tool = tools.find((t: any) => t.name === 'get_user_posts');
expect(tool).toBeDefined();
expect(tool.inputSchema.required).toContain('username');
expect(tool.inputSchema.properties).toHaveProperty('sort');
expect(tool.inputSchema.properties.sort.enum).toContain('hot');
expect(tool.inputSchema.properties.sort.enum).toContain('new');
expect(tool.inputSchema.properties.sort.enum).toContain('top');
expect(tool.inputSchema.properties.sort.enum).toContain('controversial');
});
test('includes get_user_comments tool', async () => {
const response = await sendRequest({
jsonrpc: '2.0',
id: 1,
method: 'tools/list',
params: {},
});
const tools = response.result.tools;
const tool = tools.find((t: any) => t.name === 'get_user_comments');
expect(tool).toBeDefined();
expect(tool.inputSchema.required).toContain('username');
expect(tool.inputSchema.properties).toHaveProperty('sort');
expect(tool.inputSchema.properties).toHaveProperty('time');
expect(tool.inputSchema.properties).toHaveProperty('limit');
});
test('includes get_subreddit_info tool', async () => {
const response = await sendRequest({
jsonrpc: '2.0',
id: 1,
method: 'tools/list',
params: {},
});
const tools = response.result.tools;
const tool = tools.find((t: any) => t.name === 'get_subreddit_info');
expect(tool).toBeDefined();
expect(tool.inputSchema.required).toContain('subreddit');
});
test('all tools have name and description', async () => {
const response = await sendRequest({
jsonrpc: '2.0',
id: 1,
method: 'tools/list',
params: {},
});
const tools = response.result.tools;
for (const tool of tools) {
expect(tool).toHaveProperty('name');
expect(tool).toHaveProperty('description');
expect(tool).toHaveProperty('inputSchema');
expect(tool.name).toBeTruthy();
expect(tool.description).toBeTruthy();
}
});
test('tool names follow snake_case convention', async () => {
const response = await sendRequest({
jsonrpc: '2.0',
id: 1,
method: 'tools/list',
params: {},
});
const tools = response.result.tools;
const snakeCaseRegex = /^[a-z]+(_[a-z]+)*$/;
for (const tool of tools) {
expect(snakeCaseRegex.test(tool.name)).toBe(true);
}
});
test('all tools mention TOON format in description', async () => {
const response = await sendRequest({
jsonrpc: '2.0',
id: 1,
method: 'tools/list',
params: {},
});
const tools = response.result.tools;
for (const tool of tools) {
expect(tool.description.toLowerCase()).toContain('toon');
}
});
});
describe('Error handling', () => {
test('returns error for unknown tool', async () => {
const response = await sendRequest({
jsonrpc: '2.0',
id: 2,
method: 'tools/call',
params: {
name: 'unknown_tool',
arguments: {},
},
});
expect(response.error || response.result).toBeDefined();
});
test('returns error for missing arguments', async () => {
const response = await sendRequest({
jsonrpc: '2.0',
id: 3,
method: 'tools/call',
params: {
name: 'get_subreddit_hot',
},
});
expect(response.error || response.result).toBeDefined();
});
});
});
describe('Tool Schema Validation', () => {
const sendRequest = (request: object): Promise<any> => {
return new Promise((resolve, reject) => {
const proc = spawn('node', [path.join(__dirname, '../build/index.js')], {
env: { ...process.env },
stdio: ['pipe', 'pipe', 'pipe'],
});
let stdout = '';
proc.stdout?.on('data', (data) => {
stdout += data.toString();
});
proc.on('close', () => {
try {
const lines = stdout.trim().split('\n');
for (const line of lines) {
if (line.startsWith('{')) {
resolve(JSON.parse(line));
return;
}
}
resolve({});
} catch (e) {
resolve({});
}
});
proc.on('error', reject);
proc.stdin?.write(JSON.stringify(request) + '\n');
proc.stdin?.end();
setTimeout(() => {
proc.kill();
reject(new Error('Timeout'));
}, 30000);
});
};
test('subreddit tools have consistent schema structure', async () => {
const response = await sendRequest({
jsonrpc: '2.0',
id: 1,
method: 'tools/list',
params: {},
});
const subredditTools = ['get_subreddit_hot', 'get_subreddit_new', 'get_subreddit_top'];
const tools = response.result.tools;
for (const toolName of subredditTools) {
const tool = tools.find((t: any) => t.name === toolName);
expect(tool).toBeDefined();
expect(tool.inputSchema.properties).toHaveProperty('subreddit');
expect(tool.inputSchema.properties).toHaveProperty('limit');
expect(tool.inputSchema.properties).toHaveProperty('after');
expect(tool.inputSchema.properties).toHaveProperty('save_dir');
expect(tool.inputSchema.properties).toHaveProperty('max_items');
}
});
test('user tools have consistent schema structure', async () => {
const response = await sendRequest({
jsonrpc: '2.0',
id: 1,
method: 'tools/list',
params: {},
});
const userTools = ['get_user_info', 'get_user_posts', 'get_user_comments'];
const tools = response.result.tools;
for (const toolName of userTools) {
const tool = tools.find((t: any) => t.name === toolName);
expect(tool).toBeDefined();
expect(tool.inputSchema.required).toContain('username');
expect(tool.inputSchema.properties).toHaveProperty('username');
}
});
test('pagination tools have after parameter', async () => {
const response = await sendRequest({
jsonrpc: '2.0',
id: 1,
method: 'tools/list',
params: {},
});
const paginatedTools = [
'get_subreddit_hot',
'get_subreddit_new',
'get_subreddit_top',
'search_posts',
'get_user_posts',
'get_user_comments',
];
const tools = response.result.tools;
for (const toolName of paginatedTools) {
const tool = tools.find((t: any) => t.name === toolName);
expect(tool).toBeDefined();
expect(tool.inputSchema.properties).toHaveProperty('after');
}
});
});