| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440 |
- #!/usr/bin/env bun
- /**
- * Cliente MCP para validar conformidade Streamable HTTP
- * Testa:
- * - Inicialização de sessão
- * - Reutilização de sessionId
- * - Chamada de tools
- * - Listagem de recursos
- * - SSE streaming (GET)
- */
- import { randomUUID } from 'node:crypto';
- const SERVER_URL = 'http://localhost:3002/mcp';
- const HEALTH_URL = 'http://localhost:3002/health';
- let sessionId: string | null = null;
- // ==================== HELPERS ====================
- /**
- * Fazer requisição POST para servidor MCP
- */
- async function mcpRequest(method: string, params: Record<string, unknown> = {}, id = 1) {
- const body = {
- jsonrpc: '2.0',
- id,
- method,
- params,
- };
- const headers: Record<string, string> = {
- 'Content-Type': 'application/json',
- 'Accept': 'application/json, text/event-stream',
- };
- // Adicionar sessionId se já foi inicializado
- if (sessionId) {
- headers['mcp-session-id'] = sessionId;
- }
- console.log(`\n📤 Request: ${method}`);
- if (Object.keys(params).length > 0) {
- console.log(' Params:', JSON.stringify(params).substring(0, 80) + '...');
- }
- try {
- const response = await fetch(SERVER_URL, {
- method: 'POST',
- headers,
- body: JSON.stringify(body),
- });
- // Extrair sessionId do header se presente
- const newSessionId = response.headers.get('mcp-session-id');
- if (newSessionId && !sessionId) {
- sessionId = newSessionId;
- console.log(`✅ Nova sessão criada: ${sessionId}`);
- }
- // Verificar se é SSE ou JSON
- const contentType = response.headers.get('content-type') || '';
- let data: unknown;
- if (contentType.includes('text/event-stream')) {
- // Processar SSE stream
- const text = await response.text();
- // Parse SSE format: "event: message\ndata: {...}\n\n"
- const lines = text.trim().split('\n');
- let jsonData = '';
- for (const line of lines) {
- if (line.startsWith('data: ')) {
- jsonData = line.substring(6);
- break;
- }
- }
- if (jsonData) {
- data = JSON.parse(jsonData);
- } else {
- console.log(`❌ Erro: SSE stream sem data`);
- return null;
- }
- } else {
- // JSON direto
- data = await response.json();
- }
- // Verificar se é erro JSON-RPC
- if (data && typeof data === 'object' && 'error' in data && data.error) {
- const error = data.error as { code: number; message: string };
- console.log(`❌ Erro (code ${error.code}): ${error.message}`);
- return null;
- }
- // Sucesso
- console.log(`✅ Response recebida`);
- return data;
- } catch (error) {
- console.log(`❌ Erro na requisição:`, error instanceof Error ? error.message : error);
- return null;
- }
- }
- /**
- * Testar health check
- */
- async function testHealth() {
- console.log('\n' + '='.repeat(60));
- console.log('🏥 TEST 1: Health Check');
- console.log('='.repeat(60));
- try {
- const response = await fetch(HEALTH_URL);
- const text = await response.text();
- if (response.status === 200 && text === 'OK') {
- console.log('✅ Server health check: OK');
- return true;
- } else {
- console.log(`❌ Health check falhou: ${response.status} ${text}`);
- return false;
- }
- } catch (error) {
- console.log(`❌ Erro ao testar health:`, error instanceof Error ? error.message : error);
- return false;
- }
- }
- /**
- * Testar inicialização
- */
- async function testInitialize() {
- console.log('\n' + '='.repeat(60));
- console.log('🔧 TEST 2: Initialize (Criar Sessão)');
- console.log('='.repeat(60));
- const result = await mcpRequest('initialize', {
- protocolVersion: '2024-11-05',
- capabilities: {},
- clientInfo: {
- name: 'test-client',
- version: '1.0.0',
- },
- });
- if (result && result.result) {
- console.log('✅ Servidor respondeu com result');
- console.log(' Capabilities:', JSON.stringify(result.result.capabilities || {}).substring(0, 100));
- console.log(' serverInfo:', result.result.serverInfo?.name || 'N/A');
- return true;
- }
- return false;
- }
- /**
- * Testar listagem de tools
- */
- async function testListTools() {
- console.log('\n' + '='.repeat(60));
- console.log('🔨 TEST 3: List Tools');
- console.log('='.repeat(60));
- const result = await mcpRequest('tools/list');
- if (result && result.result && Array.isArray(result.result.tools)) {
- console.log(`✅ Encontrados ${result.result.tools.length} tools`);
- result.result.tools.slice(0, 3).forEach((tool: Record<string, unknown>) => {
- console.log(` - ${tool.name}`);
- });
- if (result.result.tools.length > 3) {
- console.log(` ... e ${result.result.tools.length - 3} mais`);
- }
- return true;
- }
- return false;
- }
- /**
- * Testar chamada de tool
- */
- async function testCallTool() {
- console.log('\n' + '='.repeat(60));
- console.log('⚙️ TEST 4: Call Tool - listar_grupos_musculares');
- console.log('='.repeat(60));
- const result = await mcpRequest('tools/call', {
- name: 'listar_grupos_musculares',
- arguments: {},
- });
- if (result && result.result) {
- const content = result.result.content?.[0];
- if (content && content.type === 'text') {
- const text = content.text as string;
- const lines = text.split('\n').slice(0, 5);
- console.log('✅ Tool executada com sucesso');
- console.log(' Resposta (primeiras linhas):');
- lines.forEach(line => {
- if (line.trim()) console.log(` ${line}`);
- });
- return true;
- }
- }
- return false;
- }
- /**
- * Testar chamada de outra tool com argumentos
- */
- async function testCallToolWithArgs() {
- console.log('\n' + '='.repeat(60));
- console.log('⚙️ TEST 5: Call Tool - buscar_exercicio_por_nome');
- console.log('='.repeat(60));
- const result = await mcpRequest('tools/call', {
- name: 'buscar_exercicio_por_nome',
- arguments: {
- nome: 'supino',
- },
- });
- if (result && result.result) {
- const content = result.result.content?.[0];
- if (content && content.type === 'text') {
- const text = content.text as string;
- console.log('✅ Tool executada com sucesso');
- console.log(' Resposta (primeiras 3 linhas):');
- text.split('\n').slice(0, 3).forEach(line => {
- if (line.trim()) console.log(` ${line}`);
- });
- return true;
- }
- }
- return false;
- }
- /**
- * Testar listagem de recursos
- */
- async function testListResources() {
- console.log('\n' + '='.repeat(60));
- console.log('📚 TEST 6: List Resources');
- console.log('='.repeat(60));
- const result = await mcpRequest('resources/list');
- if (result && result.result && Array.isArray(result.result.resources)) {
- console.log(`✅ Encontrados ${result.result.resources.length} recursos`);
- result.result.resources.slice(0, 3).forEach((resource: Record<string, unknown>) => {
- console.log(` - ${resource.name} (${resource.uri})`);
- });
- if (result.result.resources.length > 3) {
- console.log(` ... e ${result.result.resources.length - 3} mais`);
- }
- return true;
- }
- return false;
- }
- /**
- * Testar GET /mcp para SSE (requer sessionId)
- */
- async function testSSEStream() {
- console.log('\n' + '='.repeat(60));
- console.log('📡 TEST 7: SSE Stream (GET /mcp)');
- console.log('='.repeat(60));
- if (!sessionId) {
- console.log('⏭️ Pulando: sessionId não disponível');
- return false;
- }
- const headers: Record<string, string> = {
- 'Accept': 'text/event-stream',
- 'mcp-session-id': sessionId,
- };
- try {
- const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), 3000); // timeout 3s
- const response = await fetch('http://localhost:3002/mcp', {
- method: 'GET',
- headers,
- signal: controller.signal,
- });
- clearTimeout(timeoutId);
- if (response.status === 200) {
- const contentType = response.headers.get('content-type');
- console.log(`✅ SSE stream conectado`);
- console.log(` Content-Type: ${contentType}`);
- console.log(` mcp-session-id header: ${response.headers.get('mcp-session-id') || 'N/A'}`);
- // Stream pode fechar após timeout ou enviar dados, ambos são válidos
- return true;
- } else {
- console.log(`❌ Falha ao conectar SSE: ${response.status}`);
- return false;
- }
- } catch (error) {
- // Timeout ou desconexão é aceitável
- if (error instanceof Error && error.name === 'AbortError') {
- console.log(`✅ SSE stream manteve conexão até timeout (comportamento esperado)`);
- return true;
- }
- console.log(`⚠️ SSE stream desconectou (aceitável):`, error instanceof Error ? error.message : error);
- // Desconexão não significa falha - a conexão foi estabelecida
- return true;
- }
- }
- /**
- * Testar reutilização de sessionId
- */
- async function testSessionReuse() {
- console.log('\n' + '='.repeat(60));
- console.log('🔄 TEST 8: Session Reuse (reutilizar sessionId)');
- console.log('='.repeat(60));
- if (!sessionId) {
- console.log('⏭️ Pulando: sessionId não disponível');
- return false;
- }
- console.log(`📤 Enviando request com sessionId existente: ${sessionId}`);
- const result = await mcpRequest('tools/list', {}, 99);
- if (result) {
- console.log('✅ sessionId reutilizado com sucesso');
- return true;
- }
- return false;
- }
- /**
- * Testar erro com sessionId inválido
- */
- async function testInvalidSession() {
- console.log('\n' + '='.repeat(60));
- console.log('❌ TEST 9: Invalid Session (testar rejeição)');
- console.log('='.repeat(60));
- const fakeSessionId = randomUUID();
- console.log(`📤 Enviando request com sessionId inválido: ${fakeSessionId}`);
- const body = {
- jsonrpc: '2.0',
- id: 1,
- method: 'tools/list',
- params: {},
- };
- try {
- const response = await fetch(SERVER_URL, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'mcp-session-id': fakeSessionId,
- },
- body: JSON.stringify(body),
- });
- const data = await response.json();
- if (data.error && data.error.code === -32000) {
- console.log('✅ Servidor rejeitou sessionId inválido corretamente');
- console.log(` Erro: ${data.error.message}`);
- return true;
- } else if (data.error) {
- console.log(`⚠️ Servidor retornou erro diferente: code ${data.error.code}`);
- return true; // Ainda é um erro, o que é esperado
- }
- } catch (error) {
- console.log(`❌ Erro:`, error instanceof Error ? error.message : error);
- return false;
- }
- return false;
- }
- // ==================== MAIN ====================
- async function main() {
- console.log('\n' + '╔' + '═'.repeat(58) + '╗');
- console.log('║' + ' '.repeat(10) + '🧪 TESTES DE CONFORMIDADE MCP' + ' '.repeat(20) + '║');
- console.log('║' + ' '.repeat(8) + 'Streamable HTTP - Servidor Academia' + ' '.repeat(16) + '║');
- console.log('╚' + '═'.repeat(58) + '╝');
- const results: { test: string; passed: boolean }[] = [];
- // Executar testes em sequência
- results.push({ test: 'Health Check', passed: await testHealth() });
- results.push({ test: 'Initialize', passed: await testInitialize() });
- results.push({ test: 'List Tools', passed: await testListTools() });
- results.push({ test: 'Call Tool (sem args)', passed: await testCallTool() });
- results.push({ test: 'Call Tool (com args)', passed: await testCallToolWithArgs() });
- results.push({ test: 'List Resources', passed: await testListResources() });
- results.push({ test: 'SSE Stream', passed: await testSSEStream() });
- results.push({ test: 'Session Reuse', passed: await testSessionReuse() });
- results.push({ test: 'Invalid Session', passed: await testInvalidSession() });
- // Resumo
- console.log('\n' + '='.repeat(60));
- console.log('📊 RESUMO DOS TESTES');
- console.log('='.repeat(60));
- const passed = results.filter(r => r.passed).length;
- const total = results.length;
- results.forEach((r, i) => {
- const icon = r.passed ? '✅' : '❌';
- console.log(`${icon} ${i + 1}. ${r.test}`);
- });
- console.log('='.repeat(60));
- console.log(`\n🎯 Resultado Final: ${passed}/${total} testes passaram`);
- if (passed === total) {
- console.log('🎉 Servidor está 100% conforme com Streamable HTTP!');
- } else if (passed >= total - 1) {
- console.log('✅ Servidor está funcionando bem!');
- } else {
- console.log('⚠️ Alguns testes falharam. Verifique os logs acima.');
- }
- console.log('');
- }
- main().catch(error => {
- console.error('❌ Erro fatal:', error);
- process.exit(1);
- });
|