Sfoglia il codice sorgente

feat: create api to use mcps and rags with ollama

tiago.cipriano 1 giorno fa
commit
4eca5b389a
13 ha cambiato i file con 1784 aggiunte e 0 eliminazioni
  1. 19 0
      .dockerignore
  2. 37 0
      .gitignore
  3. 1 0
      .tool-versions
  4. 40 0
      Dockerfile
  5. 70 0
      README.md
  6. 233 0
      bun.lock
  7. 124 0
      index.ts
  8. 524 0
      mcp-manager.ts
  9. 97 0
      ollama.ts
  10. 412 0
      orquestrador.ts
  11. 19 0
      package.json
  12. 179 0
      rag.ts
  13. 29 0
      tsconfig.json

+ 19 - 0
.dockerignore

@@ -0,0 +1,19 @@
+node_modules
+Dockerfile*
+docker-compose*
+.dockerignore
+.git
+.gitignore
+README.md
+LICENSE
+.vscode
+Makefile
+helm-charts
+.env
+.editorconfig
+.idea
+coverage*
+prompts
+.vscode
+.tool-versions
+via-stdio.ts

+ 37 - 0
.gitignore

@@ -0,0 +1,37 @@
+# dependencies (bun install)
+node_modules
+
+# output
+out
+dist
+*.tgz
+
+# code coverage
+coverage
+*.lcov
+
+# logs
+logs
+_.log
+report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# caches
+.eslintcache
+.cache
+*.tsbuildinfo
+
+# IntelliJ based IDEs
+.idea
+
+# Finder (MacOS) folder config
+.DS_Store
+.vscode
+prompts/
+.vscode/*

+ 1 - 0
.tool-versions

@@ -0,0 +1 @@
+bun 1.3.6

+ 40 - 0
Dockerfile

@@ -0,0 +1,40 @@
+# docker pull oven/bun:1.3.8-debian
+FROM oven/bun:1.3.8-debian AS base
+WORKDIR /usr/src/app
+
+# install dependencies into temp directory
+# this will cache them and speed up future builds
+FROM base AS install
+RUN mkdir -p /temp/dev
+COPY package.json bun.lock /temp/dev/
+RUN cd /temp/dev && bun install --frozen-lockfile
+
+
+# install with --production (exclude devDependencies)
+RUN mkdir -p /temp/prod
+COPY package.json bun.lock /temp/prod/
+RUN cd /temp/prod && bun install --frozen-lockfile --production
+
+# copy node_modules from temp directory
+# then copy all (non-ignored) project files into the image
+FROM base AS prerelease
+COPY --from=install /temp/dev/node_modules node_modules
+COPY . .
+
+
+# [optional] tests & build
+ENV NODE_ENV=production
+ENV PORT=3000
+# RUN bun test
+# RUN bun run build  # Removido: não há script build, e Bun executa TS diretamente
+
+# copy production dependencies and source code into final image
+FROM base AS release
+COPY --from=install /temp/prod/node_modules node_modules
+COPY --from=prerelease /usr/src/app/* .
+
+# run the app
+USER bun
+EXPOSE 3000/tcp
+ENTRYPOINT [ "bun", "run", "index.ts" ]
+

+ 70 - 0
README.md

@@ -0,0 +1,70 @@
+# api-chatbot-academia
+
+To install dependencies:
+
+```bash
+bun install
+```
+
+To run:
+
+```bash
+bun run index.ts
+```
+
+This project was created using `bun init` in bun v1.3.6. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
+
+
+```http
+curl -X POST http://localhost:3402/api/embeddings \
+  -H "Content-Type: application/json" \
+  -d '{"prompt": "on docker?", "topK": 3, "limiarSimilaridade": 0.5}'
+```
+
+```http
+curl -X POST http://localhost:3401 -H "Content-Type: application/json" -d '{
+  "jsonrpc": "2.0",
+  "id": 3,
+  "method": "tools/call",
+  "params": {
+    "name": "buscar_exercicios_por_grupo",
+    "arguments": {"grupo_muscular": "Pernas"}
+  }
+}'
+```
+
+```http
+curl -X POST http://localhost:3403/chat \
+  -H "Content-Type: application/json" \
+  -d '{
+    "mensagem": "Quais exercícios de peito eu devo fazer?"
+  }'
+```
+
+```http
+curl -X POST http://localhost:3000/chat \
+  -H "Content-Type: application/json" \
+  -d '{
+    "mensagem": "qual é o exercio 3?"
+  }'
+```
+
+```http
+curl -X POST http://localhost:3403/chat \
+  -H "Content-Type: application/json" \
+  -d '{
+    "mensagem": "listar meus exercicios + dicas de hipertrofia"
+  }'
+```
+```http
+curl -X POST http://localhost:3403/chat \
+  -H "Content-Type: application/json" \
+  -d '{
+    "mensagem": "rodar docker"
+  }'
+```
+
+
+
+docker image build --pull -t chatbot-server-academia .
+docker run --restart=always --name chatbot-server-academia --network host  -d -e PORT=3403  chatbot-server-academia

+ 233 - 0
bun.lock

@@ -0,0 +1,233 @@
+{
+  "lockfileVersion": 1,
+  "configVersion": 1,
+  "workspaces": {
+    "": {
+      "name": "api-chatbot-academia",
+      "dependencies": {
+        "@modelcontextprotocol/sdk": "^1.25.3",
+        "@types/cors": "^2.8.19",
+        "@types/express": "^5.0.6",
+        "cors": "^2.8.6",
+        "express": "^5.2.1",
+      },
+      "devDependencies": {
+        "@types/bun": "latest",
+      },
+      "peerDependencies": {
+        "typescript": "^5",
+      },
+    },
+  },
+  "packages": {
+    "@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="],
+
+    "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="],
+
+    "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
+
+    "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
+
+    "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
+
+    "@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="],
+
+    "@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="],
+
+    "@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.1", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A=="],
+
+    "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
+
+    "@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="],
+
+    "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="],
+
+    "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
+
+    "@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="],
+
+    "@types/serve-static": ["@types/serve-static@2.2.0", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*" } }, "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ=="],
+
+    "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
+
+    "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
+
+    "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
+
+    "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
+
+    "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
+
+    "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
+
+    "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
+
+    "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
+
+    "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
+
+    "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
+
+    "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
+
+    "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
+
+    "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
+
+    "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
+
+    "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+
+    "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
+
+    "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
+
+    "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
+
+    "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
+
+    "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
+
+    "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
+
+    "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
+
+    "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
+
+    "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
+
+    "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
+
+    "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
+
+    "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
+
+    "express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="],
+
+    "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
+
+    "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
+
+    "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
+
+    "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
+
+    "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
+
+    "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
+
+    "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
+
+    "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
+
+    "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
+
+    "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
+
+    "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
+
+    "hono": ["hono@4.11.7", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="],
+
+    "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
+
+    "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
+
+    "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
+
+    "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
+
+    "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
+
+    "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
+
+    "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
+
+    "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
+
+    "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
+
+    "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
+
+    "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
+
+    "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
+
+    "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
+
+    "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
+
+    "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+    "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
+
+    "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
+
+    "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
+
+    "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
+
+    "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
+
+    "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
+
+    "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
+
+    "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
+
+    "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
+
+    "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
+
+    "qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="],
+
+    "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
+
+    "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
+
+    "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
+
+    "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
+
+    "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
+
+    "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
+
+    "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
+
+    "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
+
+    "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
+
+    "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
+
+    "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
+
+    "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
+
+    "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
+
+    "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
+
+    "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
+
+    "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
+
+    "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
+
+    "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
+    "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
+
+    "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
+
+    "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
+
+    "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
+
+    "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
+
+    "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
+
+    "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
+  }
+}

+ 124 - 0
index.ts

@@ -0,0 +1,124 @@
+import express from 'express';
+import cors from 'cors';
+import { chamarOllama } from './ollama';
+import { orquestrar } from './orquestrador';
+import { gerenciadorMCP, inicializarMCPs } from './mcp-manager';
+
+// Criando a instância do Express
+const app = express();
+
+// Definindo a porta do servidor
+const PORT = process.env.PORT || 3000;
+
+// Middlewares
+// CORS: permite que o frontend faça requisições para esta API
+app.use(cors());
+
+// Express.json: permite que a API receba dados em formato JSON
+app.use(express.json());
+
+
+// Inicializa MCPs na inicialização da API
+inicializarMCPs()
+  .then(() => {
+    console.log('[INICIALIZAÇÃO] MCPs inicializados');
+    console.log('[INICIALIZAÇÃO] MCPs disponíveis:',
+      gerenciadorMCP.listarMCPs().map((m: { nome: string; }) => m.nome)
+    );
+  })
+  .catch((erro: any) => {
+    console.error('[INICIALIZAÇÃO] Erro ao inicializar MCPs:', erro);
+    console.log('[INICIALIZAÇÃO] API continuará sem MCPs');
+  });
+
+// Rota de ping
+// Esta rota é útil para verificar se a API está respondendo corretamente
+app.get('/ping', (req: express.Request, res: express.Response) => {
+  res.json({
+    message: 'pong',
+    timestamp: new Date().toISOString()
+  });
+});
+
+
+// Rota para chat INTELIGENTE (RAG + MCP)
+// Esta rota usa o orquestrador para decidir a melhor estratégia
+// Exemplo de uso: POST /chat com body { "mensagem": "Como fazer supino?" }
+app.post('/chat', async (req: express.Request, res: express.Response) => {
+  try {
+    const { mensagem } = req.body;
+
+    // Validação básica da entrada
+    if (!mensagem || typeof mensagem !== 'string') {
+      return res.status(400).json({
+        erro: 'Mensagem inválida ou não fornecida'
+      });
+    }
+
+    if (mensagem.trim().length === 0) {
+      return res.status(400).json({
+        erro: 'Mensagem não pode estar vazia'
+      });
+    }
+    console.log(`[API] Mensagem recebida: ${mensagem}`);
+
+    // Orquestra a busca de contexto (RAG e/ou MCP)
+    const resultado = await orquestrar(mensagem);
+
+    // Chama a LLM com o prompt enriquecido
+    console.log('[API] Enviando prompt para Ollama:\n', resultado.promptFinal);
+    const resposta = await chamarOllama(resultado.promptFinal);
+
+    // Retorna resposta com metadados detalhados
+    res.json({
+      resposta,
+      metadados: {
+        estrategia: resultado.estrategia,
+        documentosRAG: resultado.documentosRAG || [],
+        ferramentasMCP: resultado.ferramentasMCP || [],
+        usouRAG: !!resultado.contextoRAG,
+        usouMCP: !!resultado.contextoMCP
+      },
+      timestamp: new Date().toISOString()
+    });
+
+  } catch (erro) {
+    console.error('[API] Erro na rota /chat:', erro);
+
+    res.status(500).json({
+      erro: 'Erro ao processar a mensagem',
+      detalhes: erro instanceof Error ? erro.message : 'Erro desconhecido'
+    });
+  }
+});
+//
+// Rota para interagir com o modelo Ollama
+
+// Iniciando o servidor web
+const servidor = app.listen(PORT, () => {
+  console.log(`\n${'='.repeat(60)}`);
+  console.log(`Servidor rodando na porta ${PORT}`);
+  console.log(`Acesse: http://localhost:${PORT}/ping`);
+  console.log(`Chat: http://localhost:${PORT}/chat`);
+  console.log(`\nSistemas integrados:`);
+  console.log(`- RAG System (conhecimento textual)`);
+  console.log(`- MCP Client (dados estruturados)`);
+  console.log(`- Ollama LLM (geração de respostas)`);
+  console.log(`- Orquestrador Inteligente (decisão automática) para atividades físicas`);
+  console.log(`${'='.repeat(60)}\n`);
+});
+
+// Graceful shutdown atualizado
+process.on('SIGINT', async () => {
+  console.log('\nEncerrando aplicação...');
+
+  // Desconecta todos os MCPs antes de sair
+  await gerenciadorMCP.desconectarTodos();
+  console.log('Todos os MCPs desconectados');
+
+
+  servidor.close(() => {
+    console.log('Servidor encerrado');
+    process.exit(0);
+  });
+});

+ 524 - 0
mcp-manager.ts

@@ -0,0 +1,524 @@
+/**
+ * GERENCIADOR DE MÚLTIPLOS SERVIDORES MCP
+ * 
+ * Este módulo gerencia conexões com múltiplos servidores MCP
+ * 
+ * Por que criar um gerenciador?
+ * - Centraliza a lógica de conexão
+ * - Facilita adicionar novos MCPs
+ * - Mantém o código organizado (SOLID)
+ * - Permite reutilizar em diferentes partes da API
+ * 
+ * Como funciona?
+ * 1. Registra configurações de cada MCP
+ * 2. Inicializa conexões sob demanda
+ * 3. Mantém pool de clientes conectados
+ * 4. Fornece interface única para chamar ferramentas
+ */
+
+import { Client } from "@modelcontextprotocol/sdk/client/index.js";
+import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
+import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
+
+/**
+ * Interface para configuração de um servidor MCP
+ * 
+ * Por que criar essa interface?
+ * - Define claramente o que é necessário para cada MCP
+ * - Facilita validação e documentação
+ * - TypeScript garante que não esquecemos nenhum campo
+ */
+interface ConfiguracaoMCP {
+  id: string;                    // Identificador único (ex: 'academia-v1')
+  nome: string;                  // Nome amigável (ex: 'Academia PT-BR')
+  tipo: 'stdio' | 'sse';         // Tipo de transporte
+  url?: string;                 // URL para conexão SSE (obrigatório se tipo='sse')
+  versao: string;                // Versão do servidor
+  caminhoScript?: string;        // Caminho para o script do servidor (obrigatório se tipo='stdio')
+  idioma: 'pt-br' | 'en';       // Idioma do servidor
+  dominios: string[];            // Domínios que o MCP conhece (ex: ['exercicios', 'treinos'])
+  ferramentas?: string[];        // Lista de ferramentas disponíveis (opcional)
+}
+
+/**
+ * Interface para um cliente MCP ativo
+ */
+interface ClienteMCPAtivo {
+  config: ConfiguracaoMCP;
+  client: Client;
+  transport: StdioClientTransport | StreamableHTTPClientTransport;
+  conectadoEm: Date;
+  ferramentasDisponiveis: any[]; // Armazena a definição completa das ferramentas
+}
+
+/**
+ * Classe que gerencia múltiplos servidores MCP
+ * 
+ * Por que usar uma classe?
+ * - Encapsula estado (clientes conectados)
+ * - Métodos organizados e reutilizáveis
+ * - Facilita testes unitários
+ * - Segue o princípio de Responsabilidade Única
+ */
+class GerenciadorMCP {
+  private configuracoes: Map<string, ConfiguracaoMCP> = new Map();
+  private clientes: Map<string, ClienteMCPAtivo> = new Map();
+
+  /**
+   * Registra um novo servidor MCP
+   * 
+   * Por que registrar antes de conectar?
+   * - Permite inicialização lazy (só conecta quando necessário)
+   * - Facilita descoberta (listar MCPs disponíveis)
+   * - Validação prévia de configuração
+   */
+  registrarMCP(config: ConfiguracaoMCP): void {
+    if (this.configuracoes.has(config.id)) {
+      console.warn(`[MCP-MANAGER] MCP ${config.id} já está registrado, sobrescrevendo...`);
+    }
+
+    this.configuracoes.set(config.id, config);
+    console.log(`[MCP-MANAGER] MCP registrado: ${config.nome} (${config.id})`);
+  }
+
+  /**
+   * Conecta a um servidor MCP específico
+   * 
+   * Por que conexão sob demanda?
+   * - Economiza recursos (não conecta MCPs não usados)
+   * - Mais rápido na inicialização
+   * - Falha em um MCP não impede os outros
+   */
+  async conectarMCP(id: string): Promise<void> {
+    // Se já está conectado, não faz nada
+    if (this.clientes.has(id)) {
+      console.log(`[MCP-MANAGER] MCP ${id} já está conectado`);
+      return;
+    }
+
+    const config = this.configuracoes.get(id);
+    if (!config) {
+      throw new Error(`MCP ${id} não está registrado`);
+    }
+
+    try {
+      console.log(`[MCP-MANAGER] Conectando ao MCP: ${config.nome}...`);
+
+      let transport;
+
+      if (config.tipo === 'sse') {
+        if (!config.url) {
+          throw new Error(`MCP ${id} configurado como SSE mas sem URL`);
+        }
+        console.log(`[MCP-MANAGER] Inicializando transporte SSE: ${config.url}`);
+        transport = new StreamableHTTPClientTransport(new URL(config.url));
+      } else {
+        if (!config.caminhoScript) {
+          throw new Error(`MCP ${id} configurado como STDIO mas sem caminhoScript`);
+        }
+
+        // Cria o transporte stdio
+        transport = new StdioClientTransport({
+          command: "bun",
+          args: ["run", config.caminhoScript],
+        });
+      }
+
+      // Cria o cliente MCP v2.0
+      const client = new Client(
+        { name: `api-client-${id}`, version: "1.0.0" },
+        { capabilities: {} }
+      );
+
+      // Conecta o transporte
+      await client.connect(transport);
+      console.log(`[MCP-MANAGER] Transporte conectado para ${id} (Streamable HTTP v2024-11-05)`);
+
+      // Lista ferramentas disponíveis
+      let ferramentasDisponiveis: any[] = [];
+      try {
+        const result = await client.listTools();
+        ferramentasDisponiveis = result.tools;
+        console.log(`[MCP-MANAGER] Ferramentas carregadas de ${id}: ${ferramentasDisponiveis.length} ferramentas`);
+        ferramentasDisponiveis.slice(0, 3).forEach((tool: any) => {
+          console.log(`[MCP-MANAGER]   - ${tool.name}`);
+        });
+      } catch (erro) {
+        console.warn(`[MCP-MANAGER] Não foi possível listar ferramentas de ${id}`, erro);
+      }
+
+      // Salva o cliente ativo
+      this.clientes.set(id, {
+        config,
+        client,
+        transport,
+        conectadoEm: new Date(),
+        ferramentasDisponiveis
+      });
+
+      console.log(`[MCP-MANAGER] MCP ${config.nome} conectado com sucesso`);
+
+    } catch (erro) {
+      console.error(`[MCP-MANAGER] Erro ao conectar MCP ${id}:`, erro);
+      throw erro;
+    }
+  }
+
+  /**
+   * Conecta a todos os MCPs registrados
+   * 
+   * Útil para pré-conectar na inicialização da API
+   * Agora com conformidade MCP v2.0 (protocolo 2024-11-05)
+   */
+  async conectarTodos(): Promise<void> {
+    const ids = Array.from(this.configuracoes.keys());
+
+    console.log(`[MCP-MANAGER] Conectando a ${ids.length} MCPs com protocolo v2024-11-05...`);
+
+    // Conecta em paralelo para ser mais rápido
+    // Por que Promise.allSettled? Para não interromper se um falhar
+    const resultados = await Promise.allSettled(
+      ids.map(id => this.conectarMCP(id))
+    );
+
+    const sucessos = resultados.filter(r => r.status === 'fulfilled').length;
+    const falhas = resultados.filter(r => r.status === 'rejected').length;
+
+    console.log(`[MCP-MANAGER] ✓ Conectados: ${sucessos} | ✗ Falhas: ${falhas}`);
+
+    if (sucessos > 0) {
+      console.log(`[MCP-MANAGER] MCPs ativos:`);
+      this.listarMCPsConectados().forEach((config: ConfiguracaoMCP) => {
+        console.log(`[MCP-MANAGER]   - ${config.nome} (${config.tipo})`);
+      });
+    }
+  }
+
+  /**
+   * Chama uma ferramenta em um MCP específico
+   * 
+   * Por que especificar o MCP?
+   * - Permite controlar qual servidor usar
+   * - Evita ambiguidade se dois MCPs tiverem ferramentas com mesmo nome
+   * - Mais explícito e fácil de debugar
+   */
+  async chamarFerramenta(
+    mcpId: string,
+    nomeFerramenta: string,
+    argumentos: Record<string, any>
+  ): Promise<string> {
+    // Garante que está conectado
+    if (!this.clientes.has(mcpId)) {
+      await this.conectarMCP(mcpId);
+    }
+
+    const clienteAtivo = this.clientes.get(mcpId);
+    if (!clienteAtivo) {
+      throw new Error(`MCP ${mcpId} não está conectado`);
+    }
+
+    console.log(`[MCP-MANAGER] Chamando ${nomeFerramenta} em ${mcpId}`);
+    console.log(`[MCP-MANAGER]   Argumentos:`, JSON.stringify(argumentos).substring(0, 100));
+
+    try {
+      const resultado = await clienteAtivo.client.callTool({
+        name: nomeFerramenta,
+        arguments: argumentos,
+      });
+
+      // Valida resposta v2.0
+      if (!resultado) {
+        console.warn(`[MCP-MANAGER] Resultado vazio para ${nomeFerramenta}`);
+        return '';
+      }
+
+      // Processa o resultado para retornar string (compatibilidade)
+      if (!resultado.content || !Array.isArray(resultado.content) || resultado.content.length === 0) {
+        console.warn(`[MCP-MANAGER] ${nomeFerramenta} retornou content vazio`);
+        return '';
+      }
+
+      // Extrai texto do array de conteúdo (v2.0)
+      const conteudo = (resultado.content as any[])
+        .filter((item: any) => item && item.type === 'text')
+        .map((item: any) => item.text || '')
+        .join('\n');
+
+      if (!conteudo) {
+        console.warn(`[MCP-MANAGER] Nenhum conteúdo text extraído de ${nomeFerramenta}`);
+      }
+
+      console.log(`[MCP-MANAGER]   ✓ Resposta recebida (${conteudo.length} caracteres)`);
+      return conteudo;
+
+    } catch (erro) {
+      console.error(`[MCP-MANAGER] Erro ao chamar ${nomeFerramenta}:`, erro);
+      throw erro;
+    }
+  }
+
+  /**
+   * Retorna todas as ferramentas de todos os MCPs conectados
+   */
+  obterTodasFerramentas(): any[] {
+    const todas: any[] = [];
+    for (const cliente of this.clientes.values()) {
+      todas.push(...cliente.ferramentasDisponiveis);
+    }
+    return todas;
+  }
+
+  /**
+   * Busca e chama uma ferramenta pelo nome em qualquer MCP conectado
+   */
+  async chamarFerramentaGlobal(nome: string, args: any): Promise<string> {
+    // Encontra qual MCP tem essa ferramenta
+    let mcpIdAlvo: string | null = null;
+
+    for (const [id, cliente] of this.clientes.entries()) {
+      const temFerramenta = cliente.ferramentasDisponiveis.some(t => t.name === nome);
+      if (temFerramenta) {
+        mcpIdAlvo = id;
+        break;
+      }
+    }
+
+    if (!mcpIdAlvo) {
+      throw new Error(`Ferramenta '${nome}' não encontrada em nenhum MCP conectado.`);
+    }
+
+    return this.chamarFerramenta(mcpIdAlvo, nome, args);
+  }
+
+  /**
+   * Busca automaticamente qual MCP usar baseado em critérios
+   * 
+   * Esta é a mágica que escolhe o MCP certo automaticamente!
+   * 
+   * Como funciona?
+   * 1. Verifica idioma da mensagem
+   * 2. Verifica domínios (exercícios, nutrição, etc)
+   * 3. Retorna o MCP mais adequado
+   */
+  selecionarMCP(
+    mensagem: string,
+    criterios?: {
+      idioma?: 'pt-br' | 'en';
+      dominio?: string;
+    }
+  ): string | null {
+    const mensagemLower = mensagem.toLowerCase();
+    console.log(`[MCP-MANAGER] Selecionando MCP para mensagem: "${mensagemLower}"`);
+
+    // Detecta idioma se não foi especificado
+    const idiomaDetectado = criterios?.idioma || this.detectarIdioma(mensagem);
+
+
+    console.log(`[MCP-MANAGER] Selecionando MCP para idioma: ${idiomaDetectado}`);
+
+    // Filtra MCPs compatíveis
+    const mcpsCompativeis = Array.from(this.configuracoes.values()).filter(config => {
+      // Verifica idioma
+      if (config.idioma !== idiomaDetectado) {
+        return false;
+      }
+
+      // Verifica domínio se especificado
+      if (criterios?.dominio) {
+        return config.dominios.includes(criterios.dominio);
+      }
+
+      return true;
+    });
+
+    if (mcpsCompativeis.length === 0) {
+      console.warn(`[MCP-MANAGER] Nenhum MCP compatível encontrado`);
+      return null;
+    }
+
+    // Por enquanto, retorna o primeiro compatível
+    // Pode evoluir para scoring baseado em relevância
+    const selecionado = mcpsCompativeis[0];
+    if (selecionado) {
+      console.log(`[MCP-MANAGER] MCP selecionado: ${selecionado.nome}`);
+      return selecionado.id;
+    }
+
+    return null;
+  }
+
+  /**
+   * Detecta o idioma de uma mensagem
+   * 
+   * Por que detectar?
+   * - Permite escolher o MCP certo automaticamente
+   * - Usuário não precisa especificar
+   * - Melhora a experiência
+   * 
+   * Como funciona?
+   * - Procura por palavras comuns em cada idioma
+   * - Simples mas eficaz para nosso caso
+   * - Pode evoluir para usar biblioteca de detecção de idioma
+   */
+  private detectarIdioma(mensagem: string): 'pt-br' | 'en' {
+    const mensagemLower = mensagem.toLowerCase();
+
+    // Palavras comuns em português
+    const palavrasPT = [
+      'o que', 'como', 'quais', 'qual', 'onde', 'quando',
+      'meu', 'meus', 'minha', 'minhas',
+      'exercício', 'exercícios', 'treino',
+      'lista', 'listar', 'mostre', 'mostrar'
+    ];
+
+    // Palavras comuns em inglês
+    const palavrasEN = [
+      'what', 'how', 'which', 'where', 'when',
+      'my', 'mine',
+      'exercise', 'exercises', 'workout',
+      'list', 'show', 'display'
+    ];
+
+    const contagemPT = palavrasPT.filter(p => mensagemLower.includes(p)).length;
+    const contagemEN = palavrasEN.filter(p => mensagemLower.includes(p)).length;
+
+    // Padrão é português
+    return contagemEN > contagemPT ? 'en' : 'pt-br';
+  }
+
+  /**
+   * Lista todos os MCPs disponíveis
+   */
+  listarMCPs(): ConfiguracaoMCP[] {
+    return Array.from(this.configuracoes.values());
+  }
+
+  /**
+   * Lista apenas MCPs conectados
+   */
+  listarMCPsConectados(): ConfiguracaoMCP[] {
+    return Array.from(this.clientes.values()).map(c => c.config);
+  }
+
+  /**
+   * Verifica se um MCP específico está conectado
+   */
+  estaConectado(id: string): boolean {
+    return this.clientes.has(id);
+  }
+
+  /**
+   * Desconecta um MCP específico
+   */
+  async desconectarMCP(id: string): Promise<void> {
+    const clienteAtivo = this.clientes.get(id);
+    if (!clienteAtivo) {
+      return;
+    }
+
+    try {
+      await clienteAtivo.client.close();
+      this.clientes.delete(id);
+      console.log(`[MCP-MANAGER] MCP ${id} desconectado`);
+    } catch (erro) {
+      console.error(`[MCP-MANAGER] Erro ao desconectar ${id}:`, erro);
+    }
+  }
+
+  /**
+   * Desconecta todos os MCPs
+   * 
+   * Deve ser chamado quando a aplicação for encerrar
+   * Implementa graceful shutdown conforme MCP v2.0
+   */
+  async desconectarTodos(): Promise<void> {
+    const ids = Array.from(this.clientes.keys());
+
+    if (ids.length === 0) {
+      console.log(`[MCP-MANAGER] Nenhum MCP conectado para desconectar`);
+      return;
+    }
+
+    console.log(`[MCP-MANAGER] Desconectando ${ids.length} MCPs...`);
+
+    await Promise.allSettled(
+      ids.map(id => this.desconectarMCP(id))
+    );
+
+    console.log(`[MCP-MANAGER] ✓ Todos os MCPs desconectados com sucesso`);
+  }
+}
+
+// Singleton do gerenciador
+// Por que singleton? Para ter uma única instância compartilhada em toda a aplicação
+const gerenciadorMCP = new GerenciadorMCP();
+
+/**
+ * Função de inicialização que registra todos os MCPs disponíveis
+ * 
+ * Esta função deve ser chamada na inicialização da API
+ * 
+ * Conformidade: MCP v2.0 (protocolo 2024-11-05 com Streamable HTTP)
+ */
+async function inicializarMCPs(): Promise<void> {
+  console.log('[MCP-MANAGER] ════════════════════════════════════════');
+  console.log('[MCP-MANAGER] Inicializando MCPs - Protocolo v2024-11-05');
+  console.log('[MCP-MANAGER] ════════════════════════════════════════');
+
+  // Registra MCP v1 (Português) via Stdio Local
+  // gerenciadorMCP.registrarMCP({
+  //   id: 'academia-v1',
+  //   nome: 'Academia MCP v1 (PT-BR) (STDIO)',
+  //   tipo: 'stdio',
+  //   versao: '1.0.0',
+  //   // Caminho relativo ao api-chatbot-academia
+  //   caminhoScript: join(__dirname, '../../servidores-mcp/base-de-dados-academia/via-stdio.ts'),
+  //   idioma: 'pt-br',
+  //   dominios: ['exercicios', 'treinos', 'musculacao'],
+  //   ferramentas: [
+  //     'buscar_exercicios_por_grupo',
+  //     'listar_grupos_musculares',
+  //     'buscar_exercicio_por_nome',
+  //     'listar_todos_exercicios',
+  //     'obter_detalhes_exercicio'
+  //   ]
+  // });
+
+  // Exemplo de como registrar um MCP via SSE (para futura integração remota)
+
+  gerenciadorMCP.registrarMCP({
+    id: 'academia-remota-sse',
+    nome: 'Academia MCP Remoto (SSE)',
+    tipo: 'sse',
+    url: process.env.MCP_URL_ACADEMIA || 'http://localhost:3401/mcp', // URL do endpoint SSE (DEVE incluir /mcp)
+    versao: '1.0.0',
+    idioma: 'pt-br',
+    dominios: ['exercicios', 'treinos', 'musculacao'],
+    ferramentas: [
+      'buscar_exercicios_por_grupo',
+      'listar_grupos_musculares',
+      'buscar_exercicio_por_nome',
+      'listar_todos_exercicios',
+      'obter_detalhes_exercicio'
+    ]
+  });
+
+  // Conecta a todos
+  try {
+    await gerenciadorMCP.conectarTodos();
+    console.log('[MCP-MANAGER] ✓ Inicialização concluída com sucesso');
+    console.log('[MCP-MANAGER] Protocolo: v2024-11-05 (Streamable HTTP)');
+  } catch (erro) {
+    console.error('[MCP-MANAGER] ✗ Erro na inicialização:', erro);
+    console.log('[MCP-MANAGER] API continuará funcionando sem MCPs');
+    // Não lança erro para não impedir a API de iniciar
+  }
+}
+
+
+
+export {
+  gerenciadorMCP,
+  inicializarMCPs,
+  type ConfiguracaoMCP
+};

