@hed-hog/core 0.0.350 → 0.0.353

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (284) hide show
  1. package/dist/access-log/access-log.controller.d.ts +16 -0
  2. package/dist/access-log/access-log.controller.d.ts.map +1 -0
  3. package/dist/access-log/access-log.controller.js +44 -0
  4. package/dist/access-log/access-log.controller.js.map +1 -0
  5. package/dist/access-log/access-log.interceptor.d.ts +9 -0
  6. package/dist/access-log/access-log.interceptor.d.ts.map +1 -0
  7. package/dist/access-log/access-log.interceptor.js +46 -0
  8. package/dist/access-log/access-log.interceptor.js.map +1 -0
  9. package/dist/access-log/access-log.module.d.ts +3 -0
  10. package/dist/access-log/access-log.module.d.ts.map +1 -0
  11. package/dist/access-log/access-log.module.js +29 -0
  12. package/dist/access-log/access-log.module.js.map +1 -0
  13. package/dist/access-log/access-log.service.d.ts +47 -0
  14. package/dist/access-log/access-log.service.d.ts.map +1 -0
  15. package/dist/access-log/access-log.service.js +113 -0
  16. package/dist/access-log/access-log.service.js.map +1 -0
  17. package/dist/access-log/dto/query-access-log.dto.d.ts +8 -0
  18. package/dist/access-log/dto/query-access-log.dto.d.ts.map +1 -0
  19. package/dist/access-log/dto/query-access-log.dto.js +42 -0
  20. package/dist/access-log/dto/query-access-log.dto.js.map +1 -0
  21. package/dist/auth/auth.controller.d.ts +9 -9
  22. package/dist/auth/auth.service.d.ts +19 -19
  23. package/dist/auth/auth.service.d.ts.map +1 -1
  24. package/dist/auth/auth.service.js +0 -4
  25. package/dist/auth/auth.service.js.map +1 -1
  26. package/dist/challenge/challenge.service.d.ts +2 -2
  27. package/dist/challenge/challenge.service.d.ts.map +1 -1
  28. package/dist/challenge/challenge.service.js +0 -1
  29. package/dist/challenge/challenge.service.js.map +1 -1
  30. package/dist/core/system.module.d.ts.map +1 -1
  31. package/dist/core/system.module.js +1 -0
  32. package/dist/core/system.module.js.map +1 -1
  33. package/dist/core.module.d.ts.map +1 -1
  34. package/dist/core.module.js +12 -0
  35. package/dist/core.module.js.map +1 -1
  36. package/dist/dashboard/dashboard-user/dashboard-user.controller.d.ts +3 -3
  37. package/dist/dashboard/dashboard-user/dashboard-user.service.d.ts +3 -3
  38. package/dist/dashboard/dashboard.module.d.ts.map +1 -1
  39. package/dist/dashboard/dashboard.module.js +4 -1
  40. package/dist/dashboard/dashboard.module.js.map +1 -1
  41. package/dist/file/file.controller.js +0 -1
  42. package/dist/file/file.controller.js.map +1 -1
  43. package/dist/file/file.service.d.ts +65 -0
  44. package/dist/file/file.service.d.ts.map +1 -1
  45. package/dist/file/file.service.js +151 -3
  46. package/dist/file/file.service.js.map +1 -1
  47. package/dist/file/provider/abstract.provider.d.ts +8 -0
  48. package/dist/file/provider/abstract.provider.d.ts.map +1 -1
  49. package/dist/file/provider/abstract.provider.js.map +1 -1
  50. package/dist/file/provider/azure.provider.d.ts +4 -0
  51. package/dist/file/provider/azure.provider.d.ts.map +1 -1
  52. package/dist/file/provider/azure.provider.js +14 -0
  53. package/dist/file/provider/azure.provider.js.map +1 -1
  54. package/dist/file/provider/gcs.provider.d.ts +4 -0
  55. package/dist/file/provider/gcs.provider.d.ts.map +1 -1
  56. package/dist/file/provider/gcs.provider.js +14 -0
  57. package/dist/file/provider/gcs.provider.js.map +1 -1
  58. package/dist/file/provider/local.provider.d.ts +4 -0
  59. package/dist/file/provider/local.provider.d.ts.map +1 -1
  60. package/dist/file/provider/local.provider.js +19 -0
  61. package/dist/file/provider/local.provider.js.map +1 -1
  62. package/dist/file/provider/provider.factory.d.ts.map +1 -1
  63. package/dist/file/provider/provider.factory.js +3 -0
  64. package/dist/file/provider/provider.factory.js.map +1 -1
  65. package/dist/file/provider/s3.provider.d.ts +4 -0
  66. package/dist/file/provider/s3.provider.d.ts.map +1 -1
  67. package/dist/file/provider/s3.provider.js +17 -0
  68. package/dist/file/provider/s3.provider.js.map +1 -1
  69. package/dist/mcp/mcp-audit.service.d.ts +3 -1
  70. package/dist/mcp/mcp-audit.service.d.ts.map +1 -1
  71. package/dist/mcp/mcp-audit.service.js +14 -2
  72. package/dist/mcp/mcp-audit.service.js.map +1 -1
  73. package/dist/mcp/mcp.controller.d.ts +3 -1
  74. package/dist/mcp/mcp.controller.d.ts.map +1 -1
  75. package/dist/mcp/mcp.controller.js +10 -2
  76. package/dist/mcp/mcp.controller.js.map +1 -1
  77. package/dist/mcp/mcp.module.d.ts.map +1 -1
  78. package/dist/mcp/mcp.module.js +72 -0
  79. package/dist/mcp/mcp.module.js.map +1 -1
  80. package/dist/mcp/tools/core-ai.mcp-tools.d.ts +45 -0
  81. package/dist/mcp/tools/core-ai.mcp-tools.d.ts.map +1 -0
  82. package/dist/mcp/tools/core-ai.mcp-tools.js +214 -0
  83. package/dist/mcp/tools/core-ai.mcp-tools.js.map +1 -0
  84. package/dist/mcp/tools/core-dashboard-items.mcp-tools.d.ts +21 -0
  85. package/dist/mcp/tools/core-dashboard-items.mcp-tools.d.ts.map +1 -0
  86. package/dist/mcp/tools/core-dashboard-items.mcp-tools.js +89 -0
  87. package/dist/mcp/tools/core-dashboard-items.mcp-tools.js.map +1 -0
  88. package/dist/mcp/tools/core-dashboard.mcp-tools.d.ts +29 -0
  89. package/dist/mcp/tools/core-dashboard.mcp-tools.d.ts.map +1 -0
  90. package/dist/mcp/tools/core-dashboard.mcp-tools.js +149 -0
  91. package/dist/mcp/tools/core-dashboard.mcp-tools.js.map +1 -0
  92. package/dist/mcp/tools/core-event-webhooks.mcp-tools.d.ts +30 -0
  93. package/dist/mcp/tools/core-event-webhooks.mcp-tools.d.ts.map +1 -0
  94. package/dist/mcp/tools/core-event-webhooks.mcp-tools.js +185 -0
  95. package/dist/mcp/tools/core-event-webhooks.mcp-tools.js.map +1 -0
  96. package/dist/mcp/tools/core-files.mcp-tools.d.ts +26 -0
  97. package/dist/mcp/tools/core-files.mcp-tools.d.ts.map +1 -0
  98. package/dist/mcp/tools/core-files.mcp-tools.js +140 -0
  99. package/dist/mcp/tools/core-files.mcp-tools.js.map +1 -0
  100. package/dist/mcp/tools/core-integration-events.mcp-tools.d.ts +26 -0
  101. package/dist/mcp/tools/core-integration-events.mcp-tools.d.ts.map +1 -0
  102. package/dist/mcp/tools/core-integration-events.mcp-tools.js +168 -0
  103. package/dist/mcp/tools/core-integration-events.mcp-tools.js.map +1 -0
  104. package/dist/mcp/tools/core-mail-profile.mcp-tools.d.ts +32 -0
  105. package/dist/mcp/tools/core-mail-profile.mcp-tools.d.ts.map +1 -0
  106. package/dist/mcp/tools/core-mail-profile.mcp-tools.js +178 -0
  107. package/dist/mcp/tools/core-mail-profile.mcp-tools.js.map +1 -0
  108. package/dist/mcp/tools/core-mail-sent.mcp-tools.d.ts +14 -0
  109. package/dist/mcp/tools/core-mail-sent.mcp-tools.d.ts.map +1 -0
  110. package/dist/mcp/tools/core-mail-sent.mcp-tools.js +106 -0
  111. package/dist/mcp/tools/core-mail-sent.mcp-tools.js.map +1 -0
  112. package/dist/mcp/tools/core-mail.mcp-tools.d.ts +36 -0
  113. package/dist/mcp/tools/core-mail.mcp-tools.d.ts.map +1 -0
  114. package/dist/mcp/tools/core-mail.mcp-tools.js +184 -0
  115. package/dist/mcp/tools/core-mail.mcp-tools.js.map +1 -0
  116. package/dist/mcp/tools/core-mcp-chat.mcp-tools.d.ts +21 -0
  117. package/dist/mcp/tools/core-mcp-chat.mcp-tools.d.ts.map +1 -0
  118. package/dist/mcp/tools/core-mcp-chat.mcp-tools.js +118 -0
  119. package/dist/mcp/tools/core-mcp-chat.mcp-tools.js.map +1 -0
  120. package/dist/mcp/tools/core-menus.mcp-tools.d.ts +40 -0
  121. package/dist/mcp/tools/core-menus.mcp-tools.d.ts.map +1 -0
  122. package/dist/mcp/tools/core-menus.mcp-tools.js +268 -0
  123. package/dist/mcp/tools/core-menus.mcp-tools.js.map +1 -0
  124. package/dist/mcp/tools/core-notifications.mcp-tools.d.ts +31 -0
  125. package/dist/mcp/tools/core-notifications.mcp-tools.d.ts.map +1 -0
  126. package/dist/mcp/tools/core-notifications.mcp-tools.js +178 -0
  127. package/dist/mcp/tools/core-notifications.mcp-tools.js.map +1 -0
  128. package/dist/mcp/tools/core-profile.mcp-tools.d.ts +20 -0
  129. package/dist/mcp/tools/core-profile.mcp-tools.d.ts.map +1 -0
  130. package/dist/mcp/tools/core-profile.mcp-tools.js +111 -0
  131. package/dist/mcp/tools/core-profile.mcp-tools.js.map +1 -0
  132. package/dist/mcp/tools/core-roles.mcp-tools.d.ts +70 -0
  133. package/dist/mcp/tools/core-roles.mcp-tools.d.ts.map +1 -0
  134. package/dist/mcp/tools/core-roles.mcp-tools.js +355 -0
  135. package/dist/mcp/tools/core-roles.mcp-tools.js.map +1 -0
  136. package/dist/mcp/tools/core-routes.mcp-tools.d.ts +52 -0
  137. package/dist/mcp/tools/core-routes.mcp-tools.d.ts.map +1 -0
  138. package/dist/mcp/tools/core-routes.mcp-tools.js +254 -0
  139. package/dist/mcp/tools/core-routes.mcp-tools.js.map +1 -0
  140. package/dist/mcp/tools/core-screens.mcp-tools.d.ts +47 -0
  141. package/dist/mcp/tools/core-screens.mcp-tools.d.ts.map +1 -0
  142. package/dist/mcp/tools/core-screens.mcp-tools.js +250 -0
  143. package/dist/mcp/tools/core-screens.mcp-tools.js.map +1 -0
  144. package/dist/mcp/tools/core-sessions.mcp-tools.d.ts +20 -0
  145. package/dist/mcp/tools/core-sessions.mcp-tools.d.ts.map +1 -0
  146. package/dist/mcp/tools/core-sessions.mcp-tools.js +128 -0
  147. package/dist/mcp/tools/core-sessions.mcp-tools.js.map +1 -0
  148. package/dist/mcp/tools/core-settings.mcp-tools.d.ts +25 -0
  149. package/dist/mcp/tools/core-settings.mcp-tools.d.ts.map +1 -1
  150. package/dist/mcp/tools/core-settings.mcp-tools.js +138 -0
  151. package/dist/mcp/tools/core-settings.mcp-tools.js.map +1 -1
  152. package/dist/mcp/tools/core-system.mcp-tools.d.ts +8 -0
  153. package/dist/mcp/tools/core-system.mcp-tools.d.ts.map +1 -0
  154. package/dist/mcp/tools/core-system.mcp-tools.js +40 -0
  155. package/dist/mcp/tools/core-system.mcp-tools.js.map +1 -0
  156. package/dist/mcp/tools/core-webhooks.mcp-tools.d.ts +46 -0
  157. package/dist/mcp/tools/core-webhooks.mcp-tools.d.ts.map +1 -0
  158. package/dist/mcp/tools/core-webhooks.mcp-tools.js +266 -0
  159. package/dist/mcp/tools/core-webhooks.mcp-tools.js.map +1 -0
  160. package/dist/mcp-chat/mcp-chat.controller.d.ts +1 -1
  161. package/dist/mcp-chat/mcp-chat.service.d.ts +25 -2
  162. package/dist/mcp-chat/mcp-chat.service.d.ts.map +1 -1
  163. package/dist/mcp-chat/mcp-chat.service.js +666 -36
  164. package/dist/mcp-chat/mcp-chat.service.js.map +1 -1
  165. package/dist/notification/notification.controller.d.ts +8 -4
  166. package/dist/notification/notification.controller.d.ts.map +1 -1
  167. package/dist/notification/notification.controller.js +12 -0
  168. package/dist/notification/notification.controller.js.map +1 -1
  169. package/dist/notification/notification.service.d.ts +19 -5
  170. package/dist/notification/notification.service.d.ts.map +1 -1
  171. package/dist/notification/notification.service.js +155 -9
  172. package/dist/notification/notification.service.js.map +1 -1
  173. package/dist/pdf/pdf.service.js +1 -1
  174. package/dist/pdf/pdf.service.js.map +1 -1
  175. package/dist/profile/profile.controller.d.ts +3 -3
  176. package/dist/profile/profile.controller.js +0 -1
  177. package/dist/profile/profile.controller.js.map +1 -1
  178. package/dist/profile/profile.module.d.ts.map +1 -1
  179. package/dist/profile/profile.module.js +1 -0
  180. package/dist/profile/profile.module.js.map +1 -1
  181. package/dist/profile/profile.service.d.ts +3 -3
  182. package/dist/session/session.controller.d.ts +2 -2
  183. package/dist/session/session.service.d.ts +6 -6
  184. package/dist/setting/setting.controller.d.ts +1 -1
  185. package/dist/setting/setting.service.d.ts +1 -1
  186. package/dist/setting/setting.service.d.ts.map +1 -1
  187. package/dist/setting/setting.service.js +1 -4
  188. package/dist/setting/setting.service.js.map +1 -1
  189. package/dist/task/task.service.d.ts +1 -0
  190. package/dist/task/task.service.d.ts.map +1 -1
  191. package/dist/task/task.service.js +25 -0
  192. package/dist/task/task.service.js.map +1 -1
  193. package/dist/token/token.service.d.ts.map +1 -1
  194. package/dist/token/token.service.js +0 -1
  195. package/dist/token/token.service.js.map +1 -1
  196. package/dist/user/user.controller.d.ts +9 -9
  197. package/dist/user/user.service.d.ts +21 -21
  198. package/dist/webhook-integration/dto/webhook-action.dto.d.ts +22 -0
  199. package/dist/webhook-integration/dto/webhook-action.dto.d.ts.map +1 -0
  200. package/dist/webhook-integration/dto/webhook-action.dto.js +120 -0
  201. package/dist/webhook-integration/dto/webhook-action.dto.js.map +1 -0
  202. package/dist/webhook-integration/webhook-integration.controller.d.ts +7 -5
  203. package/dist/webhook-integration/webhook-integration.controller.d.ts.map +1 -1
  204. package/dist/webhook-integration/webhook-integration.controller.js +18 -5
  205. package/dist/webhook-integration/webhook-integration.controller.js.map +1 -1
  206. package/dist/webhook-integration/webhook-integration.service.d.ts +14 -5
  207. package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
  208. package/dist/webhook-integration/webhook-integration.service.js +174 -41
  209. package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
  210. package/hedhog/data/menu.yaml +15 -0
  211. package/hedhog/data/route.yaml +1304 -0
  212. package/hedhog/data/setting_group.yaml +67 -0
  213. package/hedhog/data/setting_subgroup.yaml +16 -0
  214. package/hedhog/frontend/app/access-log/page.tsx.ejs +772 -0
  215. package/hedhog/frontend/app/ai_agent/page.tsx.ejs +10 -3
  216. package/hedhog/frontend/app/dashboard/dashboard-home-tabs.tsx.ejs +33 -13
  217. package/hedhog/frontend/app/dashboard/management/tabs/components-tab.tsx.ejs +12 -5
  218. package/hedhog/frontend/app/dashboard/management/tabs/dashboards-tab.tsx.ejs +12 -5
  219. package/hedhog/frontend/app/integrations-webhooks/page.tsx.ejs +1817 -356
  220. package/hedhog/frontend/app/mcp_chat/chat-components.tsx.ejs +43 -5
  221. package/hedhog/frontend/app/mcp_chat/page.tsx.ejs +35 -7
  222. package/hedhog/frontend/app/menu/page.tsx.ejs +9 -3
  223. package/hedhog/frontend/app/roles/page.tsx.ejs +18 -6
  224. package/hedhog/frontend/app/routes/page.tsx.ejs +9 -3
  225. package/hedhog/frontend/app/setting-list/page.tsx.ejs +344 -171
  226. package/hedhog/frontend/app/users/user-access-sheet.tsx.ejs +310 -304
  227. package/hedhog/frontend/app/users/user-sheet.tsx.ejs +25 -7
  228. package/hedhog/frontend/messages/en.json +104 -3
  229. package/hedhog/frontend/messages/pt.json +104 -3
  230. package/hedhog/table/access_log.yaml +31 -0
  231. package/package.json +4 -3
  232. package/src/access-log/access-log.controller.ts +29 -0
  233. package/src/access-log/access-log.interceptor.ts +38 -0
  234. package/src/access-log/access-log.module.ts +16 -0
  235. package/src/access-log/access-log.service.ts +129 -0
  236. package/src/access-log/dto/query-access-log.dto.ts +23 -0
  237. package/src/auth/auth.service.ts +0 -7
  238. package/src/challenge/challenge.service.ts +0 -2
  239. package/src/core/system.module.ts +1 -0
  240. package/src/core.module.ts +12 -0
  241. package/src/dashboard/dashboard.module.ts +4 -1
  242. package/src/file/file.controller.ts +14 -14
  243. package/src/file/file.service.ts +198 -4
  244. package/src/file/provider/abstract.provider.ts +10 -0
  245. package/src/file/provider/azure.provider.ts +24 -0
  246. package/src/file/provider/gcs.provider.ts +20 -0
  247. package/src/file/provider/local.provider.ts +30 -2
  248. package/src/file/provider/provider.factory.ts +3 -0
  249. package/src/file/provider/s3.provider.ts +22 -0
  250. package/src/mcp/mcp-audit.service.ts +47 -36
  251. package/src/mcp/mcp.controller.ts +40 -30
  252. package/src/mcp/mcp.module.ts +72 -0
  253. package/src/mcp/tools/core-ai.mcp-tools.ts +156 -0
  254. package/src/mcp/tools/core-dashboard-items.mcp-tools.ts +63 -0
  255. package/src/mcp/tools/core-dashboard.mcp-tools.ts +103 -0
  256. package/src/mcp/tools/core-event-webhooks.mcp-tools.ts +134 -0
  257. package/src/mcp/tools/core-files.mcp-tools.ts +105 -0
  258. package/src/mcp/tools/core-integration-events.mcp-tools.ts +118 -0
  259. package/src/mcp/tools/core-mail-profile.mcp-tools.ts +128 -0
  260. package/src/mcp/tools/core-mail-sent.mcp-tools.ts +76 -0
  261. package/src/mcp/tools/core-mail.mcp-tools.ts +130 -0
  262. package/src/mcp/tools/core-mcp-chat.mcp-tools.ts +81 -0
  263. package/src/mcp/tools/core-menus.mcp-tools.ts +197 -0
  264. package/src/mcp/tools/core-notifications.mcp-tools.ts +128 -0
  265. package/src/mcp/tools/core-profile.mcp-tools.ts +74 -0
  266. package/src/mcp/tools/core-roles.mcp-tools.ts +288 -0
  267. package/src/mcp/tools/core-routes.mcp-tools.ts +196 -0
  268. package/src/mcp/tools/core-screens.mcp-tools.ts +186 -0
  269. package/src/mcp/tools/core-sessions.mcp-tools.ts +97 -0
  270. package/src/mcp/tools/core-settings.mcp-tools.ts +171 -53
  271. package/src/mcp/tools/core-system.mcp-tools.ts +19 -0
  272. package/src/mcp/tools/core-webhooks.mcp-tools.ts +199 -0
  273. package/src/mcp-chat/mcp-chat.service.ts +887 -38
  274. package/src/notification/notification.controller.ts +20 -8
  275. package/src/notification/notification.service.ts +209 -9
  276. package/src/pdf/pdf.service.ts +1 -1
  277. package/src/profile/profile.controller.ts +1 -1
  278. package/src/profile/profile.module.ts +1 -0
  279. package/src/setting/setting.service.ts +8 -11
  280. package/src/task/task.service.ts +31 -0
  281. package/src/token/token.service.ts +0 -2
  282. package/src/webhook-integration/dto/webhook-action.dto.ts +96 -0
  283. package/src/webhook-integration/webhook-integration.controller.ts +124 -103
  284. package/src/webhook-integration/webhook-integration.service.ts +267 -64
