浏览代码

feat: atualiza padrao

tiago.cipriano 1 天之前
父节点
当前提交
4103035264
共有 9 个文件被更改,包括 786 次插入501 次删除
  1. 7 108
      README.md
  2. 5 1
      bun.lock
  3. 440 0
      cliente.ts
  4. 5 0
      dev.env
  5. 296 71
      index.ts
  6. 4 2
      package.json
  7. 29 0
      tsconfig.json
  8. 0 10
      types.ts
  9. 0 309
      via-stdio.ts

+ 7 - 108
README.md

@@ -1,5 +1,8 @@
 # Servidor MCP: mcp-academia-server
 
+- [repo rag: git/ti2](https://git.c.net.br/gogs_supper_admin/rag-academia-server) | [github](https://github.com/TiagoTi/rag-academia-server)
+- [repo: git/ti2](https://git.ti2.net.br/gogs_supper_admin/mcp-academia-server) | [github](https://github.com/TiagoTi/mcp-academia-server)
+
 Quando começei pesquisar sobre, a maioria dos tutoriais explicavam com executar
 o servidor mcp dentro de um script com outros componente, como o código que conecta
 a uma llm, o arquivo `via-stdio.ts` é uma extração desse exemplo.
@@ -10,20 +13,13 @@ Servidor MCP Centralizado via HTTP/SSE
 
 ## Desenvolvimento
 
-### Executando via http
-
-```http
-curl -X POST http://localhost:3000 \
-    -H "Content-Type: application/json" \
-    -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}'
-```
-
-### Executando via stdio
+### Executando teste 
 
 ```sh
-echo '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}' | bun run via-stdio.ts
+bun run cliente.ts
 ```
 
+
 ## Gerando publicação via docker
 
 ### criando a imagem
@@ -36,9 +32,7 @@ docker image build \
 ### executando
 
 ```sh
-docker container run \
-    --rm --name mcp-server-academia \
-    -d -p 3000:3000 mcp-server-academia
+docker run --restart=always --name mcp-server-academia  -d -p 3401:3000 mcp-server-academia
 ```
 
 ## O que ess MCP Especificamente Faz?
@@ -72,98 +66,3 @@ Descrição: Obtém detalhes completos de um exercício específico pelo ID.
 Entrada: {"id": number} (ex: 1).
 Saída: Informações detalhadas (nome, grupo, séries, repetições, intervalo, observações).
 
-```json
-{
-    "tools": [
-        {
-            "name": "buscar_exercicios_por_grupo",
-            "description": "Busca exercícios filtrando por grupo muscular. Grupos disponíveis: 'Costas (dorsais, lombar)', 'Ombros (deltoides)', 'Pernas', 'Peito (peitoral)', 'Braços (Bíceps, Tríceps, Antebraço)'",
-            "inputSchema": {
-                "type": "object",
-                "properties": {
-                    "grupo_muscular": {
-                        "type": "string",
-                        "description": "Nome do grupo muscular (ex: 'Pernas', 'Peito (peitoral)')"
-                    }
-                },
-                "required": [
-                    "grupo_muscular"
-                ]
-            }
-        },
-        {
-            "name": "listar_grupos_musculares",
-            "description": "Lista todos os grupos musculares disponíveis no banco de dados",
-            "inputSchema": {
-                "type": "object",
-                "properties": {}
-            }
-        },
-        {
-            "name": "buscar_exercicio_por_nome",
-            "description": "Busca exercícios específicos por nome (busca parcial, case-insensitive)",
-            "inputSchema": {
-                "type": "object",
-                "properties": {
-                    "nome": {
-                        "type": "string",
-                        "description": "Nome ou parte do nome do exercício (ex: 'agachamento', 'supino')"
-                    }
-                },
-                "required": [
-                    "nome"
-                ]
-            }
-        },
-        {
-            "name": "listar_todos_exercicios",
-            "description": "Lista todos os exercícios cadastrados no banco de dados",
-            "inputSchema": {
-                "type": "object",
-                "properties": {}
-            }
-        },
-        {
-            "name": "obter_detalhes_exercicio",
-            "description": "Obtém detalhes completos de um exercício específico pelo ID",
-            "inputSchema": {
-                "type": "object",
-                "properties": {
-                    "id": {
-                        "type": "number",
-                        "description": "ID do exercício"
-                    }
-                },
-                "required": [
-                    "id"
-                ]
-            }
-        }
-    ]
-}
-```
-
-### Exemplo de uma possivel chamada do clientes
-
-```http
-curl -X POST http://localhost:3000 -H "Content-Type: application/json" -d '{
-  "jsonrpc": "2.0",
-  "id": 2,
-  "method": "tools/call",
-  "params": {
-    "name": "buscar_exercicios_por_grupo",
-    "arguments": {"grupo_muscular": "Pernas"}
-  }
-}'
-```
-
-```json
-{
-    "content": [
-        {
-            "type": "text",
-            "text": "Encontrados 8 exercícios para Pernas:\n\n**Agachamento livre - Exercício composto para quadríceps, glúteos e posterior**\n- Séries: 4\n- Repetições: 8\n- Intervalo: 90s\n- Observações: carga a definir\n\n**Extensora drop 10-10-10 - Exercício isolado para quadríceps com técnica drop set**\n- Séries: 4\n- Repetições: 8\n- Intervalo: 90s\n- Observações: carga a definir\n\n**Agachamento triângulo halteres - Agachamento com halteres em posição triângulo**\n- Séries: 4\n- Repetições: 8\n- Intervalo: 90s\n- Observações: carga a definir\n\n**Afundo - Exercício para quadríceps, glúteos e posterior**\n- Séries: 4\n- Repetições: 8\n- Intervalo: 90s\n- Observações: carga a definir\n\n**Mesa flexora - Exercício para posterior de coxa (isquiotibiais)**\n- Séries: 4\n- Repetições: 8\n- Intervalo: 90s\n- Observações: carga a definir\n\n**Abdutora - Exercício para glúteos médios e mínimo**\n- Séries: 4\n- Repetições: 8\n- Intervalo: 90s\n- Observações: carga a definir\n\n**Agachamento Sumô - Agachamento com postura sumô para glúteos e adutores**\n- Séries: 4\n- Repetições: 8\n- Intervalo: 90s\n- Observações: carga a definir\n\n**Stiffe barra - Exercício para posterior de coxa e glúteos**\n- Séries: 4\n- Repetições: 8\n- Intervalo: 90s\n- Observações: carga a definir\n"
-        }
-    ]
-}
-```

+ 5 - 1
bun.lock

@@ -5,11 +5,13 @@
     "": {
       "name": "mcp",
       "dependencies": {
-        "@modelcontextprotocol/sdk": "^1.23.0",
+        "@modelcontextprotocol/sdk": "^1.25.3",
         "@types/node": "^24.10.1",
         "@types/sqlite3": "^5.1.0",
+        "install": "^0.13.0",
         "sqlite3": "^5.1.7",
         "ts-node": "^10.9.2",
+        "zod": "^4.3.6",
       },
       "devDependencies": {
         "@types/bun": "latest",
@@ -246,6 +248,8 @@
 
     "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
 
+    "install": ["install@0.13.0", "", {}, "sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA=="],
+
     "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
 
     "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],

+ 440 - 0
cliente.ts

@@ -0,0 +1,440 @@
+#!/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);
+});

+ 5 - 0
dev.env

@@ -0,0 +1,5 @@
+PORT=3401
+DB_PATH="./academia.sqlite3"
+
+#bun run -i -p --console-depth=10 --watch --env-file=dev.env index.ts
+#bun run --watch --env-file=dev.env index.ts

+ 296 - 71
index.ts

@@ -1,15 +1,83 @@
-#!/usr/bin/env bun
-// Servidor MCP para gerenciamento de exercícios de academia
+// In the Streamable HTTP transport, the server operates as an independent process that can handle multiple client connections. This transport uses HTTP POST and GET requests. Server can optionally make use of Server-Sent Events (SSE) to stream multiple server messages. This permits basic MCP servers, as well as more feature-rich servers supporting streaming and server-to-client notifications and requests.
 
+//  The server MUST provide a single HTTP endpoint path (hereafter referred to as the MCP endpoint) that supports both POST and GET methods. For example, this could be a URL like https://example.com/mcp.
+
+import { randomUUID } from 'node:crypto';
+import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
+import {
+  CallToolRequestSchema,
+  ListToolsRequestSchema,
+  ListResourcesRequestSchema,
+  isInitializeRequest,
+} from "@modelcontextprotocol/sdk/types.js";
 import { Database } from "bun:sqlite";
-import { Exercicio } from "./types";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+
 
+interface Exercicio {
+  id: number;
+  nome: string;
+  grupo_muscular: string;
+  series: number;
+  repeticoes: number;
+  intervalo_segundos: number;
+  observacoes: string;
+}
 
+const filename_database = process.env.DB_PATH || "./academia.sqlite3";
 // Conexão com o Banco de Dados usando o SQLite nativo do Bun
-const db = new Database("./academia.sqlite3");
+const db = new Database(filename_database);
+
+// Criar servidor MCP
+const mcpServer = new McpServer({
+  name: "academia-mcp-server",
+  version: "1.0.0",
+}, {
+  capabilities: {
+    tools: {},
+    resources: {},
+  }
+});
+
+// ==================== SESSION MANAGEMENT ====================
+// Storage de transports por sessionId para suportar múltiplas conexões
+const transports: { [sessionId: string]: WebStandardStreamableHTTPServerTransport } = {};
+
+// Função auxiliar para criar resposta de erro JSON-RPC
+function createJsonRpcError(code: number, message: string, id: string | number | null = null) {
+  return {
+    jsonrpc: "2.0",
+    error: {
+      code,
+      message,
+    },
+    id,
+  };
+}
+
+
+
+// Handler para listar os recursos (exercícios)
+mcpServer.server.setRequestHandler(ListResourcesRequestSchema, async () => {
+  const query = db.query<Exercicio, []>(
+    `SELECT id, nome, grupo_muscular, series, repeticoes, intervalo_segundos, observacoes 
+     FROM exercios_vw`
+  );
+
+  const exercicios = query.all();
+  return {
+    resources: exercicios.map((ex) => ({
+      uri: `academia://exercicio/${ex.id}`,
+      name: ex.nome,
+      mimeType: "application/json",
+      text: JSON.stringify(ex, null, 2),
+    })),
+  };
+});
+
 
 // Handler para listar todas as ferramentas disponíveis
-async function handleListTools() {
+mcpServer.server.setRequestHandler(ListToolsRequestSchema, async () => {
   return {
     tools: [
       {
@@ -73,25 +141,26 @@ async function handleListTools() {
       },
     ],
   };
-}
+});
 
 // Handler para executar as ferramentas
-async function handleCallTool(request: any) {
-  const { name, arguments: args } = request.params;
+mcpServer.server.setRequestHandler(CallToolRequestSchema, async (request: unknown) => {
+  const typedRequest = request as { params: { name: string; arguments: Record<string, unknown> } };
+  const { name, arguments: args } = typedRequest.params;
 
   try {
     switch (name) {
       case "buscar_exercicios_por_grupo": {
         const { grupo_muscular } = args as { grupo_muscular: string };
-        
+
         const query = db.query<Exercicio, [string]>(
           `SELECT id, nome, grupo_muscular, series, repeticoes, intervalo_segundos, observacoes 
            FROM exercios_vw 
            WHERE grupo_muscular LIKE ?`
         );
-        
+
         const exercicios = query.all(`%${grupo_muscular}%`);
-        
+
         if (exercicios.length === 0) {
           return {
             content: [
@@ -103,7 +172,7 @@ async function handleCallTool(request: any) {
           };
         }
 
-        const resultado = exercicios.map(ex => 
+        const resultado = exercicios.map(ex =>
           `**${ex.nome}**\n` +
           `- Séries: ${ex.series}\n` +
           `- Repetições: ${ex.repeticoes}\n` +
@@ -125,7 +194,7 @@ async function handleCallTool(request: any) {
         const query = db.query<{ grupo_muscular: string }, []>(
           `SELECT DISTINCT grupo_muscular FROM exercios_vw ORDER BY grupo_muscular`
         );
-        
+
         const grupos = query.all();
         const lista = grupos.map(g => `- ${g.grupo_muscular}`).join('\n');
 
@@ -142,15 +211,15 @@ async function handleCallTool(request: any) {
       case "buscar_exercicio_por_nome": {
         console.log('Executando ferramenta buscar_exercicio_por_nome');
         const { nome } = args as { nome: string };
-        
+
         const query = db.query<Exercicio, [string]>(
           `SELECT id, nome, grupo_muscular, series, repeticoes, intervalo_segundos, observacoes 
            FROM exercios_vw 
            WHERE nome LIKE ?`
         );
-        
+
         const exercicios = query.all(`%${nome}%`);
-        
+
         if (exercicios.length === 0) {
           return {
             content: [
@@ -162,7 +231,7 @@ async function handleCallTool(request: any) {
           };
         }
 
-        const resultado = exercicios.map(ex => 
+        const resultado = exercicios.map(ex =>
           `**ID ${ex.id}: ${ex.nome}**\n` +
           `- Grupo: ${ex.grupo_muscular}\n` +
           `- Séries: ${ex.series} x ${ex.repeticoes} repetições\n` +
@@ -186,19 +255,22 @@ async function handleCallTool(request: any) {
            FROM exercios_vw 
            ORDER BY grupo_muscular, nome`
         );
-        
+
         const exercicios = query.all();
-        
+
         // Agrupa por grupo muscular
         const porGrupo: Record<string, Exercicio[]> = {};
         exercicios.forEach(ex => {
           if (!porGrupo[ex.grupo_muscular]) {
             porGrupo[ex.grupo_muscular] = [];
           }
-          porGrupo[ex.grupo_muscular].push(ex);
+          const grupo = porGrupo[ex.grupo_muscular];
+          if (grupo) {
+            grupo.push(ex);
+          }
         });
 
-        const resultado = Object.entries(porGrupo).map(([grupo, exs]) => 
+        const resultado = Object.entries(porGrupo).map(([grupo, exs]) =>
           `### ${grupo}\n` +
           exs.map(ex => `- ${ex.nome} (${ex.series}x${ex.repeticoes})`).join('\n')
         ).join('\n\n');
@@ -215,15 +287,15 @@ async function handleCallTool(request: any) {
 
       case "obter_detalhes_exercicio": {
         const { id } = args as { id: number };
-        
+
         const query = db.query<Exercicio, [number]>(
           `SELECT id, nome, grupo_muscular, series, repeticoes, intervalo_segundos, observacoes 
            FROM exercios_vw 
            WHERE id = ?`
         );
-        
+
         const exercicio = query.get(id);
-        
+
         if (!exercicio) {
           return {
             content: [
@@ -235,7 +307,7 @@ async function handleCallTool(request: any) {
           };
         }
 
-        const detalhes = 
+        const detalhes =
           `# ${exercicio.nome}\n\n` +
           `**Grupo Muscular:** ${exercicio.grupo_muscular}\n` +
           `**Séries:** ${exercicio.series}\n` +
@@ -267,69 +339,222 @@ async function handleCallTool(request: any) {
       isError: true,
     };
   }
+});
+
+// ==================== HTTP HANDLERS ====================
+
+/**
+ * Handler para POST /mcp - Processa requisições MCP (Initialize, Calls, etc)
+ * Gerencia sessões via mcp-session-id header
+ */
+async function handleMcpPost(req: Request): Promise<Response> {
+  try {
+    const sessionId = req.headers.get("mcp-session-id");
+    let transport: WebStandardStreamableHTTPServerTransport;
+
+    // Fazer clone do request para validação sem consumir o body
+    const clonedReq = req.clone();
+    let body: unknown;
+
+    // Tentar fazer parse do body apenas para validação
+    try {
+      body = await clonedReq.json();
+    } catch (e) {
+      console.error("Failed to parse JSON request body:", e);
+      return new Response(
+        JSON.stringify(createJsonRpcError(-32700, "Parse error")),
+        { status: 400, headers: { "Content-Type": "application/json" } }
+      );
+    }
+
+    // Validar estrutura JSON-RPC
+    if (typeof body !== "object" || body === null) {
+      return new Response(
+        JSON.stringify(createJsonRpcError(-32700, "Invalid Request: body must be JSON object")),
+        { status: 400, headers: { "Content-Type": "application/json" } }
+      );
+    }
+
+    const requestBody = body as Record<string, unknown>;
+    const requestId = (requestBody.id as string | number | null) || null;
+
+    // Se há sessionId existente, reutilizar transport
+    if (sessionId && transports[sessionId]) {
+      console.log(`[${sessionId}] Reusing existing transport`);
+      transport = transports[sessionId];
+    } 
+    // Se é initialize request, criar nova sessão
+    else if (!sessionId && isInitializeRequest(requestBody)) {
+      console.log("[NEW] Initialize request received, creating new session");
+      transport = new WebStandardStreamableHTTPServerTransport({
+        sessionIdGenerator: () => randomUUID(),
+        onsessioninitialized: (newSessionId: string) => {
+          console.log(`[${newSessionId}] Session initialized`);
+          transports[newSessionId] = transport;
+        },
+      });
+
+      // Conectar transport ao servidor MCP
+      await mcpServer.connect(transport);
+    } 
+    // Erro: nem sessionId válido nem initialize request
+    else {
+      console.error("Invalid request: no valid session ID or not an initialize request");
+      return new Response(
+        JSON.stringify(
+          createJsonRpcError(
+            -32000,
+            "Invalid request: provide mcp-session-id header for existing sessions or send an initialize request",
+            requestId
+          )
+        ),
+        { status: 400, headers: { "Content-Type": "application/json" } }
+      );
+    }
+
+    // Delegar ao transport para processar a requisição
+    // Usar o request original (não clonado) para que o transport possa ler o body
+    return transport.handleRequest(req);
+  } catch (error) {
+    console.error("Error handling MCP POST request:", error);
+    return new Response(
+      JSON.stringify(createJsonRpcError(-32603, "Internal server error")),
+      { status: 500, headers: { "Content-Type": "application/json" } }
+    );
+  }
 }
 
+/**
+ * Handler para GET /mcp - Estabelece stream SSE para notificações
+ * Requer mcp-session-id header válido
+ */
+async function handleMcpGet(req: Request): Promise<Response> {
+  try {
+    const sessionId = req.headers.get("mcp-session-id");
+
+    // Validar session ID
+    if (!sessionId) {
+      console.error("GET request without mcp-session-id header");
+      return new Response("Invalid or missing mcp-session-id header", {
+        status: 400,
+        headers: { "Content-Type": "text/plain" },
+      });
+    }
+
+    if (!transports[sessionId]) {
+      console.error(`[${sessionId}] Session not found`);
+      return new Response(`Session ${sessionId} not found`, {
+        status: 404,
+        headers: { "Content-Type": "text/plain" },
+      });
+    }
+
+    console.log(`[${sessionId}] Establishing SSE stream`);
+    const transport = transports[sessionId];
 
-// Iniciar servidor MCP de academia via HTTP/SSE
+    // Delegar ao transport para estabelecer SSE stream
+    return transport.handleRequest(req);
+  } catch (error) {
+    console.error("Error handling MCP GET request:", error);
+    return new Response("Internal server error", {
+      status: 500,
+      headers: { "Content-Type": "text/plain" },
+    });
+  }
+}
+
+// ==================== SERVER STARTUP ====================
+
+/**
+ * Inicia o servidor MCP com suporte a Streamable HTTP
+ * Implementa:
+ * - POST /mcp para requisições JSON-RPC e gerenciamento de sessão
+ * - GET /mcp para streams SSE
+ * - GET /health para health check
+ * - Graceful shutdown ao receber SIGINT/SIGTERM
+ */
 async function main() {
-  const port = 3000;
+  const port = process.env.PORT ? Number(process.env.PORT) : 3000;
 
-  const httpServer = Bun.serve({
+  const server = Bun.serve({
     port,
-    async fetch(req) {
-      if (req.method === 'POST' && req.headers.get('content-type')?.includes('application/json')) {
-        try {
-          const body = await req.json();
-          const { jsonrpc, id, method, params } = body;
-
-          if (jsonrpc !== '2.0') {
-            return new Response(JSON.stringify({
-              jsonrpc: '2.0',
-              error: { code: -32600, message: 'Invalid Request' },
-              id
-            }), { status: 400, headers: { 'Content-Type': 'application/json' } });
-          }
+    async fetch(req: Request) {
+      const url = new URL(req.url);
+      const pathname = url.pathname;
+      const method = req.method;
 
-          let result;
-          if (method === 'tools/list') {
-            result = await handleListTools();
-          } else if (method === 'tools/call') {
-            result = await handleCallTool(body);
-          } else {
-            return new Response(JSON.stringify({
-              jsonrpc: '2.0',
-              error: { code: -32601, message: 'Method not found' },
-              id
-            }), { status: 404, headers: { 'Content-Type': 'application/json' } });
-          }
+      // Health check endpoint
+      if (method === "GET" && pathname === "/health") {
+        return new Response("OK", { status: 200 });
+      }
 
-          return new Response(JSON.stringify(result), {
-            headers: { 'Content-Type': 'application/json' }
-          });
-        } catch (error) {
-          return new Response(JSON.stringify({
-            jsonrpc: '2.0',
-            error: { code: -32700, message: 'Parse error' },
-            id: null
-          }), { status: 400, headers: { 'Content-Type': 'application/json' } });
+      // MCP endpoint - conforme especificação Streamable HTTP
+      if (pathname === "/mcp") {
+        if (method === "POST") {
+          return await handleMcpPost(req);
+        } else if (method === "GET") {
+          return await handleMcpGet(req);
+        } else {
+          return new Response("Method not allowed", { status: 405 });
         }
       }
 
-      // Para SSE (simplificado, apenas health check)
-      if (req.method === 'GET' && req.url.endsWith('/health')) {
-        return new Response("OK", { status: 200 });
+      // 404 para rotas não encontradas
+      return new Response(
+        JSON.stringify({ error: "Not Found", path: pathname }),
+        { status: 404, headers: { "Content-Type": "application/json" } }
+      );
+    },
+  });
+
+  console.log(`[SERVER] Servidor MCP de Academia iniciado`);
+  console.log(`[SERVER] Endpoint HTTP/SSE disponível em http://localhost:${port}/mcp`);
+  console.log(`[SERVER] Health check em http://localhost:${port}/health`);
+
+  // ==================== GRACEFUL SHUTDOWN ====================
+  const shutdown = async (signal: string) => {
+    console.log(`\n[SHUTDOWN] Recebido sinal ${signal}, iniciando shutdown gracioso...`);
+
+    // Fechar todas as sessões ativas
+    const sessionIds = Object.keys(transports);
+    console.log(`[SHUTDOWN] Fechando ${sessionIds.length} sessão(ões) ativa(s)...`);
+    for (const sessionId of sessionIds) {
+      try {
+        const transport = transports[sessionId];
+        if (transport) {
+          console.log(`[SHUTDOWN] Fechando sessão ${sessionId}`);
+          await transport.close?.();
+          delete transports[sessionId];
+        }
+      } catch (error) {
+        console.error(`[SHUTDOWN] Erro ao fechar sessão ${sessionId}:`, error);
       }
+    }
 
-      return new Response("MCP Server Running", { status: 200 });
+    // Fechar servidor MCP
+    try {
+      console.log("[SHUTDOWN] Fechando servidor MCP...");
+      await mcpServer.close?.();
+    } catch (error) {
+      console.error("[SHUTDOWN] Erro ao fechar servidor MCP:", error);
     }
-  });
 
-  console.error(`Servidor MCP de Academia iniciado via HTTP na porta ${port}`);
+    // Fechar servidor HTTP
+    server.stop();
+
+    console.log("[SHUTDOWN] Shutdown concluído. Servidor parado.");
+    process.exit(0);
+  };
+
+  // Registrar handlers para SIGINT e SIGTERM
+  process.on("SIGINT", () => shutdown("SIGINT"));
+  process.on("SIGTERM", () => shutdown("SIGTERM"));
+
+  return server;
 }
 
-// Remover a chamada main(server), pois agora é main()
+// Iniciar servidor
 main().catch((error) => {
-  console.error("Erro fatal:", error);
+  console.error("[FATAL] Erro ao iniciar servidor:", error);
   process.exit(1);
 });
-

+ 4 - 2
package.json

@@ -10,10 +10,12 @@
     "typescript": "^5.9.3"
   },
   "dependencies": {
-    "@modelcontextprotocol/sdk": "^1.23.0",
+    "@modelcontextprotocol/sdk": "^1.25.3",
     "@types/node": "^24.10.1",
     "@types/sqlite3": "^5.1.0",
+    "install": "^0.13.0",
     "sqlite3": "^5.1.7",
-    "ts-node": "^10.9.2"
+    "ts-node": "^10.9.2",
+    "zod": "^4.3.6"
   }
 }

+ 29 - 0
tsconfig.json

@@ -0,0 +1,29 @@
+{
+  "compilerOptions": {
+    // Environment setup & latest features
+    "lib": ["ESNext"],
+    "target": "ESNext",
+    "module": "Preserve",
+    "moduleDetection": "force",
+    "jsx": "react-jsx",
+    "allowJs": true,
+
+    // Bundler mode
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "verbatimModuleSyntax": true,
+    "noEmit": true,
+
+    // Best practices
+    "strict": true,
+    "skipLibCheck": true,
+    "noFallthroughCasesInSwitch": true,
+    "noUncheckedIndexedAccess": true,
+    "noImplicitOverride": true,
+
+    // Some stricter flags (disabled by default)
+    "noUnusedLocals": false,
+    "noUnusedParameters": false,
+    "noPropertyAccessFromIndexSignature": false
+  }
+}

+ 0 - 10
types.ts

@@ -1,10 +0,0 @@
-// Tipos
-export interface Exercicio {
-  id: number;
-  nome: string;
-  grupo_muscular: string;
-  series: number;
-  repeticoes: number;
-  intervalo_segundos: number;
-  observacoes: string;
-}

+ 0 - 309
via-stdio.ts

@@ -1,309 +0,0 @@
-#!/usr/bin/env bun
-// Servidor MCP para gerenciamento de exercícios de academia
-
-import { Server } from "@modelcontextprotocol/sdk/server/index.js";
-import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
-import {
-  CallToolRequestSchema,
-  ListToolsRequestSchema,
-} from "@modelcontextprotocol/sdk/types.js";
-import { Database } from "bun:sqlite";
-import { Exercicio } from "./types";
-
-
-// Conexão com o Banco de Dados usando o SQLite nativo do Bun
-const db = new Database("./academia.sqlite3");
-
-// Criar servidor MCP
-const server = new Server(
-  {
-    name: "academia-mcp-server",
-    version: "1.0.0",
-  },
-  {
-    capabilities: {
-      tools: {},
-    },
-  }
-);
-
-// Handler para listar todas as ferramentas disponíveis
-server.setRequestHandler(ListToolsRequestSchema, async () => {
-  return {
-    tools: [
-      {
-        name: "buscar_exercicios_por_grupo",
-        description:
-          "Busca exercícios filtrando por grupo muscular. Grupos disponíveis: 'Costas (dorsais, lombar)', 'Ombros (deltoides)', 'Pernas', 'Peito (peitoral)', 'Braços (Bíceps, Tríceps, Antebraço)'",
-        inputSchema: {
-          type: "object",
-          properties: {
-            grupo_muscular: {
-              type: "string",
-              description: "Nome do grupo muscular (ex: 'Pernas', 'Peito (peitoral)')",
-            },
-          },
-          required: ["grupo_muscular"],
-        },
-      },
-      {
-        name: "listar_grupos_musculares",
-        description: "Lista todos os grupos musculares disponíveis no banco de dados",
-        inputSchema: {
-          type: "object",
-          properties: {},
-        },
-      },
-      {
-        name: "buscar_exercicio_por_nome",
-        description: "Busca exercícios específicos por nome (busca parcial, case-insensitive)",
-        inputSchema: {
-          type: "object",
-          properties: {
-            nome: {
-              type: "string",
-              description: "Nome ou parte do nome do exercício (ex: 'agachamento', 'supino')",
-            },
-          },
-          required: ["nome"],
-        },
-      },
-      {
-        name: "listar_todos_exercicios",
-        description: "Lista todos os exercícios cadastrados no banco de dados",
-        inputSchema: {
-          type: "object",
-          properties: {},
-        },
-      },
-      {
-        name: "obter_detalhes_exercicio",
-        description: "Obtém detalhes completos de um exercício específico pelo ID",
-        inputSchema: {
-          type: "object",
-          properties: {
-            id: {
-              type: "number",
-              description: "ID do exercício",
-            },
-          },
-          required: ["id"],
-        },
-      },
-    ],
-  };
-});
-
-
-// Handler para executar as ferramentas
-// Configuração do  servidor MCP de academia
-server.setRequestHandler(CallToolRequestSchema, async (request) => {
-  const { name, arguments: args } = request.params;
-
-  try {
-    switch (name) {
-      case "buscar_exercicios_por_grupo": {
-        const { grupo_muscular } = args as { grupo_muscular: string };
-        
-        const query = db.query<Exercicio, [string]>(
-          `SELECT id, nome, grupo_muscular, series, repeticoes, intervalo_segundos, observacoes 
-           FROM exercios_vw 
-           WHERE grupo_muscular LIKE ?`
-        );
-        
-        const exercicios = query.all(`%${grupo_muscular}%`);
-        
-        if (exercicios.length === 0) {
-          return {
-            content: [
-              {
-                type: "text",
-                text: `Nenhum exercício encontrado para o grupo muscular: ${grupo_muscular}`,
-              },
-            ],
-          };
-        }
-
-        const resultado = exercicios.map(ex => 
-          `**${ex.nome}**\n` +
-          `- Séries: ${ex.series}\n` +
-          `- Repetições: ${ex.repeticoes}\n` +
-          `- Intervalo: ${ex.intervalo_segundos}s\n` +
-          `- Observações: ${ex.observacoes}\n`
-        ).join('\n');
-
-        return {
-          content: [
-            {
-              type: "text",
-              text: `Encontrados ${exercicios.length} exercícios para ${grupo_muscular}:\n\n${resultado}`,
-            },
-          ],
-        };
-      }
-
-      case "listar_grupos_musculares": {
-        const query = db.query<{ grupo_muscular: string }, []>(
-          `SELECT DISTINCT grupo_muscular FROM exercios_vw ORDER BY grupo_muscular`
-        );
-        
-        const grupos = query.all();
-        const lista = grupos.map(g => `- ${g.grupo_muscular}`).join('\n');
-
-        return {
-          content: [
-            {
-              type: "text",
-              text: `Grupos musculares disponíveis:\n\n${lista}`,
-            },
-          ],
-        };
-      }
-
-      case "buscar_exercicio_por_nome": {
-        console.log('Executando ferramenta buscar_exercicio_por_nome');
-        const { nome } = args as { nome: string };
-        
-        const query = db.query<Exercicio, [string]>(
-          `SELECT id, nome, grupo_muscular, series, repeticoes, intervalo_segundos, observacoes 
-           FROM exercios_vw 
-           WHERE nome LIKE ?`
-        );
-        
-        const exercicios = query.all(`%${nome}%`);
-        
-        if (exercicios.length === 0) {
-          return {
-            content: [
-              {
-                type: "text",
-                text: `Nenhum exercício encontrado com o nome: ${nome}`,
-              },
-            ],
-          };
-        }
-
-        const resultado = exercicios.map(ex => 
-          `**ID ${ex.id}: ${ex.nome}**\n` +
-          `- Grupo: ${ex.grupo_muscular}\n` +
-          `- Séries: ${ex.series} x ${ex.repeticoes} repetições\n` +
-          `- Intervalo: ${ex.intervalo_segundos}s\n` +
-          `- Observações: ${ex.observacoes}\n`
-        ).join('\n');
-
-        return {
-          content: [
-            {
-              type: "text",
-              text: `Encontrados ${exercicios.length} exercício(s):\n\n${resultado}`,
-            },
-          ],
-        };
-      }
-
-      case "listar_todos_exercicios": {
-        const query = db.query<Exercicio, []>(
-          `SELECT id, nome, grupo_muscular, series, repeticoes, intervalo_segundos, observacoes 
-           FROM exercios_vw 
-           ORDER BY grupo_muscular, nome`
-        );
-        
-        const exercicios = query.all();
-        
-        // Agrupa por grupo muscular
-        const porGrupo: Record<string, Exercicio[]> = {};
-        exercicios.forEach(ex => {
-          if (!porGrupo[ex.grupo_muscular]) {
-            porGrupo[ex.grupo_muscular] = [];
-          }
-          porGrupo[ex.grupo_muscular].push(ex);
-        });
-
-        const resultado = Object.entries(porGrupo).map(([grupo, exs]) => 
-          `### ${grupo}\n` +
-          exs.map(ex => `- ${ex.nome} (${ex.series}x${ex.repeticoes})`).join('\n')
-        ).join('\n\n');
-
-        return {
-          content: [
-            {
-              type: "text",
-              text: `Total de ${exercicios.length} exercícios cadastrados:\n\n${resultado}`,
-            },
-          ],
-        };
-      }
-
-      case "obter_detalhes_exercicio": {
-        const { id } = args as { id: number };
-        
-        const query = db.query<Exercicio, [number]>(
-          `SELECT id, nome, grupo_muscular, series, repeticoes, intervalo_segundos, observacoes 
-           FROM exercios_vw 
-           WHERE id = ?`
-        );
-        
-        const exercicio = query.get(id);
-        
-        if (!exercicio) {
-          return {
-            content: [
-              {
-                type: "text",
-                text: `Exercício com ID ${id} não encontrado.`,
-              },
-            ],
-          };
-        }
-
-        const detalhes = 
-          `# ${exercicio.nome}\n\n` +
-          `**Grupo Muscular:** ${exercicio.grupo_muscular}\n` +
-          `**Séries:** ${exercicio.series}\n` +
-          `**Repetições:** ${exercicio.repeticoes}\n` +
-          `**Intervalo:** ${exercicio.intervalo_segundos} segundos\n` +
-          `**Observações:** ${exercicio.observacoes}`;
-
-        return {
-          content: [
-            {
-              type: "text",
-              text: detalhes,
-            },
-          ],
-        };
-      }
-
-      default:
-        throw new Error(`Ferramenta desconhecida: ${name}`);
-    }
-  } catch (error) {
-    return {
-      content: [
-        {
-          type: "text",
-          text: `Erro ao executar ${name}: ${error instanceof Error ? error.message : String(error)}`,
-        },
-      ],
-      isError: true,
-    };
-  }
-});
-
-
-// Iniciar servidor MCP de academia
-async function main(server: Server) {
-
-  // Server transport for stdio: this communicates with a MCP client by reading from the current process' stdin and writing to stdout.
-  const transport = new StdioServerTransport();
-
-  await server.connect(transport);
-
-  console.error("Servidor MCP de Academia iniciado via stdio");
-}
-
-main(server).catch((error) => {
-  console.error("Erro fatal:", error);
-  process.exit(1);
-});
-