+ 97 - 0
ollama.ts

@@ -0,0 +1,97 @@
+// OLLAMA
+//
+// URL base do Ollama
+// Importante: Centralizamos a configuração aqui para facilitar mudanças futuras
+//
+const OLLAMA_BASE_URL = 'http://localhost:11434';
+//
+// Interface para tipar a requisição ao Ollama
+// Isso nos ajuda a ter autocompletar e evitar erros de digitação
+interface OllamaMessage {
+  role: 'user' | 'assistant' | 'system';
+  content: string;
+}
+
+interface OllamaChatRequest {
+  model: string;
+  messages: OllamaMessage[];
+  stream?: boolean;
+}
+
+interface OllamaChatResponse {
+  model: string;
+  created_at: string;
+  message: {
+    role: string;
+    content: string;
+  };
+  done: boolean;
+}
+
+// Método que chama o Ollama via HTTP request
+// Este método encapsula toda a lógica de comunicação com o Ollama
+// Seguindo o princípio de Responsabilidade Única (Single Responsibility - SOLID)
+// 
+// Agora aceita um array de mensagens para suportar RAG com contexto
+async function chamarOllama(
+  mensagemUsuario: string, 
+  mensagemSistema?: string,
+  modelo: string = 'qwen3:0.6b',
+): Promise<string> {
+  try {
+    // Prepara as mensagens
+    // Se houver mensagem de sistema (contexto RAG), adiciona primeiro
+    const messages: OllamaMessage[] = [];
+    
+    if (mensagemSistema) {
+      messages.push({
+        role: 'system',
+        content: mensagemSistema
+      });
+    }
+    
+    messages.push({
+      role: 'user',
+      content: mensagemUsuario
+    });
+
+    // Preparamos o corpo da requisição conforme a documentação do Ollama
+    const requestBody: OllamaChatRequest = {
+      model: modelo, // Você pode mudar para outros modelos como 'mistral', 'codellama', etc
+      messages: messages,
+      stream: false // Desabilitamos o streaming para simplificar a resposta
+    };
+
+    // Fazemos a requisição HTTP POST para o endpoint do Ollama
+    // O endpoint /api/chat é o padrão para conversas
+    const response = await fetch(`${OLLAMA_BASE_URL}/api/chat`, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify(requestBody)
+    });
+
+    // Verificamos se a requisição foi bem-sucedida
+    // Status 200-299 são considerados sucesso
+    if (!response.ok) {
+      throw new Error(`Ollama retornou status ${response.status}`);
+    }
+
+    // Convertemos a resposta para JSON
+    const data = await response.json() as OllamaChatResponse;
+
+    // Retornamos apenas o conteúdo da mensagem do assistente
+    return data.message.content;
+
+  } catch (erro) {
+    // Registramos o erro no console para facilitar debug
+    console.error('Erro ao chamar Ollama:', erro);
+    
+    // Lançamos um erro mais descritivo
+    // Isso permite que quem chama essa função saiba o que aconteceu
+    throw new Error('Falha na comunicação com o Ollama');
+  }
+}
+
+export { chamarOllama };