@@ -11,7 +11,7 @@ import {
11
11
  NotFoundException,
12
12
  } from '@nestjs/common';
13
13
  import axios from 'axios';
14
- import { McpRegistryService } from '../mcp/mcp-registry.service';
14
+ import { McpRegistryService, McpToolEntry } from '../mcp/mcp-registry.service';
15
15
  import { McpContext } from '../mcp/types/mcp-context.type';
16
16
  import { SettingService } from '../setting/setting.service';
17
17
  import { CreateConversationDTO } from './dto/create-conversation.dto';
@@ -37,16 +37,52 @@ interface OpenAiToolDefinition {
37
37
  };
38
38
  }
39
39
 
40
+ interface GeminiFunctionDeclaration {
41
+ name: string;
42
+ description: string;
43
+ parameters: Record<string, any>;
44
+ }
45
+
46
+ interface GeminiPart {
47
+ text?: string;
48
+ functionCall?: { name: string; args: Record<string, any> };
49
+ functionResponse?: { name: string; response: { output: string } };
50
+ }
51
+
52
+ interface GeminiContent {
53
+ role: 'user' | 'model';
54
+ parts: GeminiPart[];
55
+ }
56
+
40
57
  interface ScoredTool {
41
58
  name: string;
59
+ domain: string;
42
60
  readOnly: boolean;
43
61
  priorityBucket: number;
44
62
  keywordBoost: number;
63
+ matchScore: number;
45
64
  index: number;
46
65
  tool: OpenAiToolDefinition;
47
66
  }
