index.ts 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. #!/usr/bin/env bun
  2. // Servidor MCP para gerenciamento de exercícios de academia
  3. import { Database } from "bun:sqlite";
  4. // Conexão com o Banco de Dados usando o SQLite nativo do Bun
  5. const db = new Database("./academia.sqlite3");
  6. // Handler para listar todas as ferramentas disponíveis
  7. async function handleListTools() {
  8. return {
  9. tools: [
  10. {
  11. name: "buscar_exercicios_por_grupo",
  12. description:
  13. "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)'",
  14. inputSchema: {
  15. type: "object",
  16. properties: {
  17. grupo_muscular: {
  18. type: "string",
  19. description: "Nome do grupo muscular (ex: 'Pernas', 'Peito (peitoral)')",
  20. },
  21. },
  22. required: ["grupo_muscular"],
  23. },
  24. },
  25. {
  26. name: "listar_grupos_musculares",
  27. description: "Lista todos os grupos musculares disponíveis no banco de dados",
  28. inputSchema: {
  29. type: "object",
  30. properties: {},
  31. },
  32. },
  33. {
  34. name: "buscar_exercicio_por_nome",
  35. description: "Busca exercícios específicos por nome (busca parcial, case-insensitive)",
  36. inputSchema: {
  37. type: "object",
  38. properties: {
  39. nome: {
  40. type: "string",
  41. description: "Nome ou parte do nome do exercício (ex: 'agachamento', 'supino')",
  42. },
  43. },
  44. required: ["nome"],
  45. },
  46. },
  47. {
  48. name: "listar_todos_exercicios",
  49. description: "Lista todos os exercícios cadastrados no banco de dados",
  50. inputSchema: {
  51. type: "object",
  52. properties: {},
  53. },
  54. },
  55. {
  56. name: "obter_detalhes_exercicio",
  57. description: "Obtém detalhes completos de um exercício específico pelo ID",
  58. inputSchema: {
  59. type: "object",
  60. properties: {
  61. id: {
  62. type: "number",
  63. description: "ID do exercício",
  64. },
  65. },
  66. required: ["id"],
  67. },
  68. },
  69. ],
  70. };
  71. }
  72. // Handler para executar as ferramentas
  73. async function handleCallTool(request: any) {
  74. const { name, arguments: args } = request.params;
  75. try {
  76. switch (name) {
  77. case "buscar_exercicios_por_grupo": {
  78. const { grupo_muscular } = args as { grupo_muscular: string };
  79. const query = db.query<Exercicio, [string]>(
  80. `SELECT id, nome, grupo_muscular, series, repeticoes, intervalo_segundos, observacoes
  81. FROM exercios_vw
  82. WHERE grupo_muscular LIKE ?`
  83. );
  84. const exercicios = query.all(`%${grupo_muscular}%`);
  85. if (exercicios.length === 0) {
  86. return {
  87. content: [
  88. {
  89. type: "text",
  90. text: `Nenhum exercício encontrado para o grupo muscular: ${grupo_muscular}`,
  91. },
  92. ],
  93. };
  94. }
  95. const resultado = exercicios.map(ex =>
  96. `**${ex.nome}**\n` +
  97. `- Séries: ${ex.series}\n` +
  98. `- Repetições: ${ex.repeticoes}\n` +
  99. `- Intervalo: ${ex.intervalo_segundos}s\n` +
  100. `- Observações: ${ex.observacoes}\n`
  101. ).join('\n');
  102. return {
  103. content: [
  104. {
  105. type: "text",
  106. text: `Encontrados ${exercicios.length} exercícios para ${grupo_muscular}:\n\n${resultado}`,
  107. },
  108. ],
  109. };
  110. }
  111. case "listar_grupos_musculares": {
  112. const query = db.query<{ grupo_muscular: string }, []>(
  113. `SELECT DISTINCT grupo_muscular FROM exercios_vw ORDER BY grupo_muscular`
  114. );
  115. const grupos = query.all();
  116. const lista = grupos.map(g => `- ${g.grupo_muscular}`).join('\n');
  117. return {
  118. content: [
  119. {
  120. type: "text",
  121. text: `Grupos musculares disponíveis:\n\n${lista}`,
  122. },
  123. ],
  124. };
  125. }
  126. case "buscar_exercicio_por_nome": {
  127. console.log('Executando ferramenta buscar_exercicio_por_nome');
  128. const { nome } = args as { nome: string };
  129. const query = db.query<Exercicio, [string]>(
  130. `SELECT id, nome, grupo_muscular, series, repeticoes, intervalo_segundos, observacoes
  131. FROM exercios_vw
  132. WHERE nome LIKE ?`
  133. );
  134. const exercicios = query.all(`%${nome}%`);
  135. if (exercicios.length === 0) {
  136. return {
  137. content: [
  138. {
  139. type: "text",
  140. text: `Nenhum exercício encontrado com o nome: ${nome}`,
  141. },
  142. ],
  143. };
  144. }
  145. const resultado = exercicios.map(ex =>
  146. `**ID ${ex.id}: ${ex.nome}**\n` +
  147. `- Grupo: ${ex.grupo_muscular}\n` +
  148. `- Séries: ${ex.series} x ${ex.repeticoes} repetições\n` +
  149. `- Intervalo: ${ex.intervalo_segundos}s\n` +
  150. `- Observações: ${ex.observacoes}\n`
  151. ).join('\n');
  152. return {
  153. content: [
  154. {
  155. type: "text",
  156. text: `Encontrados ${exercicios.length} exercício(s):\n\n${resultado}`,
  157. },
  158. ],
  159. };
  160. }
  161. case "listar_todos_exercicios": {
  162. const query = db.query<Exercicio, []>(
  163. `SELECT id, nome, grupo_muscular, series, repeticoes, intervalo_segundos, observacoes
  164. FROM exercios_vw
  165. ORDER BY grupo_muscular, nome`
  166. );
  167. const exercicios = query.all();
  168. // Agrupa por grupo muscular
  169. const porGrupo: Record<string, Exercicio[]> = {};
  170. exercicios.forEach(ex => {
  171. if (!porGrupo[ex.grupo_muscular]) {
  172. porGrupo[ex.grupo_muscular] = [];
  173. }
  174. porGrupo[ex.grupo_muscular].push(ex);
  175. });
  176. const resultado = Object.entries(porGrupo).map(([grupo, exs]) =>
  177. `### ${grupo}\n` +
  178. exs.map(ex => `- ${ex.nome} (${ex.series}x${ex.repeticoes})`).join('\n')
  179. ).join('\n\n');
  180. return {
  181. content: [
  182. {
  183. type: "text",
  184. text: `Total de ${exercicios.length} exercícios cadastrados:\n\n${resultado}`,
  185. },
  186. ],
  187. };
  188. }
  189. case "obter_detalhes_exercicio": {
  190. const { id } = args as { id: number };
  191. const query = db.query<Exercicio, [number]>(
  192. `SELECT id, nome, grupo_muscular, series, repeticoes, intervalo_segundos, observacoes
  193. FROM exercios_vw
  194. WHERE id = ?`
  195. );
  196. const exercicio = query.get(id);
  197. if (!exercicio) {
  198. return {
  199. content: [
  200. {
  201. type: "text",
  202. text: `Exercício com ID ${id} não encontrado.`,
  203. },
  204. ],
  205. };
  206. }
  207. const detalhes =
  208. `# ${exercicio.nome}\n\n` +
  209. `**Grupo Muscular:** ${exercicio.grupo_muscular}\n` +
  210. `**Séries:** ${exercicio.series}\n` +
  211. `**Repetições:** ${exercicio.repeticoes}\n` +
  212. `**Intervalo:** ${exercicio.intervalo_segundos} segundos\n` +
  213. `**Observações:** ${exercicio.observacoes}`;
  214. return {
  215. content: [
  216. {
  217. type: "text",
  218. text: detalhes,
  219. },
  220. ],
  221. };
  222. }
  223. default:
  224. throw new Error(`Ferramenta desconhecida: ${name}`);
  225. }
  226. } catch (error) {
  227. return {
  228. content: [
  229. {
  230. type: "text",
  231. text: `Erro ao executar ${name}: ${error instanceof Error ? error.message : String(error)}`,
  232. },
  233. ],
  234. isError: true,
  235. };
  236. }
  237. }
  238. // Iniciar servidor MCP de academia via HTTP/SSE
  239. async function main() {
  240. const port = 3000;
  241. const httpServer = Bun.serve({
  242. port,
  243. async fetch(req) {
  244. if (req.method === 'POST' && req.headers.get('content-type')?.includes('application/json')) {
  245. try {
  246. const body = await req.json();
  247. const { jsonrpc, id, method, params } = body;
  248. if (jsonrpc !== '2.0') {
  249. return new Response(JSON.stringify({
  250. jsonrpc: '2.0',
  251. error: { code: -32600, message: 'Invalid Request' },
  252. id
  253. }), { status: 400, headers: { 'Content-Type': 'application/json' } });
  254. }
  255. let result;
  256. if (method === 'tools/list') {
  257. result = await handleListTools();
  258. } else if (method === 'tools/call') {
  259. result = await handleCallTool(body);
  260. } else {
  261. return new Response(JSON.stringify({
  262. jsonrpc: '2.0',
  263. error: { code: -32601, message: 'Method not found' },
  264. id
  265. }), { status: 404, headers: { 'Content-Type': 'application/json' } });
  266. }
  267. return new Response(JSON.stringify(result), {
  268. headers: { 'Content-Type': 'application/json' }
  269. });
  270. } catch (error) {
  271. return new Response(JSON.stringify({
  272. jsonrpc: '2.0',
  273. error: { code: -32700, message: 'Parse error' },
  274. id: null
  275. }), { status: 400, headers: { 'Content-Type': 'application/json' } });
  276. }
  277. }
  278. // Para SSE (simplificado, apenas health check)
  279. if (req.method === 'GET' && req.url.endsWith('/health')) {
  280. return new Response("OK", { status: 200 });
  281. }
  282. return new Response("MCP Server Running", { status: 200 });
  283. }
  284. });
  285. console.error(`Servidor MCP de Academia iniciado via HTTP na porta ${port}`);
  286. }
  287. // Remover a chamada main(server), pois agora é main()
  288. main().catch((error) => {
  289. console.error("Erro fatal:", error);
  290. process.exit(1);
  291. });