cliente.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. #!/usr/bin/env bun
  2. /**
  3. * Cliente MCP para validar conformidade Streamable HTTP
  4. * Testa:
  5. * - Inicialização de sessão
  6. * - Reutilização de sessionId
  7. * - Chamada de tools
  8. * - Listagem de recursos
  9. * - SSE streaming (GET)
  10. */
  11. import { randomUUID } from 'node:crypto';
  12. const SERVER_URL = 'http://localhost:3002/mcp';
  13. const HEALTH_URL = 'http://localhost:3002/health';
  14. let sessionId: string | null = null;
  15. // ==================== HELPERS ====================
  16. /**
  17. * Fazer requisição POST para servidor MCP
  18. */
  19. async function mcpRequest(method: string, params: Record<string, unknown> = {}, id = 1) {
  20. const body = {
  21. jsonrpc: '2.0',
  22. id,
  23. method,
  24. params,
  25. };
  26. const headers: Record<string, string> = {
  27. 'Content-Type': 'application/json',
  28. 'Accept': 'application/json, text/event-stream',
  29. };
  30. // Adicionar sessionId se já foi inicializado
  31. if (sessionId) {
  32. headers['mcp-session-id'] = sessionId;
  33. }
  34. console.log(`\n📤 Request: ${method}`);
  35. if (Object.keys(params).length > 0) {
  36. console.log(' Params:', JSON.stringify(params).substring(0, 80) + '...');
  37. }
  38. try {
  39. const response = await fetch(SERVER_URL, {
  40. method: 'POST',
  41. headers,
  42. body: JSON.stringify(body),
  43. });
  44. // Extrair sessionId do header se presente
  45. const newSessionId = response.headers.get('mcp-session-id');
  46. if (newSessionId && !sessionId) {
  47. sessionId = newSessionId;
  48. console.log(`✅ Nova sessão criada: ${sessionId}`);
  49. }
  50. // Verificar se é SSE ou JSON
  51. const contentType = response.headers.get('content-type') || '';
  52. let data: unknown;
  53. if (contentType.includes('text/event-stream')) {
  54. // Processar SSE stream
  55. const text = await response.text();
  56. // Parse SSE format: "event: message\ndata: {...}\n\n"
  57. const lines = text.trim().split('\n');
  58. let jsonData = '';
  59. for (const line of lines) {
  60. if (line.startsWith('data: ')) {
  61. jsonData = line.substring(6);
  62. break;
  63. }
  64. }
  65. if (jsonData) {
  66. data = JSON.parse(jsonData);
  67. } else {
  68. console.log(`❌ Erro: SSE stream sem data`);
  69. return null;
  70. }
  71. } else {
  72. // JSON direto
  73. data = await response.json();
  74. }
  75. // Verificar se é erro JSON-RPC
  76. if (data && typeof data === 'object' && 'error' in data && data.error) {
  77. const error = data.error as { code: number; message: string };
  78. console.log(`❌ Erro (code ${error.code}): ${error.message}`);
  79. return null;
  80. }
  81. // Sucesso
  82. console.log(`✅ Response recebida`);
  83. return data;
  84. } catch (error) {
  85. console.log(`❌ Erro na requisição:`, error instanceof Error ? error.message : error);
  86. return null;
  87. }
  88. }
  89. /**
  90. * Testar health check
  91. */
  92. async function testHealth() {
  93. console.log('\n' + '='.repeat(60));
  94. console.log('🏥 TEST 1: Health Check');
  95. console.log('='.repeat(60));
  96. try {
  97. const response = await fetch(HEALTH_URL);
  98. const text = await response.text();
  99. if (response.status === 200 && text === 'OK') {
  100. console.log('✅ Server health check: OK');
  101. return true;
  102. } else {
  103. console.log(`❌ Health check falhou: ${response.status} ${text}`);
  104. return false;
  105. }
  106. } catch (error) {
  107. console.log(`❌ Erro ao testar health:`, error instanceof Error ? error.message : error);
  108. return false;
  109. }
  110. }
  111. /**
  112. * Testar inicialização
  113. */
  114. async function testInitialize() {
  115. console.log('\n' + '='.repeat(60));
  116. console.log('🔧 TEST 2: Initialize (Criar Sessão)');
  117. console.log('='.repeat(60));
  118. const result = await mcpRequest('initialize', {
  119. protocolVersion: '2024-11-05',
  120. capabilities: {},
  121. clientInfo: {
  122. name: 'test-client',
  123. version: '1.0.0',
  124. },
  125. });
  126. if (result && result.result) {
  127. console.log('✅ Servidor respondeu com result');
  128. console.log(' Capabilities:', JSON.stringify(result.result.capabilities || {}).substring(0, 100));
  129. console.log(' serverInfo:', result.result.serverInfo?.name || 'N/A');
  130. return true;
  131. }
  132. return false;
  133. }
  134. /**
  135. * Testar listagem de tools
  136. */
  137. async function testListTools() {
  138. console.log('\n' + '='.repeat(60));
  139. console.log('🔨 TEST 3: List Tools');
  140. console.log('='.repeat(60));
  141. const result = await mcpRequest('tools/list');
  142. if (result && result.result && Array.isArray(result.result.tools)) {
  143. console.log(`✅ Encontrados ${result.result.tools.length} tools`);
  144. result.result.tools.slice(0, 3).forEach((tool: Record<string, unknown>) => {
  145. console.log(` - ${tool.name}`);
  146. });
  147. if (result.result.tools.length > 3) {
  148. console.log(` ... e ${result.result.tools.length - 3} mais`);
  149. }
  150. return true;
  151. }
  152. return false;
  153. }
  154. /**
  155. * Testar chamada de tool
  156. */
  157. async function testCallTool() {
  158. console.log('\n' + '='.repeat(60));
  159. console.log('⚙️ TEST 4: Call Tool - listar_grupos_musculares');
  160. console.log('='.repeat(60));
  161. const result = await mcpRequest('tools/call', {
  162. name: 'listar_grupos_musculares',
  163. arguments: {},
  164. });
  165. if (result && result.result) {
  166. const content = result.result.content?.[0];
  167. if (content && content.type === 'text') {
  168. const text = content.text as string;
  169. const lines = text.split('\n').slice(0, 5);
  170. console.log('✅ Tool executada com sucesso');
  171. console.log(' Resposta (primeiras linhas):');
  172. lines.forEach(line => {
  173. if (line.trim()) console.log(` ${line}`);
  174. });
  175. return true;
  176. }
  177. }
  178. return false;
  179. }
  180. /**
  181. * Testar chamada de outra tool com argumentos
  182. */
  183. async function testCallToolWithArgs() {
  184. console.log('\n' + '='.repeat(60));
  185. console.log('⚙️ TEST 5: Call Tool - buscar_exercicio_por_nome');
  186. console.log('='.repeat(60));
  187. const result = await mcpRequest('tools/call', {
  188. name: 'buscar_exercicio_por_nome',
  189. arguments: {
  190. nome: 'supino',
  191. },
  192. });
  193. if (result && result.result) {
  194. const content = result.result.content?.[0];
  195. if (content && content.type === 'text') {
  196. const text = content.text as string;
  197. console.log('✅ Tool executada com sucesso');
  198. console.log(' Resposta (primeiras 3 linhas):');
  199. text.split('\n').slice(0, 3).forEach(line => {
  200. if (line.trim()) console.log(` ${line}`);
  201. });
  202. return true;
  203. }
  204. }
  205. return false;
  206. }
  207. /**
  208. * Testar listagem de recursos
  209. */
  210. async function testListResources() {
  211. console.log('\n' + '='.repeat(60));
  212. console.log('📚 TEST 6: List Resources');
  213. console.log('='.repeat(60));
  214. const result = await mcpRequest('resources/list');
  215. if (result && result.result && Array.isArray(result.result.resources)) {
  216. console.log(`✅ Encontrados ${result.result.resources.length} recursos`);
  217. result.result.resources.slice(0, 3).forEach((resource: Record<string, unknown>) => {
  218. console.log(` - ${resource.name} (${resource.uri})`);
  219. });
  220. if (result.result.resources.length > 3) {
  221. console.log(` ... e ${result.result.resources.length - 3} mais`);
  222. }
  223. return true;
  224. }
  225. return false;
  226. }
  227. /**
  228. * Testar GET /mcp para SSE (requer sessionId)
  229. */
  230. async function testSSEStream() {
  231. console.log('\n' + '='.repeat(60));
  232. console.log('📡 TEST 7: SSE Stream (GET /mcp)');
  233. console.log('='.repeat(60));
  234. if (!sessionId) {
  235. console.log('⏭️ Pulando: sessionId não disponível');
  236. return false;
  237. }
  238. const headers: Record<string, string> = {
  239. 'Accept': 'text/event-stream',
  240. 'mcp-session-id': sessionId,
  241. };
  242. try {
  243. const controller = new AbortController();
  244. const timeoutId = setTimeout(() => controller.abort(), 3000); // timeout 3s
  245. const response = await fetch('http://localhost:3002/mcp', {
  246. method: 'GET',
  247. headers,
  248. signal: controller.signal,
  249. });
  250. clearTimeout(timeoutId);
  251. if (response.status === 200) {
  252. const contentType = response.headers.get('content-type');
  253. console.log(`✅ SSE stream conectado`);
  254. console.log(` Content-Type: ${contentType}`);
  255. console.log(` mcp-session-id header: ${response.headers.get('mcp-session-id') || 'N/A'}`);
  256. // Stream pode fechar após timeout ou enviar dados, ambos são válidos
  257. return true;
  258. } else {
  259. console.log(`❌ Falha ao conectar SSE: ${response.status}`);
  260. return false;
  261. }
  262. } catch (error) {
  263. // Timeout ou desconexão é aceitável
  264. if (error instanceof Error && error.name === 'AbortError') {
  265. console.log(`✅ SSE stream manteve conexão até timeout (comportamento esperado)`);
  266. return true;
  267. }
  268. console.log(`⚠️ SSE stream desconectou (aceitável):`, error instanceof Error ? error.message : error);
  269. // Desconexão não significa falha - a conexão foi estabelecida
  270. return true;
  271. }
  272. }
  273. /**
  274. * Testar reutilização de sessionId
  275. */
  276. async function testSessionReuse() {
  277. console.log('\n' + '='.repeat(60));
  278. console.log('🔄 TEST 8: Session Reuse (reutilizar sessionId)');
  279. console.log('='.repeat(60));
  280. if (!sessionId) {
  281. console.log('⏭️ Pulando: sessionId não disponível');
  282. return false;
  283. }
  284. console.log(`📤 Enviando request com sessionId existente: ${sessionId}`);
  285. const result = await mcpRequest('tools/list', {}, 99);
  286. if (result) {
  287. console.log('✅ sessionId reutilizado com sucesso');
  288. return true;
  289. }
  290. return false;
  291. }
  292. /**
  293. * Testar erro com sessionId inválido
  294. */
  295. async function testInvalidSession() {
  296. console.log('\n' + '='.repeat(60));
  297. console.log('❌ TEST 9: Invalid Session (testar rejeição)');
  298. console.log('='.repeat(60));
  299. const fakeSessionId = randomUUID();
  300. console.log(`📤 Enviando request com sessionId inválido: ${fakeSessionId}`);
  301. const body = {
  302. jsonrpc: '2.0',
  303. id: 1,
  304. method: 'tools/list',
  305. params: {},
  306. };
  307. try {
  308. const response = await fetch(SERVER_URL, {
  309. method: 'POST',
  310. headers: {
  311. 'Content-Type': 'application/json',
  312. 'mcp-session-id': fakeSessionId,
  313. },
  314. body: JSON.stringify(body),
  315. });
  316. const data = await response.json();
  317. if (data.error && data.error.code === -32000) {
  318. console.log('✅ Servidor rejeitou sessionId inválido corretamente');
  319. console.log(` Erro: ${data.error.message}`);
  320. return true;
  321. } else if (data.error) {
  322. console.log(`⚠️ Servidor retornou erro diferente: code ${data.error.code}`);
  323. return true; // Ainda é um erro, o que é esperado
  324. }
  325. } catch (error) {
  326. console.log(`❌ Erro:`, error instanceof Error ? error.message : error);
  327. return false;
  328. }
  329. return false;
  330. }
  331. // ==================== MAIN ====================
  332. async function main() {
  333. console.log('\n' + '╔' + '═'.repeat(58) + '╗');
  334. console.log('║' + ' '.repeat(10) + '🧪 TESTES DE CONFORMIDADE MCP' + ' '.repeat(20) + '║');
  335. console.log('║' + ' '.repeat(8) + 'Streamable HTTP - Servidor Academia' + ' '.repeat(16) + '║');
  336. console.log('╚' + '═'.repeat(58) + '╝');
  337. const results: { test: string; passed: boolean }[] = [];
  338. // Executar testes em sequência
  339. results.push({ test: 'Health Check', passed: await testHealth() });
  340. results.push({ test: 'Initialize', passed: await testInitialize() });
  341. results.push({ test: 'List Tools', passed: await testListTools() });
  342. results.push({ test: 'Call Tool (sem args)', passed: await testCallTool() });
  343. results.push({ test: 'Call Tool (com args)', passed: await testCallToolWithArgs() });
  344. results.push({ test: 'List Resources', passed: await testListResources() });
  345. results.push({ test: 'SSE Stream', passed: await testSSEStream() });
  346. results.push({ test: 'Session Reuse', passed: await testSessionReuse() });
  347. results.push({ test: 'Invalid Session', passed: await testInvalidSession() });
  348. // Resumo
  349. console.log('\n' + '='.repeat(60));
  350. console.log('📊 RESUMO DOS TESTES');
  351. console.log('='.repeat(60));
  352. const passed = results.filter(r => r.passed).length;
  353. const total = results.length;
  354. results.forEach((r, i) => {
  355. const icon = r.passed ? '✅' : '❌';
  356. console.log(`${icon} ${i + 1}. ${r.test}`);
  357. });
  358. console.log('='.repeat(60));
  359. console.log(`\n🎯 Resultado Final: ${passed}/${total} testes passaram`);
  360. if (passed === total) {
  361. console.log('🎉 Servidor está 100% conforme com Streamable HTTP!');
  362. } else if (passed >= total - 1) {
  363. console.log('✅ Servidor está funcionando bem!');
  364. } else {
  365. console.log('⚠️ Alguns testes falharam. Verifique os logs acima.');
  366. }
  367. console.log('');
  368. }
  369. main().catch(error => {
  370. console.error('❌ Erro fatal:', error);
  371. process.exit(1);
  372. });