48
67
 
68
+ interface ToolSelectionResult<TTool> {
69
+ tools: TTool[];
70
+ clarification?: string;
71
+ }
72
+
73
+ type ConversationHistoryItem = {
74
+ role: string;
75
+ content: string;
76
+ tool_name: string | null;
77
+ tool_call_id: string | null;
78
+ };
79
+
49
80
  const MAX_OPENAI_TOOLS = 128;
81
+ const TARGET_SELECTED_TOOLS = 112;
82
+ const MAX_ITERATIONS = 20;
83
+ const FORCE_FINISH_THRESHOLD = 2;
84
+ const MIN_DOMAIN_CONFIDENCE = 2;
85
+ const AMBIGUOUS_DOMAIN_DELTA = 1;
50
86
 
51
87
  const SYSTEM_PROMPT =
52
88
  'You are a helpful assistant with access to tools from the HedHog system. ' +
@@ -56,6 +92,144 @@ const SYSTEM_PROMPT =
56
92
  const sanitize = (n: string) => n.replace(/\./g, '__');
57
93
  const restore = (n: string) => n.replace(/__/g, '.');
58
94
 
95
+ const DOMAIN_KEYWORDS: Record<string, string[]> = {
96
+ finance: [
97
+ 'finance',
98
+ 'financeiro',
99
+ 'financeira',
100
+ 'financas',
101
+ 'finanças',
102
+ 'banco',
103
+ 'bancos',
104
+ 'bank',
105
+ 'banks',
106
+ 'conta',
107
+ 'contas',
108
+ 'account',
109
+ 'accounts',
110
+ 'conta bancaria',
111
+ 'contas bancarias',
112
+ 'conta bancária',
113
+ 'contas bancárias',
114
+ 'extrato',
115
+ 'extratos',
116
+ 'statement',
117
+ 'statements',
118
+ 'pagamento',
119
+ 'pagamentos',
120
+ 'payment',
121
+ 'payments',
122
+ 'recebimento',
123
+ 'recebimentos',
124
+ 'receivable',
125
+ 'payable',
126
+ 'currency',
127
+ 'moeda',
128
+ 'transferencia',
129
+ 'transferência',
130
+ ],
131
+ operations: [
132
+ 'operations',
133
+ 'operacoes',
134
+ 'operações',
135
+ 'tarefa',
136
+ 'tarefas',
137
+ 'task',
138
+ 'tasks',
139
+ 'projeto',
140
+ 'projetos',
141
+ 'project',
142
+ 'projects',
143
+ 'timesheet',
144
+ 'apontamento',
145
+ 'aprovacao',
146
+ 'aprovação',
147
+ 'approval',
148
+ 'colaborador',
149
+ 'collaborator',
150
+ ],
151
+ lms: [
152
+ 'lms',
153
+ 'curso',
154
+ 'cursos',
155
+ 'course',
156
+ 'courses',
157
+ 'aula',
158
+ 'aulas',
159
+ 'lesson',
160
+ 'lessons',
161
+ 'treinamento',
162
+ 'training',
163
+ 'turma',
164
+ 'class',
165
+ 'aluno',
166
+ 'student',
167
+ 'prova',
168
+ 'exam',
169
+ ],
170
+ core: [
171
+ 'core',
172
+ 'usuario',
173
+ 'usuário',
174
+ 'usuarios',
175
+ 'usuários',
176
+ 'user',
177
+ 'users',
178
+ 'perfil',
179
+ 'profile',
180
+ 'role',
181
+ 'roles',
182
+ 'menu',
183
+ 'menus',
184
+ 'rota',
185
+ 'rotas',
186
+ 'route',
187
+ 'routes',
188
+ 'configuracao',
189
+ 'configuração',
190
+ 'setting',
191
+ 'settings',
192
+ ],
193
+ contact: [
194
+ 'contato',
195
+ 'contatos',
196
+ 'contact',
197
+ 'contacts',
198
+ 'pessoa',
199
+ 'pessoas',
200
+ 'person',
201
+ 'proposal',
202
+ 'proposta',
203
+ ],
204
+ };
205
+
206
+ const ACTION_KEYWORDS = [
207
+ 'listar',
208
+ 'lista',
209
+ 'buscar',
210
+ 'procurar',
211
+ 'consultar',
212
+ 'mostrar',
213
+ 'ver',
214
+ 'criar',
215
+ 'adicionar',
216
+ 'atualizar',
217
+ 'alterar',
218
+ 'remover',
219
+ 'deletar',
220
+ 'excluir',
221
+ 'list',
222
+ 'search',
223
+ 'find',
224
+ 'show',
225
+ 'get',
226
+ 'create',
227
+ 'add',
228
+ 'update',
229
+ 'delete',
230
+ 'remove',
231
+ ];
232
+
59
233
  @Injectable()
