index.ts 9.8 KB

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