+ 412 - 0
orquestrador.ts

@@ -0,0 +1,412 @@
+/**
+ * ORQUESTRADOR INTELIGENTE (Versão Multi-MCP)
+ * 
+ * Agora suporta múltiplos servidores MCP!
+ * 
+ * O que mudou?
+ * - Antes: Um único cliente MCP
+ * - Agora: Gerenciador com múltiplos MCPs
+ * - Seleção automática do MCP baseado em idioma/domínio
+ */
+
+import { buscarContexto, type ResultadoBuscaComContexto } from './rag';
+import { gerenciadorMCP } from './mcp-manager';
+// import { resolve } from 'bun';
+
+/**
+ * Enum para definir a estratégia de busca
+ * 
+ * Por que usar enum?
+ * - Tipagem forte (TypeScript nos avisa de erros)
+ * - Autodocumentação (fica claro quais são as opções)
+ * - Fácil de estender no futuro
+ */
+enum EstrategiaBusca {
+  APENAS_RAG = 'apenas_rag',
+  APENAS_MCP = 'apenas_mcp',
+  HIBRIDO = 'hibrido',
+  DIRETO = 'direto' // Sem contexto, chat direto
+}
+
+/**
+ * Interface para o resultado da orquestração
+ */
+interface ResultadoOrquestracao {
+  estrategia: EstrategiaBusca;
+  contextoRAG?: string;
+  contextoMCP?: string;
+  documentosRAG?: Array<{ nome: string; similaridade: number }>;
+  ferramentasMCP?: string[];
+  mcpUtilizado?: string; // NOVO: qual MCP foi usado
+  promptFinal: string;
+}
+
+/**
+ * BUSCADOR MCP (Versão Multi-MCP)
+ * 
+ * Agora detecta automaticamente qual MCP usar!
+ */
+async function buscarDadosMCP(mensagem: string): Promise<{ conteudo: string; mcpId: string }> {
+  const mensagemLower = mensagem.toLowerCase();
+
+  // Passo 1: Selecionar qual MCP usar
+  const mcpId = gerenciadorMCP.selecionarMCP(mensagem);
+  
+  if (!mcpId) {
+    console.warn('[ORQUESTRADOR] Nenhum MCP disponível');
+    return {
+      conteudo: 'Sistema de dados de exercícios temporariamente indisponível.',
+      mcpId: 'nenhum'
+    };
+  }
+
+  console.log(`[ORQUESTRADOR] Usando MCP: ${mcpId}`);
+
+  try {
+    // Passo 2: Decidir qual ferramenta chamar
+    // As ferramentas podem ter nomes diferentes entre MCPs (pt vs en)
+    const ferramentas = getFerramentaMapping(mcpId);
+
+    // Estratégia 1: Busca por nome de exercício
+    const nomesExercicios = ['supino', 'agachamento', 'rosca', 'levantamento', 'squat', 'bench'];
+    
+    for (const nome of nomesExercicios) {
+      if (mensagemLower.includes(nome)) {
+        try {
+          const resultado = await gerenciadorMCP.chamarFerramenta(
+            mcpId,
+            ferramentas.buscarPorNome,
+            { [ferramentas.paramNome]: nome }
+          );
+          
+          if (!resultado.includes('Nenhum') && !resultado.includes('No exercise')) {
+            return { conteudo: resultado, mcpId };
+          }
+        } catch (erro) {
+          console.error('[ORQUESTRADOR] Erro ao buscar por nome:', erro);
+        }
+      }
+    }
+
+    // Estratégia 2: Busca por grupo muscular
+    const grupos = detectarGrupoMuscular(mensagemLower);
+    if (grupos.length > 0) {
+      try {
+        const resultado = await gerenciadorMCP.chamarFerramenta(
+          mcpId,
+          ferramentas.buscarPorGrupo,
+          { [ferramentas.paramGrupo]: grupos[0] }
+        );
+        return { conteudo: resultado, mcpId };
+      } catch (erro) {
+        console.error('[ORQUESTRADOR] Erro ao buscar por grupo:', erro);
+      }
+    }
+
+    // Estratégia 3: Listar todos
+    const resultado = await gerenciadorMCP.chamarFerramenta(
+      mcpId,
+      ferramentas.listarTodos,
+      {}
+    );
+    return { conteudo: resultado, mcpId };
+
+  } catch (erro) {
+    console.error('[ORQUESTRADOR] Erro ao buscar dados MCP:', erro);
+    return {
+      conteudo: 'Não foi possível acessar os dados de exercícios.',
+      mcpId
+    };
+  }
+}
+
+/**
+ * Mapeia nomes de ferramentas para cada MCP
+ * 
+ * Por que criar esse mapeamento?
+ * - MCPs diferentes podem ter nomes diferentes
+ * - v1 usa português, v2 usa inglês
+ * - Centraliza a lógica de compatibilidade
+ */
+function getFerramentaMapping(mcpId: string): any {
+  if (mcpId === 'academia-v1') {
+    return {
+      buscarPorNome: 'buscar_exercicio_por_nome',
+      buscarPorGrupo: 'buscar_exercicios_por_grupo',
+      listarTodos: 'listar_todos_exercicios',
+      paramNome: 'nome',
+      paramGrupo: 'grupo_muscular'
+    };
+  }
+  
+  if (mcpId === 'academia-v2') {
+    return {
+      buscarPorNome: 'buscar_exercicio_por_nome', // mesmo nome
+      buscarPorGrupo: 'search_exercises_by_group',
+      listarTodos: 'list_all_exercises',
+      paramNome: 'nome',
+      paramGrupo: 'muscle_group'
+    };
+  }
+
+  // Padrão (v1)
+  return {
+    buscarPorNome: 'buscar_exercicio_por_nome',
+    buscarPorGrupo: 'buscar_exercicios_por_grupo',
+    listarTodos: 'listar_todos_exercicios',
+    paramNome: 'nome',
+    paramGrupo: 'grupo_muscular'
+  };
+}
+
+/**
+ * Detecta grupos musculares na mensagem
+ */
+function detectarGrupoMuscular(mensagem: string): string[] {
+  const grupos = [
+    { palavras: ['perna', 'pernas', 'leg', 'legs'], pt: 'Pernas', en: 'Legs' },
+    { palavras: ['peito', 'peitoral', 'chest'], pt: 'Peito (peitoral)', en: 'Chest' },
+    { palavras: ['costas', 'dorsal', 'back'], pt: 'Costas (dorsais, lombar)', en: 'Back' },
+    { palavras: ['ombro', 'ombros', 'shoulder'], pt: 'Ombros (deltoides)', en: 'Shoulders' },
+    { palavras: ['braço', 'braços', 'arm', 'arms'], pt: 'Braços (Bíceps, Tríceps, Antebraço)', en: 'Arms' }
+  ];
+
+  const encontrados = [];
+  for (const grupo of grupos) {
+    if (grupo.palavras.some(p => mensagem.includes(p))) {
+      // Retorna ambas as versões
+      encontrados.push(grupo.pt, grupo.en);
+    }
+  }
+
+  return encontrados;
+}
+
+function detectarPorSimilaridade(mensagem: string, exemplos: string[]): boolean {
+  // Simples: verifica se alguma palavra da mensagem está em exemplos
+  const palavrasMensagem = mensagem.toLowerCase().split(' ');
+  return exemplos.some(ex => palavrasMensagem.some(p => ex.includes(p)));
+}
+
+/**
+ * ANALISADOR DE INTENÇÃO
+ * 
+ * Esta função analisa a pergunta do usuário e decide qual estratégia usar
+ * 
+ * Como funciona?
+ * 1. Procura por palavras-chave que indicam dados estruturados (MCP)
+ * 2. Sempre tenta RAG para contexto documental
+ * 3. Decide se usa ambos baseado na relevância
+ * 
+ * Por que essa abordagem?
+ * - Simples e eficaz (KISS)
+ * - Pode evoluir para usar o próprio LLM para decidir
+ * - Transparente para debug
+ */
+function analisarIntencao(mensagem: string): EstrategiaBusca {
+
+  const mensagemLower = mensagem.toLowerCase();
+
+  console.log(`[ANALISADOR] Analisando: "${mensagem}"`);
+
+  // Palavras que indicam necessidade de dados estruturados (MCP)
+  const palavrasChaveMCP = [
+  'meu', 'meus', 'minhas', 'minha',
+  'my', 'mine',
+  'cadastrado', 'cadastrados', 'registered',
+  'tenho', 'possuo', 'have',
+  'listar', 'liste', 'list', 'mostrar', 'mostre', 'show',
+  // Novos: adicionar sinônimos para mais flexibilidade
+  'quais', 'qual', 'que', 'existe', 'disponível', 'disponíveis',
+  'exercícios', 'exercicio', 'treino', 'treinos', 'rotina'
+];
+
+  // Palavras que indicam necessidade de conhecimento/técnica (RAG)
+  const palavrasChaveRAG = [
+  'como fazer', 'como executar', 'how to',
+  'o que é', 'what is',
+  'explique', 'explain',
+  'técnica', 'technique',
+  'forma correta', 'correct form',
+  // Novos: adicionar variações
+  'dica', 'dicas', 'passo', 'passos', 'guia', 'tutorial',
+  'benefício', 'benefícios', 'vantagem', 'vantagens'
+];
+
+  // Verifica indicadores MCP
+  const indicadoresMCP = palavrasChaveMCP.filter(palavra => 
+    mensagemLower.includes(palavra)
+  );
+
+  // Verifica indicadores RAG
+  const indicadoresRAG = palavrasChaveRAG.filter(palavra => 
+    mensagemLower.includes(palavra)
+  );
+
+  console.log(`[ANALISADOR] Indicadores MCP encontrados: [${indicadoresMCP.join(', ')}]`);
+  console.log(`[ANALISADOR] Indicadores RAG encontrados: [${indicadoresRAG.join(', ')}]`);
+
+
+/*
+Este trecho de código visa
+tentar dar outra opção para a analise
+// Novo: exemplos de frases para detectar intenção
+const exemplosMCP = [
+  'liste meus exercícios',
+  'quais treinos tenho',
+  'mostre dados cadastrados'
+];
+
+const exemplosRAG = [
+  'como fazer supino',
+  'explique técnica de agachamento',
+  'dicas para musculação'
+];
+
+function detectarPorSimilaridade(mensagem: string, exemplos: string[]): boolean {
+  // Simples: verifica se alguma palavra da mensagem está em exemplos
+  const palavrasMensagem = mensagem.toLowerCase().split(' ');
+  return exemplos.some(ex => palavrasMensagem.some(p => ex.includes(p)));
+}
+*/
+// Novo: exemplos de frases para detectar intenção
+const exemplosMCP = [
+  'liste meus exercícios',
+  'quais treinos tenho',
+  'mostre dados cadastrados'
+];
+
+const exemplosRAG = [
+  'como fazer supino',
+  'explique técnica de agachamento',
+  'dicas para musculação'
+];
+
+
+
+
+  // const temIndicadorMCP = indicadoresMCP.length > 0;
+  // const temIndicadorRAG = indicadoresRAG.length > 0;
+
+  const temIndicadorMCP = indicadoresMCP.length > 0 || detectarPorSimilaridade(mensagemLower, exemplosMCP);
+  const temIndicadorRAG = indicadoresRAG.length > 0 || detectarPorSimilaridade(mensagemLower, exemplosRAG);
+
+  // Lógica de decisão
+  if (temIndicadorMCP && temIndicadorRAG) {
+    console.log('[ANALISADOR] Decisão: HÍBRIDO (ambos indicadores presentes)');
+    return EstrategiaBusca.HIBRIDO;
+  }
+
+  if (temIndicadorMCP) {
+    console.log('[ANALISADOR] Decisão: APENAS MCP (dados estruturados)');
+    return EstrategiaBusca.APENAS_MCP;
+  }
+
+  if (temIndicadorRAG) {
+    console.log('[ANALISADOR] Decisão: APENAS RAG (conhecimento/técnica)');
+    return EstrategiaBusca.APENAS_RAG;
+  }
+
+  // Padrão: Sem indicador
+  console.log('[ANALISADOR] Decisão: sem parametrização');
+  return EstrategiaBusca.DIRETO;
+}
+
+/**
+ * ORQUESTRADOR PRINCIPAL (Versão Multi-MCP)
+ */
+async function orquestrar(mensagem: string): Promise<ResultadoOrquestracao> {
+  console.log('\n[ORQUESTRADOR] Analisando pergunta...');
+  
+  const estrategia = analisarIntencao(mensagem);
+  console.log(`[ORQUESTRADOR] Estratégia: ${estrategia}`);
+
+  let contextoRAG = '';
+  let contextoMCP = '';
+  let documentosRAG: Array<{ nome: string; similaridade: number }> = [];
+  let ferramentasMCP: string[] = [];
+  let mcpUtilizado: string | undefined;
+
+  switch (estrategia) {
+    case EstrategiaBusca.APENAS_RAG:
+      const resultadoRAG = await buscarContexto(mensagem, 3, 0.3);
+      if (resultadoRAG.resultados.length > 0) {
+        contextoRAG = resultadoRAG.contexto;
+        documentosRAG = resultadoRAG.resultados.map(doc => ({ nome: doc.nome, similaridade: doc.similaridade }));
+      }
+      break;
+
+    case EstrategiaBusca.APENAS_MCP:
+      const resultadoMCP = await buscarDadosMCP(mensagem);
+      contextoMCP = resultadoMCP.conteudo;
+      mcpUtilizado = resultadoMCP.mcpId;
+      ferramentasMCP.push(mcpUtilizado);
+      break;
+
+    case EstrategiaBusca.HIBRIDO:
+      const resultadoHibridoRAG = await buscarContexto(mensagem, 2, 0.3);
+      if (resultadoHibridoRAG.resultados.length > 0) {
+        contextoRAG = resultadoHibridoRAG.contexto;
+        documentosRAG = resultadoHibridoRAG.resultados.map(doc => ({ nome: doc.nome, similaridade: doc.similaridade }));
+      }
+
+      const resultadoHibridoMCP = await buscarDadosMCP(mensagem);
+      contextoMCP = resultadoHibridoMCP.conteudo;
+      mcpUtilizado = resultadoHibridoMCP.mcpId;
+      ferramentasMCP.push(mcpUtilizado);
+      break;
+  }
+
+  const promptFinal = montarPromptFinal(mensagem, contextoRAG, contextoMCP);
+
+  return {
+    estrategia,
+    contextoRAG: contextoRAG || undefined,
+    contextoMCP: contextoMCP || undefined,
+    documentosRAG,
+    ferramentasMCP,
+    mcpUtilizado,
+    promptFinal
+  };
+}
+
+/**
+ * Monta o prompt final para a LLM
+ * 
+ * Por que essa função separada?
+ * - Facilita testar diferentes formatos de prompt
+ * - Mantém a lógica de prompt isolada
+ * - Fácil de ajustar sem mexer na orquestração
+ */
+function montarPromptFinal(
+  mensagem: string,
+  contextoRAG: string,
+  contextoMCP: string
+): string {
+  let prompt = 'Você é um assistente especializado em saúde, fitness e bem-estar.\n\n';
+
+  // Adiciona contexto RAG se existir
+  if (contextoRAG) {
+    prompt += contextoRAG + '\n';
+  }
+
+  // Adiciona contexto MCP se existir
+  if (contextoMCP) {
+    prompt += 'DADOS ESTRUTURADOS:\n\n' + contextoMCP + '\n\n';
+  }
+
+  // Instruções para a LLM
+  prompt += 'INSTRUÇÕES:\n';
+  prompt += '- Use as informações fornecidas\n';
+  prompt += '- Seja claro e objetivo\n';
+  prompt += '- Responda no idioma da pergunta\n\n';
+  prompt += `PERGUNTA: ${mensagem}\n\nRESPOSTA:`;
+
+  return prompt;
+}
+
+export {
+  orquestrar,
+  EstrategiaBusca,
+  type ResultadoOrquestracao
+};