60
234
  export class McpChatService {
61
235
  private readonly logger = new Logger(McpChatService.name);
@@ -107,9 +281,14 @@ export class McpChatService {
107
281
  locale: string,
108
282
  message: string,
109
283
  ) {
284
+ const cfg = await this.setting.getSettingValues(['mcp-enabled']);
285
+ if (cfg['mcp-enabled'] === false) {
286
+ throw new BadRequestException('MCP server is disabled.');
287
+ }
288
+
110
289
  await this.assertOwnership(conversationId, userId);
111
290
 
112
- const apiKey = await this.getOpenAiKey();
291
+ const { provider, apiKey } = await this.getProviderKey();
113
292
 
114
293
  // Save user message
115
294
  await this.prisma.mcp_conversation_message.create({
@@ -131,7 +310,21 @@ export class McpChatService {
131
310
  });
132
311
  }
133
312
 
134
- // Build OpenAI messages from history
313
+ if (provider === 'gemini') {
314
+ return this.runGeminiLoop(conversationId, userId, locale, message, apiKey);
315
+ }
316
+ return this.runOpenAiLoop(conversationId, userId, locale, message, apiKey);
317
+ }
318
+
319
+ // ── OpenAI loop ──────────────────────────────────────────────────────────
320
+
321
+ private async runOpenAiLoop(
322
+ conversationId: number,
323
+ userId: number,
324
+ locale: string,
325
+ message: string,
326
+ apiKey: string,
327
+ ) {
135
328
  const history = await this.prisma.mcp_conversation_message.findMany({
136
329
  where: { conversation_id: conversationId },
137
330
  orderBy: { created_at: 'asc' },
@@ -142,12 +335,24 @@ export class McpChatService {
142
335
  ...this.toOpenAiMessages(history),
143
336
  ];
144
337
 
145
- // Build tool definitions
146
- const tools = this.buildOpenAiTools(message, conversationId, userId);
338
+ const toolSelection = await this.buildOpenAiTools(
339
+ message,
340
+ conversationId,
341
+ userId,
342
+ history,
343
+ );
344
+ if (toolSelection.clarification) {
345
+ return this.recordAssistantAndReturnHistory(
346
+ conversationId,
347
+ toolSelection.clarification,
348
+ );
349
+ }
350
+ const tools = toolSelection.tools;
147
351
 
148
- // Tool-calling loop
149
- let maxIterations = 10;
352
+ const callSignatures = new Set<string>();
353
+ let maxIterations = MAX_ITERATIONS;
150
354
  while (maxIterations-- > 0) {
355
+ const forceFinish = maxIterations < FORCE_FINISH_THRESHOLD;
151
356
  let response: any;
152
357
  try {
153
358
  response = await axios.post(
@@ -155,8 +360,8 @@ export class McpChatService {
155
360
  {
156
361
  model: 'gpt-4o-mini',
157
362
  messages: openAiMessages,
158
- tools: tools.length > 0 ? tools : undefined,
159
- tool_choice: tools.length > 0 ? 'auto' : undefined,
363
+ tools: !forceFinish && tools.length > 0 ? tools : undefined,
364
+ tool_choice: !forceFinish && tools.length > 0 ? 'auto' : undefined,
160
365
  },
161
366
  {
162
367
  headers: {
@@ -186,15 +391,13 @@ export class McpChatService {
186
391
  const choice = response.data?.choices?.[0];
187
392
  const assistantMsg = choice?.message;
188
393
 
189
- if (assistantMsg?.tool_calls?.length) {
190
- // Add assistant tool-call message to context
394
+ if (assistantMsg?.tool_calls?.length && !forceFinish) {
191
395
  openAiMessages.push({
192
396
  role: 'assistant',
193
397
  content: null,
194
398
  tool_calls: assistantMsg.tool_calls,
195
399
  });
196
400
 
197
- // Execute each tool call
198
401
  for (const toolCall of assistantMsg.tool_calls as OpenAiToolCall[]) {
199
402
  const originalName = restore(toolCall.function.name);
200
403
  const argsJson = toolCall.function.arguments;
@@ -205,7 +408,6 @@ export class McpChatService {
205
408
  args = {};
206
409
  }
207
410
 
208
- // Persist tool_call message
209
411
  await this.prisma.mcp_conversation_message.create({
210
412
  data: {
211
413
  conversation_id: conversationId,
@@ -216,7 +418,31 @@ export class McpChatService {
216
418
  },
217
419
  });
218
420
 
219
- // Execute the tool
421
+ const sig = `${toolCall.function.name}:${argsJson}`;
422
+ if (callSignatures.has(sig)) {
423
+ this.logger.warn(
424
+ `MCP chat duplicate tool call detected — aborting re-execution. ` +
425
+ `conversationId=${conversationId} tool=${originalName}`,
426
+ );
427
+ const dupResult = JSON.stringify({ error: 'Duplicate tool call detected, aborting loop.' });
428
+ await this.prisma.mcp_conversation_message.create({
429
+ data: {
430
+ conversation_id: conversationId,
431
+ role: 'tool_result',
432
+ content: dupResult,
433
+ tool_name: originalName,
434
+ tool_call_id: toolCall.id,
435
+ },
436
+ });
437
+ openAiMessages.push({
438
+ role: 'tool',
439
+ tool_call_id: toolCall.id,
440
+ content: dupResult,
441
+ });
442
+ continue;
443
+ }
444
+ callSignatures.add(sig);
445
+
220
446
  let resultContent: string;
221
447
  const entry = this.registry.get(originalName);
222
448
  if (entry) {
@@ -236,7 +462,6 @@ export class McpChatService {
236
462
  resultContent = JSON.stringify({ error: `Tool '${originalName}' not found` });
237
463
  }
238
464
 
239
- // Persist tool_result message
240
465
  await this.prisma.mcp_conversation_message.create({
241
466
  data: {
242
467
  conversation_id: conversationId,
@@ -247,7 +472,6 @@ export class McpChatService {
247
472
  },
248
473
  });
249
474
 
250
- // Add tool result to context for next iteration
251
475
  openAiMessages.push({
252
476
  role: 'tool',
253
477
  tool_call_id: toolCall.id,
@@ -255,10 +479,9 @@ export class McpChatService {
255
479
  });
256
480
  }
257
481
 
258
- continue; // next iteration with tool results
482
+ continue;
259
483
  }
260
484
 
261
- // Final assistant response
262
485
  const finalContent: string = assistantMsg?.content || '';
263
486
  await this.prisma.mcp_conversation_message.create({
264
487
  data: {
@@ -277,6 +500,165 @@ export class McpChatService {
277
500
  throw new BadRequestException('Maximum tool call iterations exceeded.');
278
501
  }
279
502
 
503
+ // ── Gemini loop ──────────────────────────────────────────────────────────
504
+
505
+ private async runGeminiLoop(
506
+ conversationId: number,
507
+ userId: number,
508
+ locale: string,
509
+ message: string,
510
+ apiKey: string,
511
+ ) {
512
+ const history = await this.prisma.mcp_conversation_message.findMany({
513
+ where: { conversation_id: conversationId },
514
+ orderBy: { created_at: 'asc' },
515
+ });
516
+
517
+ const geminiContents: GeminiContent[] = this.toGeminiContents(history);
518
+ const toolSelection = await this.buildGeminiTools(
519
+ message,
520
+ conversationId,
521
+ userId,
522
+ history,
523
+ );
524
+ if (toolSelection.clarification) {
525
+ return this.recordAssistantAndReturnHistory(
526
+ conversationId,
527
+ toolSelection.clarification,
528
+ );
529
+ }
530
+ const functionDeclarations = toolSelection.tools;
531
+
532
+ let maxIterations = MAX_ITERATIONS;
533
+ let callIndex = 0;
534
+
535
+ while (maxIterations-- > 0) {
536
+ const forceFinish = maxIterations < FORCE_FINISH_THRESHOLD;
537
+ let response: any;
538
+ try {
539
+ response = await axios.post(
540
+ `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`,
541
+ {
542
+ systemInstruction: { parts: [{ text: SYSTEM_PROMPT }] },
543
+ contents: geminiContents,
544
+ tools:
545
+ !forceFinish && functionDeclarations.length > 0
546
+ ? [{ functionDeclarations }]
547
+ : undefined,
548
+ },
549
+ { headers: { 'Content-Type': 'application/json' } },
550
+ );
551
+ } catch (err: any) {
552
+ const status = err?.response?.status;
553
+ const errData = err?.response?.data;
554
+ if (status === 429) {
555
+ throw new HttpException(
556
+ errData?.error?.message ?? 'Gemini rate limit exceeded. Please try again later.',
557
+ HttpStatus.TOO_MANY_REQUESTS,
558
+ );
559
+ }
560
+ if (status === 400) {
561
+ throw new BadRequestException(
562
+ errData?.error?.message ?? 'Invalid Gemini API request.',
563
+ );
564
+ }
565
+ if (status === 401 || status === 403) {
566
+ throw new BadRequestException('Invalid Gemini API key.');
567
+ }
568
+ throw new HttpException(
569
+ errData?.error?.message ?? 'Gemini API error.',
570
+ status ?? HttpStatus.BAD_GATEWAY,
571
+ );
572
+ }
573
+
574
+ const candidate = response.data?.candidates?.[0];
575
+ const parts: GeminiPart[] = candidate?.content?.parts ?? [];
576
+
577
+ const functionCallParts = parts.filter((p) => p.functionCall);
578
+ const textParts = parts.filter((p) => p.text);
579
+
580
+ if (functionCallParts.length > 0 && !forceFinish) {
581
+ geminiContents.push({ role: 'model', parts: functionCallParts });
582
+
583
+ const functionResponseParts: GeminiPart[] = [];
584
+
585
+ for (const part of functionCallParts) {
586
+ const { name, args } = part.functionCall!;
587
+ const originalName = restore(name);
588
+ const argsJson = JSON.stringify(args ?? {});
589
+ const toolCallId = `gemini-${callIndex++}`;
590
+
591
+ await this.prisma.mcp_conversation_message.create({
592
+ data: {
593
+ conversation_id: conversationId,
594
+ role: 'tool_call',
595
+ content: argsJson,
596
+ tool_name: originalName,
597
+ tool_call_id: toolCallId,
598
+ },
599
+ });
600
+
601
+ let resultContent: string;
602
+ const entry = this.registry.get(originalName);
603
+ if (entry) {
604
+ const context: McpContext = {
605
+ userId,
606
+ sessionId: '',
607
+ locale,
608
+ toolName: originalName,
609
+ };
610
+ try {
611
+ const result = await entry.handler(args ?? {}, context);
612
+ resultContent = JSON.stringify(result);
613
+ } catch (e: any) {
614
+ resultContent = JSON.stringify({ error: e.message });
615
+ }
616
+ } else {
617
+ resultContent = JSON.stringify({ error: `Tool '${originalName}' not found` });
618
+ }
619
+
620
+ await this.prisma.mcp_conversation_message.create({
621
+ data: {
622
+ conversation_id: conversationId,
623
+ role: 'tool_result',
624
+ content: resultContent,
625
+ tool_name: originalName,
626
+ tool_call_id: toolCallId,
627
+ },
628
+ });
629
+
630
+ functionResponseParts.push({
631
+ functionResponse: {
632
+ name,
633
+ response: { output: resultContent },
634
+ },
635
+ });
636
+ }
637
+
638
+ geminiContents.push({ role: 'user', parts: functionResponseParts });
639
+ continue;
640
+ }
641
+
642
+ const finalContent = textParts.map((p) => p.text).join('');
643
+ await this.prisma.mcp_conversation_message.create({
644
+ data: {
645
+ conversation_id: conversationId,
646
+ role: 'assistant',
647
+ content: finalContent,
648
+ },
649
+ });
650
+
651
+ return this.prisma.mcp_conversation_message.findMany({
652
+ where: { conversation_id: conversationId },
653
+ orderBy: { created_at: 'asc' },
654
+ });
655
+ }
656
+
657
+ throw new BadRequestException('Maximum tool call iterations exceeded.');
658
+ }
659
+
660
+ // ── Helpers ──────────────────────────────────────────────────────────────
661
+
280
662
  private toOpenAiMessages(
281
663
  history: Array<{
282
664
  role: string;
@@ -319,6 +701,63 @@ export class McpChatService {
319
701
  return messages;
320
702
  }
321
703
 
704
+ private toGeminiContents(
705
+ history: Array<{
706
+ role: string;
707
+ content: string;
708
+ tool_name: string | null;
709
+ tool_call_id: string | null;
710
+ }>,
711
+ ): GeminiContent[] {
712
+ const contents: GeminiContent[] = [];
713
+ let i = 0;
714
+
715
+ while (i < history.length) {
716
+ const msg = history[i];
717
+
718
+ if (msg.role === 'user') {
719
+ contents.push({ role: 'user', parts: [{ text: msg.content }] });
720
+ i++;
721
+ } else if (msg.role === 'assistant') {
722
+ contents.push({ role: 'model', parts: [{ text: msg.content }] });
723
+ i++;
724
+ } else if (msg.role === 'tool_call') {
725
+ const functionCallParts: GeminiPart[] = [];
726
+ while (i < history.length && history[i].role === 'tool_call') {
727
+ const m = history[i];
728
+ let args: Record<string, any> = {};
729
+ try {
730
+ args = JSON.parse(m.content);
731
+ } catch {
732
+ args = {};
733
+ }
734
+ functionCallParts.push({
735
+ functionCall: { name: sanitize(m.tool_name ?? ''), args },
736
+ });
737
+ i++;
738
+ }
739
+ contents.push({ role: 'model', parts: functionCallParts });
740
+ } else if (msg.role === 'tool_result') {
741
+ const functionResponseParts: GeminiPart[] = [];
742
+ while (i < history.length && history[i].role === 'tool_result') {
743
+ const m = history[i];
744
+ functionResponseParts.push({
745
+ functionResponse: {
746
+ name: sanitize(m.tool_name ?? ''),
747
+ response: { output: m.content },
748
+ },
749
+ });
750
+ i++;
751
+ }
752
+ contents.push({ role: 'user', parts: functionResponseParts });
753
+ } else {
754
+ i++;
755
+ }
756
+ }
757
+
758
+ return contents;
759
+ }
760
+
322
761
  private async assertOwnership(conversationId: number, userId: number) {
323
762
  const conv = await this.prisma.mcp_conversation.findUnique({
324
763
  where: { id: conversationId },
@@ -332,32 +771,115 @@ export class McpChatService {
332
771
  }
333
772
  }
334
773
 
335
- private async getOpenAiKey(): Promise<string> {
336
- const settings = await this.setting.getSettingValues(['ai-openai-api-key']);
337
- const key = settings['ai-openai-api-key'];
774
+ private async getProviderKey(): Promise<{ provider: 'openai' | 'gemini'; apiKey: string }> {
775
+ const settings = await this.setting.getSettingValues([
776
+ 'mcp-ai-provider',
777
+ 'ai-openai-api-key',
778
+ 'ai-gemini-api-key',
779
+ ]);
780
+ const provider = ((settings['mcp-ai-provider'] as string) || 'openai') as 'openai' | 'gemini';
781
+
782
+ if (provider === 'gemini') {
783
+ const key = settings['ai-gemini-api-key'] as string;
784
+ if (!key) {
785
+ throw new BadRequestException(
786
+ 'Gemini API key is not configured. Please set it in Settings → AI → Gemini API Key.',
787
+ );
788
+ }
789
+ return { provider: 'gemini', apiKey: key };
790
+ }
791
+
792
+ const key = settings['ai-openai-api-key'] as string;
338
793
  if (!key) {
339
794
  throw new BadRequestException(
340
- 'OpenAI API key is not configured (setting "ai-openai-api-key").',
795
+ 'OpenAI API key is not configured. Please set it in Settings → AI → OpenAI API Key.',
341
796
  );
342
797
  }
343
- return key;
798
+ return { provider: 'openai', apiKey: key };
799
+ }
800
+
801
+ private async buildOpenAiTools(
802
+ message: string,
803
+ conversationId: number,
804
+ userId: number,
805
+ history: ConversationHistoryItem[],
806
+ ): Promise<ToolSelectionResult<OpenAiToolDefinition>> {
807
+ return this.selectOpenAiTools(message, conversationId, userId, history);
808
+ }
809
+
810
+ private async buildGeminiTools(
811
+ message: string,
812
+ conversationId: number,
813
+ userId: number,
814
+ history: ConversationHistoryItem[],
815
+ ): Promise<ToolSelectionResult<GeminiFunctionDeclaration>> {
816
+ const selection = await this.selectOpenAiTools(
817
+ message,
818
+ conversationId,
819
+ userId,
820
+ history,
821
+ );
822
+
823
+ return {
824
+ clarification: selection.clarification,
825
+ tools: selection.tools.map((t) => t.function),
826
+ };
344
827
  }
345
828
 
346
- private buildOpenAiTools(
829
+ private async selectOpenAiTools(
347
830
  message: string,
348
831
  conversationId: number,
349
832
  userId: number,
350
- ): OpenAiToolDefinition[] {
833
+ history: ConversationHistoryItem[],
834
+ ): Promise<ToolSelectionResult<OpenAiToolDefinition>> {
351
835
  const allEntries = this.registry.getAll();
836
+ const allowedToolNames = await this.getAllowedMcpToolNames(userId);
837
+ const allowedEntries = allEntries.filter((entry) =>
838
+ allowedToolNames.has(entry.definition.name),
839
+ );
352
840
 
353
841
  const dedupedBySanitized = new Map<string, ScoredTool>();
354
842
  const excludedDuplicates: string[] = [];
355
- const normalizedMessage = message.toLowerCase();
843
+ const routerText = this.buildToolRouterText(message, history);
844
+ const normalizedMessage = this.normalizeText(routerText);
845
+ const messageTerms = this.getMessageKeywordSet(normalizedMessage);
846
+ const domainScores = this.scoreToolDomains(normalizedMessage, allowedEntries);
847
+ const candidateDomains = this.getCandidateDomains(domainScores);
848
+ const clarification = this.getToolSelectionClarification(
849
+ message,
850
+ normalizedMessage,
851
+ domainScores,
852
+ candidateDomains,
853
+ allowedEntries,
854
+ );
356
855
 
357
- for (const [index, entry] of allEntries.entries()) {
856
+ if (clarification) {
857
+ this.logger.log(
858
+ `MCP tools need clarification: conversationId=${conversationId} userId=${userId} ` +
859
+ `registered=${allEntries.length} authorized=${allowedEntries.length} ` +
860
+ `domains=${this.formatDomainScores(domainScores)} selected=0`,
861
+ );
862
+ return { tools: [], clarification };
863
+ }
864
+
865
+ const entriesForRanking =
866
+ candidateDomains.length > 0
867
+ ? allowedEntries.filter((entry) =>
868
+ candidateDomains.includes(this.getToolDomain(entry.definition.name)),
869
+ )
870
+ : allowedEntries;
871
+
872
+ for (const [index, entry] of entriesForRanking.entries()) {
358
873
  const originalName = entry.definition.name;
359
874
  const sanitizedName = sanitize(originalName);
360
875
  const readOnly = Boolean(entry.definition.readOnly);
876
+ const domain = this.getToolDomain(originalName);
877
+ const matchScore = this.scoreToolMatch(
878
+ normalizedMessage,
879
+ messageTerms,
880
+ entry,
881
+ candidateDomains,
882
+ );
361
883
 
362
884
  if (dedupedBySanitized.has(sanitizedName)) {
363
885
  excludedDuplicates.push(originalName);
@@ -366,9 +888,11 @@ export class McpChatService {
366
888
 
367
889
  const scored: ScoredTool = {
368
890
  name: originalName,
891
+ domain,
369
892
  readOnly,
370
- priorityBucket: this.getDomainPriorityBucket(originalName),
893
+ priorityBucket: this.getDomainPriorityBucket(originalName, candidateDomains),
371
894
  keywordBoost: this.getKeywordBoost(normalizedMessage, originalName),
895
+ matchScore,
372
896
  index,
373
897
  tool: {
374
898
  type: 'function',
@@ -388,6 +912,9 @@ export class McpChatService {
388
912
 
389
913
  const scoredTools = Array.from(dedupedBySanitized.values());
390
914
  scoredTools.sort((a, b) => {
915
+ if (a.matchScore !== b.matchScore) {
916
+ return b.matchScore - a.matchScore;
917
+ }
391
918
  if (a.priorityBucket !== b.priorityBucket) {
392
919
  return a.priorityBucket - b.priorityBucket;
393
920
  }
@@ -400,9 +927,9 @@ export class McpChatService {
400
927
  return a.index - b.index;
401
928
  });
402
929
 
403
- const selected = scoredTools.slice(0, MAX_OPENAI_TOOLS);
930
+ const selected = scoredTools.slice(0, TARGET_SELECTED_TOOLS);
404
931
  const excludedByLimit = scoredTools
405
- .slice(MAX_OPENAI_TOOLS)
932
+ .slice(TARGET_SELECTED_TOOLS)
406
933
  .map((tool) => tool.name);
407
934
 
408
935
  if (excludedDuplicates.length > 0) {
@@ -414,7 +941,7 @@ export class McpChatService {
414
941
 
415
942
  if (excludedByLimit.length > 0) {
416
943
  this.logger.warn(
417
- `MCP tools truncated for OpenAI limit (${MAX_OPENAI_TOOLS}). ` +
944
+ `MCP tools truncated for provider budget (${TARGET_SELECTED_TOOLS}/${MAX_OPENAI_TOOLS}). ` +
418
945
  `conversationId=${conversationId} userId=${userId} ` +
419
946
  `available=${scoredTools.length} selected=${selected.length} excluded=${excludedByLimit.length}. ` +
420
947
  `Top excluded: ${excludedByLimit.slice(0, 12).join(', ')}`,
@@ -423,17 +950,50 @@ export class McpChatService {
423
950
 
424
951
  this.logger.log(
425
952
  `MCP tools prepared: conversationId=${conversationId} userId=${userId} ` +
426
- `raw=${allEntries.length} deduped=${scoredTools.length} selected=${selected.length}`,
953
+ `registered=${allEntries.length} authorized=${allowedEntries.length} ` +
954
+ `domains=${this.formatDomainScores(domainScores)} ranked=${scoredTools.length} ` +
955
+ `selected=${selected.length}`,
427
956
  );
428
957
 
429
- return selected.map((entry) => entry.tool);
958
+ return { tools: selected.map((entry) => entry.tool) };
430
959
  }
431
960
 
432
- private getDomainPriorityBucket(toolName: string): number {
433
- if (toolName.startsWith('operations.')) return 0;
434
- if (toolName.startsWith('lms.')) return 1;
435
- if (toolName.startsWith('core.')) return 2;
436
- return 3;
961
+ private async getAllowedMcpToolNames(userId: number): Promise<Set<string>> {
962
+ const routes = await this.prisma.route.findMany({
963
+ where: {
964
+ type: 'MCP' as any,
965
+ tool_name: { not: null },
966
+ role_route: {
967
+ some: {
968
+ role: {
969
+ role_user: {
970
+ some: { user_id: userId },
971
+ },
972
+ },
973
+ },
974
+ },
975
+ },
976
+ select: { tool_name: true },
977
+ });
978
+
979
+ return new Set(
980
+ routes
981
+ .map((route) => route.tool_name)
982
+ .filter((toolName): toolName is string => typeof toolName === 'string'),
983
+ );
984
+ }
985
+
986
+ private getDomainPriorityBucket(toolName: string, candidateDomains: string[]): number {
987
+ const domain = this.getToolDomain(toolName);
988
+ const candidateIndex = candidateDomains.indexOf(domain);
989
+ if (candidateIndex >= 0) {
990
+ return candidateIndex;
991
+ }
992
+ if (toolName.startsWith('operations.')) return 10;
993
+ if (toolName.startsWith('finance.')) return 11;
994
+ if (toolName.startsWith('lms.')) return 12;
995
+ if (toolName.startsWith('core.')) return 13;
996
+ return 99;
437
997
  }
438
998
 
439
999
  private getKeywordBoost(message: string, toolName: string): number {
@@ -475,6 +1035,289 @@ export class McpChatService {
475
1035
  return boost;
476
1036
  }
477
1037
 
1038
+ private buildToolRouterText(
1039
+ message: string,
1040
+ history: ConversationHistoryItem[],
1041
+ ): string {
1042
+ const recentContext = history
1043
+ .slice(-8)
1044
+ .map((item) =>
1045
+ [item.role === 'user' ? item.content : undefined, item.tool_name]
1046
+ .filter(Boolean)
1047
+ .join(' '),
1048
+ )
1049
+ .join(' ');
1050
+
1051
+ return `${recentContext} ${message}`;
1052
+ }
1053
+
1054
+ private normalizeText(value: string): string {
1055
+ return value
1056
+ .toLowerCase()
1057
+ .normalize('NFD')
1058
+ .replace(/[\u0300-\u036f]/g, '')
1059
+ .replace(/[^a-z0-9]+/g, ' ')
1060
+ .replace(/\s+/g, ' ')
1061
+ .trim();
1062
+ }
1063
+
1064
+ private getToolDomain(toolName: string): string {
1065
+ return toolName.split('.')[0] || 'unknown';
1066
+ }
1067
+
1068
+ private scoreToolDomains(
1069
+ normalizedMessage: string,
1070
+ entries: McpToolEntry[],
1071
+ ): Map<string, number> {
1072
+ const scores = new Map<string, number>();
1073
+
1074
+ for (const [domain, keywords] of Object.entries(DOMAIN_KEYWORDS)) {
1075
+ let score = 0;
1076
+ if (this.hasTerm(normalizedMessage, domain)) {
1077
+ score += 3;
1078
+ }
1079
+ for (const keyword of keywords) {
1080
+ const normalizedKeyword = this.normalizeText(keyword);
1081
+ if (normalizedKeyword && normalizedMessage.includes(normalizedKeyword)) {
1082
+ score += normalizedKeyword.includes(' ') ? 3 : 2;
1083
+ }
1084
+ }
1085
+ if (score > 0 && entries.some((entry) => this.getToolDomain(entry.definition.name) === domain)) {
1086
+ scores.set(domain, score);
1087
+ }
1088
+ }
1089
+
1090
+ for (const entry of entries) {
1091
+ const domain = this.getToolDomain(entry.definition.name);
1092
+ const toolText = this.normalizeText(
1093
+ `${entry.definition.name} ${entry.definition.description ?? ''}`,
1094
+ );
1095
+ let score = scores.get(domain) ?? 0;
1096
+ for (const term of normalizedMessage.split(' ')) {
1097
+ if (term.length >= 4 && toolText.includes(term)) {
1098
+ score += 1;
1099
+ }
1100
+ }
1101
+ if (score > 0) {
1102
+ scores.set(domain, score);
1103
+ }
1104
+ }
1105
+
1106
+ return scores;
1107
+ }
1108
+
1109
+ private getCandidateDomains(domainScores: Map<string, number>): string[] {
1110
+ const ranked = Array.from(domainScores.entries())
1111
+ .filter(([, score]) => score >= MIN_DOMAIN_CONFIDENCE)
1112
+ .sort((a, b) => b[1] - a[1]);
1113
+
1114
+ if (ranked.length === 0) {
1115
+ return [];
1116
+ }
1117
+
1118
+ const topScore = ranked[0][1];
1119
+ return ranked
1120
+ .filter(([, score]) => topScore - score <= AMBIGUOUS_DOMAIN_DELTA)
1121
+ .map(([domain]) => domain);
1122
+ }
1123
+
1124
+ private getToolSelectionClarification(
1125
+ originalMessage: string,
1126
+ normalizedMessage: string,
1127
+ domainScores: Map<string, number>,
1128
+ candidateDomains: string[],
1129
+ entries: McpToolEntry[],
1130
+ ): string | undefined {
1131
+ if (!this.messageLooksLikeToolRequest(normalizedMessage)) {
1132
+ return undefined;
1133
+ }
1134
+
1135
+ const rankedDomains = Array.from(domainScores.entries()).sort((a, b) => b[1] - a[1]);
1136
+ if (rankedDomains.length === 0 || rankedDomains[0][1] < MIN_DOMAIN_CONFIDENCE) {
1137
+ return this.isLikelyPortuguese(originalMessage)
1138
+ ? 'Você quer consultar qual área do sistema? Por exemplo: financeiro, operações, LMS ou configurações.'
1139
+ : 'Which system area do you want to query? For example: finance, operations, LMS, or settings.';
1140
+ }
1141
+
1142
+ if (candidateDomains.length > 1) {
1143
+ const options = candidateDomains
1144
+ .slice(0, 3)
1145
+ .map((domain) => this.getDomainLabel(domain, originalMessage))
1146
+ .join(', ');
1147
+ return this.isLikelyPortuguese(originalMessage)
1148
+ ? `Você quer consultar qual área: ${options}?`
1149
+ : `Which area do you want to query: ${options}?`;
1150
+ }
1151
+
1152
+ if (
1153
+ candidateDomains[0] === 'finance' &&
1154
+ this.isAmbiguousFinanceAccountsRequest(normalizedMessage, entries)
1155
+ ) {
1156
+ return this.isLikelyPortuguese(originalMessage)
1157
+ ? 'Você quer consultar contas bancárias, contas a pagar ou contas a receber?'
1158
+ : 'Do you want bank accounts, accounts payable, or accounts receivable?';
1159
+ }
1160
+
1161
+ return undefined;
1162
+ }
1163
+
1164
+ private scoreToolMatch(
1165
+ normalizedMessage: string,
1166
+ terms: Set<string>,
1167
+ entry: McpToolEntry,
1168
+ candidateDomains: string[],
1169
+ ): number {
1170
+ const toolName = entry.definition.name;
1171
+ const toolText = this.normalizeText(
1172
+ `${toolName} ${entry.definition.description ?? ''} ${this.getSchemaText(entry.definition.inputSchema)}`,
1173
+ );
1174
+ const domain = this.getToolDomain(toolName);
1175
+ let score = candidateDomains.includes(domain) ? 8 : 0;
1176
+
1177
+ for (const term of normalizedMessage.split(' ')) {
1178
+ if (term.length < 3) continue;
1179
+ if (toolText.includes(term)) {
1180
+ score += term.length >= 5 ? 2 : 1;
1181
+ }
1182
+ }
1183
+
1184
+ if (terms.has('list') && entry.definition.readOnly) score += 4;
1185
+ if (terms.has('write') && !entry.definition.readOnly) score += 4;
1186
+
1187
+ score += this.getFinanceSpecificBoost(normalizedMessage, toolName);
1188
+ score += this.getKeywordBoost(normalizedMessage, toolName);
1189
+
1190
+ return score;
1191
+ }
1192
+
1193
+ private getSchemaText(schema: Record<string, any> | undefined): string {
1194
+ if (!schema || typeof schema !== 'object') {
1195
+ return '';
1196
+ }
1197
+ const properties = schema.properties;
1198
+ if (!properties || typeof properties !== 'object') {
1199
+ return '';
1200
+ }
1201
+ return Object.keys(properties).join(' ');
1202
+ }
1203
+
1204
+ private getFinanceSpecificBoost(message: string, toolName: string): number {
1205
+ if (!toolName.startsWith('finance.')) {
1206
+ return 0;
1207
+ }
1208
+
1209
+ let boost = 0;
1210
+ if (
1211
+ /banco|bancaria|bancarias|bank/.test(message) &&
1212
+ toolName.includes('bank-accounts')
1213
+ ) {
1214
+ boost += 10;
1215
+ }
1216
+ if (/extrato|statement/.test(message) && toolName.includes('statements')) {
1217
+ boost += 10;
1218
+ }
1219
+ if (/pagar|payable/.test(message) && toolName.includes('accounts-payable')) {
1220
+ boost += 10;
1221
+ }
1222
+ if (/receber|receivable/.test(message) && toolName.includes('accounts-receivable')) {
1223
+ boost += 10;
1224
+ }
1225
+ if (/moeda|currency/.test(message) && toolName.includes('currencies')) {
1226
+ boost += 8;
1227
+ }
1228
+ if (/transferencia|transfer|transferencias/.test(message) && toolName.includes('transfers')) {
1229
+ boost += 8;
1230
+ }
1231
+ return boost;
1232
+ }
1233
+
1234
+ private messageLooksLikeToolRequest(normalizedMessage: string): boolean {
1235
+ return ACTION_KEYWORDS.some((keyword) =>
1236
+ this.hasTerm(normalizedMessage, this.normalizeText(keyword)),
1237
+ );
1238
+ }
1239
+
1240
+ private isAmbiguousFinanceAccountsRequest(
1241
+ normalizedMessage: string,
1242
+ entries: McpToolEntry[],
1243
+ ): boolean {
1244
+ const mentionsGenericAccounts = /\b(conta|contas|account|accounts)\b/.test(normalizedMessage);
1245
+ const hasQualifier =
1246
+ /banco|bancaria|bancarias|bank|extrato|statement|pagar|payable|receber|receivable/.test(
1247
+ normalizedMessage,
1248
+ );
1249
+ if (!mentionsGenericAccounts || hasQualifier) {
1250
+ return false;
1251
+ }
1252
+
1253
+ const financeToolNames = entries
1254
+ .map((entry) => entry.definition.name)
1255
+ .filter((name) => name.startsWith('finance.'));
1256
+
1257
+ return (
1258
+ financeToolNames.some((name) => name.includes('bank-accounts')) &&
1259
+ (financeToolNames.some((name) => name.includes('accounts-payable')) ||
1260
+ financeToolNames.some((name) => name.includes('accounts-receivable')))
1261
+ );
1262
+ }
1263
+
1264
+ private hasTerm(text: string, term: string): boolean {
1265
+ return new RegExp(`(^|\\s)${this.escapeRegex(term)}($|\\s)`).test(text);
1266
+ }
1267
+
1268
+ private escapeRegex(value: string): string {
1269
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1270
+ }
1271
+
1272
+ private getDomainLabel(domain: string, message: string): string {
1273
+ const labels: Record<string, { pt: string; en: string }> = {
1274
+ finance: { pt: 'financeiro', en: 'finance' },
1275
+ operations: { pt: 'operações', en: 'operations' },
1276
+ lms: { pt: 'LMS/cursos', en: 'LMS/courses' },
1277
+ core: { pt: 'configurações/core', en: 'settings/core' },
1278
+ contact: { pt: 'contatos', en: 'contacts' },
1279
+ };
1280
+ const label = labels[domain];
1281
+ if (!label) {
1282
+ return domain;
1283
+ }
1284
+ return this.isLikelyPortuguese(message) ? label.pt : label.en;
1285
+ }
1286
+
1287
+ private isLikelyPortuguese(message: string): boolean {
1288
+ return /(voce|você|qual|consultar|listar|conta|contas|bancaria|bancária|extrato|tarefas|curso|cursos)/i.test(
1289
+ message,
1290
+ );
1291
+ }
1292
+
1293
+ private formatDomainScores(domainScores: Map<string, number>): string {
1294
+ const formatted = Array.from(domainScores.entries())
1295
+ .sort((a, b) => b[1] - a[1])
1296
+ .slice(0, 5)
1297
+ .map(([domain, score]) => `${domain}:${score}`)
1298
+ .join(',');
1299
+
1300
+ return formatted || 'none';
1301
+ }
1302
+
1303
+ private async recordAssistantAndReturnHistory(
1304
+ conversationId: number,
1305
+ content: string,
1306
+ ) {
1307
+ await this.prisma.mcp_conversation_message.create({
1308
+ data: {
1309
+ conversation_id: conversationId,
1310
+ role: 'assistant',
1311
+ content,
1312
+ },
1313
+ });
1314
+
1315
+ return this.prisma.mcp_conversation_message.findMany({
1316
+ where: { conversation_id: conversationId },
1317
+ orderBy: { created_at: 'asc' },
1318
+ });
1319
+ }
1320
+
478
1321
  private getMessageKeywordSet(message: string): Set<string> {
479
1322
  const keywords = new Set<string>();
480
1323
  if (/(tarefa|tarefas|task|tasks)/i.test(message)) {
@@ -489,6 +1332,12 @@ export class McpChatService {
489
1332
  if (/(pendente|pendentes|pending|open|aberta|abertas)/i.test(message)) {
490
1333
  keywords.add('pending');
491
1334
  }
1335
+ if (/(listar|lista|buscar|procurar|consultar|mostrar|ver|list|search|find|show|get)/i.test(message)) {
1336
+ keywords.add('list');
1337
+ }
1338
+ if (/(criar|adicionar|atualizar|alterar|remover|deletar|excluir|create|add|update|delete|remove)/i.test(message)) {
1339
+ keywords.add('write');
1340
+ }
492
1341
  return keywords;
493
1342
  }
494
1343
  }