// ABOUTME: Comprehensive MCP SSE/HTTP integration test suite
// ABOUTME: Tests all transport modes, protocols, and newly implemented features
import http from 'node:http';
import { afterAll, beforeAll, describe, expect, test } from '@jest/globals';
// Use Node.js built-in fetch (available in Node 18+)
const fetch = globalThis.fetch;
interface TestSession {
sessionId: string;
initialized: boolean;
}
class McpSseClient {
private baseUrl: string;
private session: TestSession;
constructor(baseUrl = 'http://localhost:3000') {
this.baseUrl = baseUrl;
this.session = {
sessionId: `test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
initialized: false,
};
}
async makeRequest(
method: string,
params: Record<string, unknown> = {},
id = 1,
): Promise<unknown> {
return new Promise((resolve, reject) => {
const data = JSON.stringify({
jsonrpc: '2.0',
id,
method,
params,
});
const url = new URL('/mcp', this.baseUrl);
const options = {
hostname: url.hostname,
port: url.port || (url.protocol === 'https:' ? 443 : 80),
path: url.pathname,
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json, text/event-stream',
'Mcp-Session-Id': this.session.sessionId,
'Content-Length': Buffer.byteLength(data),
},
};
const req = http.request(options, (res) => {
let body = '';
res.on('data', (chunk) => {
body += chunk;
});
res.on('end', () => {
// Update session ID if server provides one
if (res.headers['mcp-session-id']) {
this.session.sessionId = res.headers['mcp-session-id'] as string;
}
try {
let responseData: unknown;
// Handle SSE format
if (body.startsWith('event:')) {
const lines = body.split('\n');
const dataLine = lines.find((line) => line.startsWith('data:'));
if (dataLine) {
responseData = JSON.parse(dataLine.substring(5));
} else {
throw new Error('No data line found in SSE response');
}
} else {
// Handle regular JSON
responseData = JSON.parse(body);
}
if (responseData.error) {
const error = new Error(responseData.error.message);
(error as unknown as { code: unknown; data: unknown }).code = responseData.error.code;
(error as unknown as { code: unknown; data: unknown }).data = responseData.error.data;
reject(error);
} else {
resolve({
status: res.statusCode,
headers: res.headers,
data: responseData,
});
}
} catch (error) {
reject(new Error(`Failed to parse response: ${error}. Body: ${body}`));
}
});
});
req.on('error', reject);
req.write(data);
req.end();
});
}
async initialize(): Promise<void> {
const response = await this.makeRequest('initialize', {
protocolVersion: '2024-11-05',
capabilities: {
tools: {},
resources: {},
prompts: {},
sampling: {},
},
clientInfo: {
name: 'mcp-integration-test',
version: '1.0.3',
},
});
expect(response.status).toBe(200);
expect(response.data.result.protocolVersion).toBe('2024-11-05');
expect(response.data.result.serverInfo.name).toBe('mcp-agent-social');
this.session.initialized = true;
}
}
// Helper function to check if server is running
async function isServerRunning(url: string): Promise<boolean> {
try {
const response = await fetch(`${url}/mcp`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: '{}',
});
return response.status !== undefined;
} catch {
return false;
}
}
describe('MCP SSE Integration Tests', () => {
let client: McpSseClient;
let serverRunning = false;
beforeAll(async () => {
const testUrl = process.env.TEST_URL || 'http://localhost:3000';
serverRunning = await isServerRunning(testUrl);
if (serverRunning) {
client = new McpSseClient(testUrl);
// Wait a bit for server to be fully ready
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log('✅ Using existing server on', testUrl);
} else {
// If TEST_SERVER_AUTO_START is set, these tests should always run
if (process.env.TEST_SERVER_AUTO_START === 'true') {
throw new Error(
`❌ Expected server to be running on ${testUrl} but it's not available. Check test setup.`,
);
}
console.warn('⚠️ HTTP MCP server not running on', testUrl);
console.warn(' To run these tests, start the server with: npm run start:http');
console.warn(' Or use the full integration script: npm run test:integration');
}
});
afterAll(async () => {
// Clean up if needed
});
describe('Protocol Initialization', () => {
test('should initialize MCP connection successfully', async () => {
if (!serverRunning) {
if (process.env.TEST_SERVER_AUTO_START === 'true') {
throw new Error('Server should be running but is not available');
}
console.log('⏭️ Skipping - server not running');
return; // Skip test gracefully
}
await client.initialize();
expect(client.session.initialized).toBe(true);
});
test('should fail requests before initialization when server is running', async () => {
if (!serverRunning) {
console.log('⏭️ Skipping - server not running');
return; // Skip test gracefully
}
const uninitializedClient = new McpSseClient();
await expect(uninitializedClient.makeRequest('tools/list')).rejects.toThrow();
});
});
describe('Basic Connectivity', () => {
test('should detect server availability', async () => {
// This test always runs to document server state
const testUrl = process.env.TEST_URL || 'http://localhost:3000';
const running = await isServerRunning(testUrl);
if (running) {
console.log('✅ Server is running on', testUrl);
expect(running).toBe(true);
} else {
console.log('ℹ️ Server not detected on', testUrl);
console.log(' Start server with: npm run start:http');
expect(running).toBe(false); // Document the state
}
});
});
describe('Tools API (when server running)', () => {
test('should list available tools', async () => {
if (!serverRunning) {
console.log('⏭️ Skipping - server not running');
return;
}
// Make sure client is initialized first
if (!client.session.initialized) {
await client.initialize();
}
const response = await client.makeRequest('tools/list');
expect(response.data.result.tools).toBeDefined();
expect(Array.isArray(response.data.result.tools)).toBe(true);
const toolNames = response.data.result.tools.map((t: { name: string }) => t.name);
expect(toolNames).toContain('login');
expect(toolNames).toContain('create_post');
expect(toolNames).toContain('read_posts');
expect(toolNames).toContain('sampling_create');
});
});
describe('Resources API (when server running)', () => {
test('should list available resources', async () => {
if (!serverRunning) {
console.log('⏭️ Skipping - server not running');
return;
}
// Make sure client is initialized first
if (!client.session.initialized) {
await client.initialize();
}
const response = await client.makeRequest('resources/list');
expect(response.data.result.resources).toBeDefined();
expect(Array.isArray(response.data.result.resources)).toBe(true);
const resourceUris = response.data.result.resources.map((r: { uri: string }) => r.uri);
expect(resourceUris).toContain('social://feed');
expect(resourceUris).toContain('social://notifications');
expect(resourceUris).toContain('social://roots');
});
});
});