+ 19 - 0
package.json

@@ -0,0 +1,19 @@
+{
+  "name": "api-chatbot-academia",
+  "module": "index.ts",
+  "type": "module",
+  "private": true,
+  "devDependencies": {
+    "@types/bun": "latest"
+  },
+  "peerDependencies": {
+    "typescript": "^5"
+  },
+  "dependencies": {
+    "@modelcontextprotocol/sdk": "^1.25.3",
+    "@types/cors": "^2.8.19",
+    "@types/express": "^5.0.6",
+    "cors": "^2.8.6",
+    "express": "^5.2.1"
+  }
+}

+ 179 - 0
rag.ts

@@ -0,0 +1,179 @@
+/**
+ * Módulo RAG (Retrieval-Augmented Generation)
+ * 
+ * Este módulo integra a busca por documentos similares com a geração de respostas
+ * Ele importa as funções da pasta rag e as adapta para uso na API
+ */
+
+// URL da nova API de embeddings
+const EMBEDDINGS_API_URL = process.env.EMBEDDINGS_API_URL || 'http://localhost:3402/api/embeddings';
+console.log(`[API::rag.ts]Usando EMBEDDINGS_API_URL: ${EMBEDDINGS_API_URL}`);
+
+/**
+ * Interface para os resultados da busca
+ */
+interface ResultadoBusca {
+  nome: string;
+  conteudo: string;
+  similaridade: number;
+}
+
+/**
+ * Interface para o resultado da busca com contexto
+ */
+interface ResultadoBuscaComContexto {
+  contexto: string;
+  resultados: ResultadoBusca[];
+}
+
+/**
+ * Interface para a resposta da API de embeddings
+ */
+interface ApiEmbeddingsResponse {
+  contexto: string;
+  resultados: Array<{
+    documento: {
+      nome: string;
+      caminho: string;
+      conteudo: string;
+      tamanho: number;
+      embedding: number[];
+    };
+    similaridade: number;
+  }>;
+}
+
+/**
+ * Interface para a requisição da API de embeddings
+ */
+interface ApiEmbeddingsRequest {
+  prompt: string;
+  topK: number;
+  limiarSimilaridade: number;
+}
+
+
+
+/**
+ * Chama a API de embeddings para buscar documentos similares
+ */
+async function chamarApiEmbeddings(
+  prompt: string,
+  topK: number = 2,
+  limiarSimilaridade: number = 0.3
+): Promise<ApiEmbeddingsResponse> {
+  console.log('Chamando API de embeddings...');
+  try {
+    const requestBody: ApiEmbeddingsRequest = {
+      prompt,
+      topK,
+      limiarSimilaridade
+    };
+
+    const response = await fetch(EMBEDDINGS_API_URL, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify(requestBody)
+    });
+
+    if (!response.ok) {
+      throw new Error(`API de embeddings retornou status ${response.status}`);
+    }
+
+    const data = await response.json() as ApiEmbeddingsResponse;
+    return data;
+  } catch (erro) {
+    console.error('Erro ao chamar API de embeddings:', erro);
+    throw new Error('Falha na comunicação com a API de embeddings');
+  }
+}
+
+/**
+ * Busca documentos similares no banco de dados
+ * 
+ * Esta função é otimizada para uso na API:
+ * - Logs menos verbosos (para produção)
+ * - Retorna dados simplificados
+ * - Usa a nova API de embeddings
+ * 
+ * @param pergunta - A pergunta do usuário
+ * @param topK - Quantos documentos retornar (padrão: 2)
+ * @param limiarSimilaridade - Similaridade mínima (padrão: 0.3)
+ * @returns Objeto com contexto formatado e array com os documentos mais similares
+ */
+async function buscarContexto(
+  pergunta: string,
+  topK: number = 2,
+  limiarSimilaridade: number = 0.3
+): Promise<ResultadoBuscaComContexto> {
+  try {
+    // Chama a API de embeddings
+    const respostaApi = await chamarApiEmbeddings(pergunta, topK, limiarSimilaridade);
+
+    // Mapeia os resultados para o formato esperado
+    const resultados: ResultadoBusca[] = respostaApi.resultados.map(resultado => ({
+      nome: resultado.documento.nome,
+      conteudo: resultado.documento.conteudo,
+      similaridade: resultado.similaridade
+    }));
+
+    return {
+      contexto: respostaApi.contexto,
+      resultados
+    };
+
+  } catch (erro) {
+    console.error('Erro ao buscar contexto:', erro);
+    // Em caso de erro, retorna objeto vazio
+    return {
+      contexto: '',
+      resultados: []
+    };
+  }
+}
+
+/**
+ * Monta um prompt enriquecido com contexto RAG
+ * 
+ * Esta é a função principal que integra tudo:
+ * 1. Busca documentos relevantes usando a nova API
+ * 2. Retorna o contexto formatado da API
+ * 
+ * Por que usar a API?
+ * - Centraliza a lógica de busca e formatação
+ * - Reduz código duplicado
+ * - Melhora a manutenibilidade
+ * 
+ * @param pergunta - A pergunta do usuário
+ * @param topK - Quantos documentos usar (padrão: 2)
+ * @param limiarSimilaridade - Similaridade mínima (padrão: 0.3)
+ * @returns Prompt formatado com contexto
+ */
+async function montarPromptComRAG(
+  pergunta: string,
+  topK: number = 2,
+  limiarSimilaridade: number = 0.3
+): Promise<string> {
+  try {
+    // Chama a API de embeddings
+    const respostaApi = await chamarApiEmbeddings(pergunta, topK, limiarSimilaridade);
+
+    // Retorna o contexto formatado diretamente da API
+    return respostaApi.contexto;
+
+  } catch (erro) {
+    console.error('Erro ao montar prompt com RAG:', erro);
+    // Em caso de erro, retorna apenas a pergunta
+    return pergunta;
+  }
+}
+
+// Exporta as funções para serem usadas na API
+export {
+  buscarContexto,
+  montarPromptComRAG,
+  type ResultadoBusca,
+  type ResultadoBuscaComContexto
+};

+ 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
+  }
+}