@samanhappy/mcphub 1.0.39 → 1.0.41

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 (140) hide show
  1. package/dist/betterAuth.js +23 -0
  2. package/dist/betterAuth.js.map +1 -1
  3. package/dist/controllers/mcpbController.js +8 -0
  4. package/dist/controllers/mcpbController.js.map +1 -1
  5. package/dist/controllers/oauthCallbackController.js +85 -43
  6. package/dist/controllers/oauthCallbackController.js.map +1 -1
  7. package/dist/controllers/serverController.js +52 -7
  8. package/dist/controllers/serverController.js.map +1 -1
  9. package/dist/controllers/smartRoutingController.js +408 -0
  10. package/dist/controllers/smartRoutingController.js.map +1 -0
  11. package/dist/dao/CredentialBindingDao.js +5 -0
  12. package/dist/dao/CredentialBindingDao.js.map +1 -1
  13. package/dist/dao/CredentialBindingDaoDbImpl.js +7 -0
  14. package/dist/dao/CredentialBindingDaoDbImpl.js.map +1 -1
  15. package/dist/db/connection.js +38 -11
  16. package/dist/db/connection.js.map +1 -1
  17. package/dist/db/groupSchema.js +2 -2
  18. package/dist/db/groupSchema.js.map +1 -1
  19. package/dist/db/repositories/ActivityRepository.js +2 -2
  20. package/dist/db/repositories/ActivityRepository.js.map +1 -1
  21. package/dist/db/repositories/BaseRepository.js +4 -1
  22. package/dist/db/repositories/BaseRepository.js.map +1 -1
  23. package/dist/db/repositories/BearerKeyRepository.js +3 -1
  24. package/dist/db/repositories/BearerKeyRepository.js.map +1 -1
  25. package/dist/db/repositories/BuiltinPromptRepository.js +2 -2
  26. package/dist/db/repositories/BuiltinPromptRepository.js.map +1 -1
  27. package/dist/db/repositories/BuiltinResourceRepository.js +2 -2
  28. package/dist/db/repositories/BuiltinResourceRepository.js.map +1 -1
  29. package/dist/db/repositories/GroupRepository.js +2 -2
  30. package/dist/db/repositories/GroupRepository.js.map +1 -1
  31. package/dist/db/repositories/OAuthClientRepository.js +2 -2
  32. package/dist/db/repositories/OAuthClientRepository.js.map +1 -1
  33. package/dist/db/repositories/OAuthTokenRepository.js +3 -1
  34. package/dist/db/repositories/OAuthTokenRepository.js.map +1 -1
  35. package/dist/db/repositories/ServerRepository.js +2 -2
  36. package/dist/db/repositories/ServerRepository.js.map +1 -1
  37. package/dist/db/repositories/UserConfigRepository.js +2 -2
  38. package/dist/db/repositories/UserConfigRepository.js.map +1 -1
  39. package/dist/db/repositories/UserRepository.js +3 -1
  40. package/dist/db/repositories/UserRepository.js.map +1 -1
  41. package/dist/middlewares/auth.js +1 -1
  42. package/dist/middlewares/auth.js.map +1 -1
  43. package/dist/middlewares/index.js +18 -8
  44. package/dist/middlewares/index.js.map +1 -1
  45. package/dist/routes/index.js +12 -2
  46. package/dist/routes/index.js.map +1 -1
  47. package/dist/services/betterAuthConfig.js +19 -0
  48. package/dist/services/betterAuthConfig.js.map +1 -1
  49. package/dist/services/mcpOAuthProvider.js +7 -0
  50. package/dist/services/mcpOAuthProvider.js.map +1 -1
  51. package/dist/services/mcpService.js +99 -18
  52. package/dist/services/mcpService.js.map +1 -1
  53. package/dist/services/principalRuntimeService.js +1 -3
  54. package/dist/services/principalRuntimeService.js.map +1 -1
  55. package/dist/services/vectorSearchService.js +40 -17
  56. package/dist/services/vectorSearchService.js.map +1 -1
  57. package/dist/utils/oauthIssuer.js +15 -1
  58. package/dist/utils/oauthIssuer.js.map +1 -1
  59. package/dist/utils/rateLimit.js +20 -2
  60. package/dist/utils/rateLimit.js.map +1 -1
  61. package/dist/utils/requireAdmin.js +8 -0
  62. package/dist/utils/requireAdmin.js.map +1 -1
  63. package/dist/utils/serialization.js +14 -2
  64. package/dist/utils/serialization.js.map +1 -1
  65. package/dist/utils/serverConfigPersistence.js +12 -0
  66. package/dist/utils/serverConfigPersistence.js.map +1 -1
  67. package/dist/utils/smartRouting.js +69 -27
  68. package/dist/utils/smartRouting.js.map +1 -1
  69. package/dist/utils/upstreamError.js +60 -0
  70. package/dist/utils/upstreamError.js.map +1 -0
  71. package/dist/utils/withTimeout.js +25 -0
  72. package/dist/utils/withTimeout.js.map +1 -0
  73. package/frontend/dist/assets/{ActivityPage-CDKSVr2I.js → ActivityPage-adLbSuxT.js} +2 -2
  74. package/frontend/dist/assets/{ActivityPage-CDKSVr2I.js.map → ActivityPage-adLbSuxT.js.map} +1 -1
  75. package/frontend/dist/assets/CopyClientConfigDialog-hRNdG_Hz.js +4 -0
  76. package/frontend/dist/assets/CopyClientConfigDialog-hRNdG_Hz.js.map +1 -0
  77. package/frontend/dist/assets/{CredentialsPage-DMJE4uT2.js → CredentialsPage-BSogppb6.js} +2 -2
  78. package/frontend/dist/assets/{CredentialsPage-DMJE4uT2.js.map → CredentialsPage-BSogppb6.js.map} +1 -1
  79. package/frontend/dist/assets/Dashboard-BttcAnP2.js +2 -0
  80. package/frontend/dist/assets/{Dashboard-Dh_39img.js.map → Dashboard-BttcAnP2.js.map} +1 -1
  81. package/frontend/dist/assets/{EndpointCopy-CQmwGXbD.js → EndpointCopy-CNhtxQdx.js} +2 -2
  82. package/frontend/dist/assets/{EndpointCopy-CQmwGXbD.js.map → EndpointCopy-CNhtxQdx.js.map} +1 -1
  83. package/frontend/dist/assets/GroupsPage-CQA7x2cz.js +33 -0
  84. package/frontend/dist/assets/{GroupsPage-BqscWSfR.js.map → GroupsPage-CQA7x2cz.js.map} +1 -1
  85. package/frontend/dist/assets/{LoginPage-DcrwvgS6.js → LoginPage-C4qBJzYk.js} +2 -2
  86. package/frontend/dist/assets/{LoginPage-DcrwvgS6.js.map → LoginPage-C4qBJzYk.js.map} +1 -1
  87. package/frontend/dist/assets/{LogsPage--Blkr-Ni.js → LogsPage-BXHqsBa0.js} +2 -2
  88. package/frontend/dist/assets/{LogsPage--Blkr-Ni.js.map → LogsPage-BXHqsBa0.js.map} +1 -1
  89. package/frontend/dist/assets/MarketPage-C2q7W2an.js +2 -0
  90. package/frontend/dist/assets/MarketPage-C2q7W2an.js.map +1 -0
  91. package/frontend/dist/assets/{OAuthConsentPage-DCKZIMzE.js → OAuthConsentPage-D-UU5XNF.js} +2 -2
  92. package/frontend/dist/assets/{OAuthConsentPage-DCKZIMzE.js.map → OAuthConsentPage-D-UU5XNF.js.map} +1 -1
  93. package/frontend/dist/assets/{PromptsPage-DoVmnVnp.js → PromptsPage-2BBvSosU.js} +2 -2
  94. package/frontend/dist/assets/{PromptsPage-DoVmnVnp.js.map → PromptsPage-2BBvSosU.js.map} +1 -1
  95. package/frontend/dist/assets/{ResourcesPage-B8udCQQR.js → ResourcesPage-DLVYyIm0.js} +2 -2
  96. package/frontend/dist/assets/{ResourcesPage-B8udCQQR.js.map → ResourcesPage-DLVYyIm0.js.map} +1 -1
  97. package/frontend/dist/assets/ServersPage-Cx9zIvKw.js +39 -0
  98. package/frontend/dist/assets/ServersPage-Cx9zIvKw.js.map +1 -0
  99. package/frontend/dist/assets/SettingsPage-pZCOi8NF.js +12 -0
  100. package/frontend/dist/assets/SettingsPage-pZCOi8NF.js.map +1 -0
  101. package/frontend/dist/assets/{StatusDot-DgDU3plH.js → StatusDot-CAv_zsld.js} +2 -2
  102. package/frontend/dist/assets/{StatusDot-DgDU3plH.js.map → StatusDot-CAv_zsld.js.map} +1 -1
  103. package/frontend/dist/assets/{ToggleGroup-11yhmcwY.js → ToggleGroup-ffEOMWal.js} +2 -2
  104. package/frontend/dist/assets/{ToggleGroup-11yhmcwY.js.map → ToggleGroup-ffEOMWal.js.map} +1 -1
  105. package/frontend/dist/assets/{UsersPage-BqPEkoIX.js → UsersPage-Cx6EDM_s.js} +2 -2
  106. package/frontend/dist/assets/{UsersPage-BqPEkoIX.js.map → UsersPage-Cx6EDM_s.js.map} +1 -1
  107. package/frontend/dist/assets/clipboard-_snued0J.js +2 -0
  108. package/frontend/dist/assets/clipboard-_snued0J.js.map +1 -0
  109. package/frontend/dist/assets/{icons-vendor-Bk5sng-h.js → icons-vendor-CNEaJWqW.js} +78 -63
  110. package/frontend/dist/assets/icons-vendor-CNEaJWqW.js.map +1 -0
  111. package/frontend/dist/assets/index-Dt8f_hd5.js +3 -0
  112. package/frontend/dist/assets/index-Dt8f_hd5.js.map +1 -0
  113. package/frontend/dist/assets/index-ru-OAIcF.css +1 -0
  114. package/frontend/dist/assets/{resourceService-D-PRt7tB.js → resourceService-BCl2XrNf.js} +2 -2
  115. package/frontend/dist/assets/{resourceService-D-PRt7tB.js.map → resourceService-BCl2XrNf.js.map} +1 -1
  116. package/frontend/dist/assets/{useCostData-B-uSfiuA.js → useCostData-D3JNFGsT.js} +2 -2
  117. package/frontend/dist/assets/{useCostData-B-uSfiuA.js.map → useCostData-D3JNFGsT.js.map} +1 -1
  118. package/frontend/dist/assets/{variableDetection-B7Ko56CL.js → variableDetection-CGuC1br1.js} +3 -3
  119. package/frontend/dist/assets/variableDetection-CGuC1br1.js.map +1 -0
  120. package/frontend/dist/index.html +3 -3
  121. package/package.json +4 -1
  122. package/frontend/dist/assets/CopyClientConfigDialog-DYiucK0d.js +0 -4
  123. package/frontend/dist/assets/CopyClientConfigDialog-DYiucK0d.js.map +0 -1
  124. package/frontend/dist/assets/Dashboard-Dh_39img.js +0 -2
  125. package/frontend/dist/assets/GroupsPage-BqscWSfR.js +0 -33
  126. package/frontend/dist/assets/MarketPage-DWv18j4V.js +0 -3
  127. package/frontend/dist/assets/MarketPage-DWv18j4V.js.map +0 -1
  128. package/frontend/dist/assets/ServersPage-OLpH1K1I.js +0 -39
  129. package/frontend/dist/assets/ServersPage-OLpH1K1I.js.map +0 -1
  130. package/frontend/dist/assets/SettingsPage-DlK0Jcta.js +0 -12
  131. package/frontend/dist/assets/SettingsPage-DlK0Jcta.js.map +0 -1
  132. package/frontend/dist/assets/clipboard-BukcCPLt.js +0 -2
  133. package/frontend/dist/assets/clipboard-BukcCPLt.js.map +0 -1
  134. package/frontend/dist/assets/icons-vendor-Bk5sng-h.js.map +0 -1
  135. package/frontend/dist/assets/index-D4Vy78bV.css +0 -1
  136. package/frontend/dist/assets/index-DhzWX9nb.js +0 -3
  137. package/frontend/dist/assets/index-DhzWX9nb.js.map +0 -1
  138. package/frontend/dist/assets/useSettingsData-Bk1AwN5F.js +0 -2
  139. package/frontend/dist/assets/useSettingsData-Bk1AwN5F.js.map +0 -1
  140. package/frontend/dist/assets/variableDetection-B7Ko56CL.js.map +0 -1
@@ -0,0 +1,408 @@
1
+ import { getSmartRoutingConfig } from '../utils/smartRouting.js';
2
+ import { hasCredentialTemplate } from '../utils/credentialTemplate.js';
3
+ import { requireAdmin } from '../utils/requireAdmin.js';
4
+ import { logger } from '../utils/logger.js';
5
+ import { getCredentialBindingDao, getUserDao } from '../dao/DaoFactory.js';
6
+ import { getAppDataSource, getDatabaseHealth, initializeDatabase, isDatabaseConnected, } from '../db/connection.js';
7
+ import { saveToolsAsVectorEmbeddings } from '../services/vectorSearchService.js';
8
+ import { getServerToolsForPrincipal, getServersInfo } from '../services/mcpService.js';
9
+ /**
10
+ * Resolve the effective embedding model that the vector store is (or will be)
11
+ * persisted with. Mirrors the resolution in vectorSearchService.saveToolsAsVectorEmbeddings
12
+ * so the reported model matches what is actually written to vector_embeddings.
13
+ */
14
+ const resolvePersistedEmbeddingModel = (config) => {
15
+ const provider = config.embeddingProvider || 'openai';
16
+ if (provider === 'azure_openai') {
17
+ return config.azureOpenaiEmbeddingModel?.trim() || 'text-embedding-3-small';
18
+ }
19
+ return config.embeddingModel?.trim() || null;
20
+ };
21
+ /**
22
+ * Read aggregate statistics from the vector_embeddings table.
23
+ * Returns null when the query fails so callers can degrade gracefully instead
24
+ * of returning a 500 for a purely informational endpoint.
25
+ */
26
+ const queryVectorStoreStats = async () => {
27
+ try {
28
+ const dataSource = getAppDataSource();
29
+ const [overview, byModel, byServer, freshness, dims] = await Promise.all([
30
+ dataSource.query(`SELECT
31
+ count(*) FILTER (WHERE content_type = 'tool') AS tool_rows,
32
+ count(*) FILTER (WHERE content_type = 'server') AS server_rows,
33
+ count(*) AS total_rows,
34
+ count(DISTINCT metadata::jsonb->>'serverName') AS distinct_servers
35
+ FROM vector_embeddings`),
36
+ dataSource.query(`SELECT model,
37
+ count(*) FILTER (WHERE content_type = 'tool') AS tool_count,
38
+ count(*) FILTER (WHERE content_type = 'server') AS server_count
39
+ FROM vector_embeddings
40
+ GROUP BY model
41
+ ORDER BY model`),
42
+ dataSource.query(`SELECT metadata::jsonb->>'serverName' AS server_name, count(*) AS tool_count
43
+ FROM vector_embeddings
44
+ WHERE content_type = 'tool'
45
+ GROUP BY 1
46
+ ORDER BY tool_count DESC
47
+ LIMIT 500`),
48
+ dataSource.query(`SELECT to_char(min(updated_at), 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') AS oldest,
49
+ to_char(max(updated_at), 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') AS newest
50
+ FROM vector_embeddings`),
51
+ dataSource.query(`SELECT atttypmod AS dimensions
52
+ FROM pg_attribute
53
+ WHERE attrelid = 'vector_embeddings'::regclass
54
+ AND attname = 'embedding'`),
55
+ ]);
56
+ const rawDimensions = dims?.[0]?.dimensions;
57
+ const dimValue = Number(rawDimensions);
58
+ const dimensions = rawDimensions !== undefined && rawDimensions !== null && Number.isFinite(dimValue)
59
+ ? dimValue
60
+ : null;
61
+ return {
62
+ totalRows: Number(overview?.[0]?.total_rows ?? 0),
63
+ toolRows: Number(overview?.[0]?.tool_rows ?? 0),
64
+ serverRows: Number(overview?.[0]?.server_rows ?? 0),
65
+ distinctServers: Number(overview?.[0]?.distinct_servers ?? 0),
66
+ dimensions,
67
+ byModel: (byModel ?? []).map((row) => ({
68
+ model: String(row.model ?? ''),
69
+ toolCount: Number(row.tool_count ?? 0),
70
+ serverCount: Number(row.server_count ?? 0),
71
+ })),
72
+ byServer: (byServer ?? []).map((row) => ({
73
+ serverName: String(row.server_name ?? ''),
74
+ toolCount: Number(row.tool_count ?? 0),
75
+ })),
76
+ oldestUpdatedAt: freshness?.[0]?.oldest ?? null,
77
+ newestUpdatedAt: freshness?.[0]?.newest ?? null,
78
+ };
79
+ }
80
+ catch (error) {
81
+ logger.warn('Failed to collect vector store statistics:', error);
82
+ return null;
83
+ }
84
+ };
85
+ /**
86
+ * Serializes reindex passes inside this process. Overlapping runs would
87
+ * interleave their DELETE and re-embed work — leaving partially indexed servers
88
+ * behind — and double-spend embedding quota, so a second caller is rejected
89
+ * with 409 instead of racing the run in flight.
90
+ */
91
+ let reindexInFlight = false;
92
+ /**
93
+ * GET /api/smart-routing/performance
94
+ *
95
+ * Returns a read-only snapshot of smart routing health and the vector store:
96
+ * resolved embedding configuration, database connection state, row counts,
97
+ * per-model/per-server breakdown, and sync freshness.
98
+ *
99
+ * Admin-only: the dashboard renders this section behind the admin-only
100
+ * `settings:smart_routing` permission, and the snapshot exposes global
101
+ * operational state (database configuration, index coverage).
102
+ */
103
+ export const getSmartRoutingPerformance = async (req, res) => {
104
+ if (!(await requireAdmin(req, res)))
105
+ return;
106
+ try {
107
+ const smartRoutingConfig = await getSmartRoutingConfig();
108
+ const databaseHealth = getDatabaseHealth();
109
+ // Only reach into the database when smart routing is enabled and a DB URL
110
+ // is configured; otherwise report the disabled/unconfigured state as-is.
111
+ const dbAvailable = smartRoutingConfig.enabled &&
112
+ Boolean(smartRoutingConfig.dbUrl || process.env.DB_URL);
113
+ let vectorStore = null;
114
+ if (dbAvailable) {
115
+ if (!isDatabaseConnected()) {
116
+ try {
117
+ await initializeDatabase();
118
+ }
119
+ catch (error) {
120
+ logger.warn('Failed to initialize database for performance stats:', error);
121
+ }
122
+ }
123
+ if (isDatabaseConnected()) {
124
+ vectorStore = await queryVectorStoreStats();
125
+ }
126
+ }
127
+ // Coverage is measured against every enabled server, not only the ones this
128
+ // principal happens to see connected: servers that require personal
129
+ // credentials are never connected globally, so a "connected" baseline would
130
+ // silently hide them from the index report.
131
+ let enabledServerNames = [];
132
+ let connectedServers = 0;
133
+ let personalCredentialServers = 0;
134
+ try {
135
+ const servers = await getServersInfo();
136
+ const enabled = servers.filter((server) => server.enabled !== false);
137
+ enabledServerNames = enabled.map((server) => server.name);
138
+ connectedServers = enabled.filter((server) => server.status === 'connected').length;
139
+ personalCredentialServers = enabled.filter((server) => hasCredentialTemplate(server.config)).length;
140
+ }
141
+ catch (error) {
142
+ logger.warn('Failed to resolve servers for performance stats:', error);
143
+ }
144
+ const indexedServerSet = new Set((vectorStore?.byServer ?? []).map((entry) => entry.serverName));
145
+ const missingIndexServers = enabledServerNames.filter((name) => !indexedServerSet.has(name));
146
+ const provider = smartRoutingConfig.embeddingProvider || 'openai';
147
+ const data = {
148
+ enabled: smartRoutingConfig.enabled,
149
+ reindexing: reindexInFlight,
150
+ config: {
151
+ provider,
152
+ model: resolvePersistedEmbeddingModel(smartRoutingConfig),
153
+ configuredDimensions: typeof smartRoutingConfig.embeddingDimensions === 'number'
154
+ ? smartRoutingConfig.embeddingDimensions
155
+ : null,
156
+ },
157
+ database: {
158
+ connected: databaseHealth.connected,
159
+ healthy: databaseHealth.healthy,
160
+ lastError: databaseHealth.lastError,
161
+ },
162
+ vectorStore: {
163
+ available: vectorStore !== null,
164
+ ...(vectorStore ?? {
165
+ totalRows: 0,
166
+ toolRows: 0,
167
+ serverRows: 0,
168
+ distinctServers: 0,
169
+ dimensions: null,
170
+ byModel: [],
171
+ byServer: [],
172
+ oldestUpdatedAt: null,
173
+ newestUpdatedAt: null,
174
+ }),
175
+ },
176
+ coverage: {
177
+ totalServers: enabledServerNames.length,
178
+ connectedServers,
179
+ personalCredentialServers,
180
+ indexedServers: indexedServerSet.size,
181
+ missingIndexServerCount: missingIndexServers.length,
182
+ missingIndexServers: missingIndexServers.slice(0, 100),
183
+ },
184
+ };
185
+ const response = { success: true, data };
186
+ res.json(response);
187
+ }
188
+ catch (error) {
189
+ logger.error('Failed to collect smart routing performance stats:', error);
190
+ res.status(500).json({ success: false, message: 'Failed to collect smart routing performance stats' });
191
+ }
192
+ };
193
+ /**
194
+ * Usernames holding a personal credential binding for a server, ordered with
195
+ * the server owner first so indexing favours the account that defines it.
196
+ */
197
+ const listBindingUsernames = async (serverName, owner) => {
198
+ try {
199
+ const usernames = await getCredentialBindingDao().listUsernames(serverName);
200
+ const unique = [
201
+ ...new Set(usernames.filter((name) => typeof name === 'string' && name.trim().length > 0)),
202
+ ];
203
+ unique.sort((a, b) => {
204
+ if (a === owner)
205
+ return -1;
206
+ if (b === owner)
207
+ return 1;
208
+ return a.localeCompare(b);
209
+ });
210
+ return unique;
211
+ }
212
+ catch (error) {
213
+ logger.warn(`Failed to list credential bindings for server "${serverName}":`, error);
214
+ return [];
215
+ }
216
+ };
217
+ /**
218
+ * Collect the union of tools that the binding principals can see for a server.
219
+ * Principals are resolved sequentially because each one may spawn its own
220
+ * upstream connection; a reindex pass should not fan out across all of them.
221
+ */
222
+ const collectToolsForPrincipals = async (serverName, usernames) => {
223
+ const merged = new Map();
224
+ const principals = [];
225
+ const errors = [];
226
+ for (const username of usernames) {
227
+ try {
228
+ const user = await getUserDao().findByUsername(username);
229
+ if (!user) {
230
+ errors.push(`${username}: user not found`);
231
+ continue;
232
+ }
233
+ const tools = await getServerToolsForPrincipal(serverName, {
234
+ username,
235
+ isAdmin: Boolean(user.isAdmin),
236
+ credentialEligible: true,
237
+ });
238
+ for (const tool of tools) {
239
+ if (tool?.name && !merged.has(tool.name)) {
240
+ merged.set(tool.name, tool);
241
+ }
242
+ }
243
+ principals.push(username);
244
+ }
245
+ catch (error) {
246
+ errors.push(`${username}: ${error?.message ? String(error.message) : 'unknown error'}`);
247
+ }
248
+ }
249
+ return { tools: [...merged.values()], principals, errors };
250
+ };
251
+ /**
252
+ * POST /api/smart-routing/reindex
253
+ *
254
+ * Force a full rebuild of all vector embeddings: every row in
255
+ * vector_embeddings is cleared, then every enabled server is re-embedded.
256
+ *
257
+ * Servers that declare a personal credential template are never connected
258
+ * globally — their runtime only exists for principals that bound their own
259
+ * credentials. Those servers are therefore indexed on behalf of the users who
260
+ * hold a binding (the server owner first), and the union of the tools those
261
+ * principals can see is what gets embedded. A server with no binding at all is
262
+ * reported as skipped instead of being silently dropped from the index.
263
+ *
264
+ * Progress is streamed to authenticated clients through the existing
265
+ * embedding-sync-progress SSE events emitted by saveToolsAsVectorEmbeddings.
266
+ * Response is sent only when the whole pass finishes (synchronous execution).
267
+ *
268
+ * Admin-only, and serialized: it clears a global table and spends embedding
269
+ * quota, so a second concurrent call is answered with 409.
270
+ */
271
+ export const reindexSmartRouting = async (req, res) => {
272
+ if (!(await requireAdmin(req, res)))
273
+ return;
274
+ if (reindexInFlight) {
275
+ res.status(409).json({
276
+ success: false,
277
+ message: 'A reindex pass is already running',
278
+ });
279
+ return;
280
+ }
281
+ reindexInFlight = true;
282
+ try {
283
+ const smartRoutingConfig = await getSmartRoutingConfig();
284
+ if (!smartRoutingConfig.enabled) {
285
+ res.status(400).json({ success: false, message: 'Smart routing is not enabled' });
286
+ return;
287
+ }
288
+ if (!smartRoutingConfig.dbUrl && !process.env.DB_URL) {
289
+ res.status(400).json({ success: false, message: 'Database (DB_URL) is not configured' });
290
+ return;
291
+ }
292
+ if (!isDatabaseConnected()) {
293
+ await initializeDatabase();
294
+ }
295
+ // 1. Clear the existing index so every tool is regenerated from scratch.
296
+ logger.log('Reindex: clearing all vector embeddings');
297
+ await getAppDataSource().query('DELETE FROM vector_embeddings');
298
+ // 2. Re-embed every enabled server.
299
+ const servers = await getServersInfo();
300
+ const results = [];
301
+ let totalTools = 0;
302
+ let syncedServers = 0;
303
+ let failedServers = 0;
304
+ let skippedServers = 0;
305
+ for (const server of servers) {
306
+ if (server.enabled === false) {
307
+ continue;
308
+ }
309
+ let tools;
310
+ let principals;
311
+ if (hasCredentialTemplate(server.config)) {
312
+ // Personal-credential server: borrow the principals that can reach it.
313
+ const usernames = await listBindingUsernames(server.name, server.config?.owner);
314
+ if (usernames.length === 0) {
315
+ skippedServers += 1;
316
+ results.push({
317
+ serverName: server.name,
318
+ toolCount: 0,
319
+ ok: false,
320
+ skipped: true,
321
+ error: 'No user has bound personal credentials for this server',
322
+ });
323
+ continue;
324
+ }
325
+ const collected = await collectToolsForPrincipals(server.name, usernames);
326
+ principals = collected.principals;
327
+ tools = collected.tools;
328
+ if (tools.length === 0) {
329
+ failedServers += 1;
330
+ results.push({
331
+ serverName: server.name,
332
+ toolCount: 0,
333
+ ok: false,
334
+ principals,
335
+ error: collected.errors.join('; ') ||
336
+ 'No tools could be resolved with the bound personal credentials',
337
+ });
338
+ continue;
339
+ }
340
+ }
341
+ else {
342
+ if (server.status !== 'connected') {
343
+ skippedServers += 1;
344
+ results.push({
345
+ serverName: server.name,
346
+ toolCount: 0,
347
+ ok: false,
348
+ skipped: true,
349
+ error: `Server is not connected (status: ${server.status ?? 'unknown'})`,
350
+ });
351
+ continue;
352
+ }
353
+ tools = server.tools ?? [];
354
+ if (tools.length === 0) {
355
+ skippedServers += 1;
356
+ results.push({
357
+ serverName: server.name,
358
+ toolCount: 0,
359
+ ok: false,
360
+ skipped: true,
361
+ error: 'Server exposes no tools',
362
+ });
363
+ continue;
364
+ }
365
+ }
366
+ try {
367
+ await saveToolsAsVectorEmbeddings(server.name, tools, { reportProgress: true });
368
+ syncedServers += 1;
369
+ totalTools += tools.length;
370
+ results.push({
371
+ serverName: server.name,
372
+ toolCount: tools.length,
373
+ ok: true,
374
+ ...(principals ? { principals } : {}),
375
+ });
376
+ }
377
+ catch (error) {
378
+ failedServers += 1;
379
+ logger.error(`Reindex failed for server "${server.name}":`, error);
380
+ results.push({
381
+ serverName: server.name,
382
+ toolCount: tools.length,
383
+ ok: false,
384
+ ...(principals ? { principals } : {}),
385
+ error: error?.message ? String(error.message) : 'Unknown error',
386
+ });
387
+ }
388
+ }
389
+ const data = {
390
+ syncedServers,
391
+ failedServers,
392
+ skippedServers,
393
+ totalTools,
394
+ results,
395
+ };
396
+ logger.log('Reindex completed', { syncedServers, failedServers, skippedServers, totalTools });
397
+ const response = { success: true, data };
398
+ res.json(response);
399
+ }
400
+ catch (error) {
401
+ logger.error('Failed to rebuild smart routing index:', error);
402
+ res.status(500).json({ success: false, message: 'Failed to rebuild smart routing index' });
403
+ }
404
+ finally {
405
+ reindexInFlight = false;
406
+ }
407
+ };
408
+ //# sourceMappingURL=smartRoutingController.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"smartRoutingController.js","sourceRoot":"","sources":["../../src/controllers/smartRoutingController.ts"],"names":[],"mappings":"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"}
@@ -36,6 +36,11 @@ export class CredentialBindingDaoImpl {
36
36
  return (this.readAll().find((item) => item.serverName === serverName && item.username === username) ??
37
37
  null);
38
38
  }
39
+ async listUsernames(serverName) {
40
+ return this.readAll()
41
+ .filter((item) => item.serverName === serverName)
42
+ .map((item) => item.username);
43
+ }
39
44
  async save(binding) {
40
45
  const bindings = this.readAll().filter((item) => item.serverName !== binding.serverName || item.username !== binding.username);
41
46
  this.write([...bindings, binding]);
@@ -1 +1 @@
1
- {"version":3,"file":"CredentialBindingDao.js","sourceRoot":"","sources":["../../src/dao/CredentialBindingDao.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAUrD,oFAAoF;AACpF,MAAM,OAAO,wBAAwB;IACnC,KAAK,CAAC,WAAW;QACf,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;IACnC,CAAC;IAED,OAAO;QACL,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,GAAG,eAAe,EAAE,mBAAmB,EAAE,MAAM,CAAC,CAAC,CAAC;YAC9F,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC;gBAAE,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;YAClF,OAAO,QAAQ,CAAC;QAClB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;gBAAE,OAAO,EAAE,CAAC;YAClE,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,QAAmC;QAC/C,MAAM,IAAI,GAAG,GAAG,eAAe,EAAE,mBAAmB,CAAC;QACrD,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACtD,MAAM,SAAS,GAAG,GAAG,IAAI,IAAI,UAAU,EAAE,MAAM,CAAC;QAChD,IAAI,CAAC;YACH,EAAE,CAAC,aAAa,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YACnF,EAAE,CAAC,UAAU,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;QACjC,CAAC;gBAAS,CAAC;YACT,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACxC,CAAC;IACH,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,UAAkB,EAAE,QAAgB;QAC5C,OAAO,CACL,IAAI,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,KAAK,UAAU,IAAI,IAAI,CAAC,QAAQ,KAAK,QAAQ,CAAC;YAC3F,IAAI,CACL,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,OAAgC;QACzC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,MAAM,CACpC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,KAAK,OAAO,CAAC,UAAU,IAAI,IAAI,CAAC,QAAQ,KAAK,OAAO,CAAC,QAAQ,CACvF,CAAC;QACF,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;IACrC,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,MAAkD;QAC7D,IAAI,CAAC,KAAK,CACR,IAAI,CAAC,OAAO,EAAE,CAAC,MAAM,CACnB,CAAC,IAAI,EAAE,EAAE,CACP,CAAC,MAAM,CAAC,UAAU,KAAK,SAAS,IAAI,IAAI,CAAC,UAAU,KAAK,MAAM,CAAC,UAAU,CAAC;YAC1E,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,IAAI,IAAI,CAAC,QAAQ,KAAK,MAAM,CAAC,QAAQ,CAAC,CACvE,CACF,CAAC;IACJ,CAAC;CACF"}
1
+ {"version":3,"file":"CredentialBindingDao.js","sourceRoot":"","sources":["../../src/dao/CredentialBindingDao.ts"],"names":[],"mappings":"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"}
@@ -10,6 +10,13 @@ export class CredentialBindingDaoDbImpl {
10
10
  async get(serverName, username) {
11
11
  return this.repository.findOneBy({ serverName, username });
12
12
  }
13
+ async listUsernames(serverName) {
14
+ const rows = await this.repository.find({
15
+ where: { serverName },
16
+ select: { username: true },
17
+ });
18
+ return rows.map((row) => row.username);
19
+ }
13
20
  async save(binding) {
14
21
  await this.repository.upsert(binding, ['serverName', 'username']);
15
22
  }
@@ -1 +1 @@
1
- {"version":3,"file":"CredentialBindingDaoDbImpl.js","sourceRoot":"","sources":["../../src/dao/CredentialBindingDaoDbImpl.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qCAAqC,CAAC;AAIxE,MAAM,OAAO,0BAA0B;IACrC,IAAY,UAAU;QACpB,OAAO,gBAAgB,EAAE,CAAC,aAAa,CAAC,iBAAiB,CAAC,CAAC;IAC7D,CAAC;IAED,KAAK,CAAC,WAAW;QACf,OAAO,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC;IAClC,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,UAAkB,EAAE,QAAgB;QAC5C,OAAO,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC7D,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,OAAgC;QACzC,MAAM,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC,CAAC;IACpE,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,MAAkD;QAC7D,MAAM,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACvC,CAAC;CACF"}
1
+ {"version":3,"file":"CredentialBindingDaoDbImpl.js","sourceRoot":"","sources":["../../src/dao/CredentialBindingDaoDbImpl.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qCAAqC,CAAC;AAIxE,MAAM,OAAO,0BAA0B;IACrC,IAAY,UAAU;QACpB,OAAO,gBAAgB,EAAE,CAAC,aAAa,CAAC,iBAAiB,CAAC,CAAC;IAC7D,CAAC;IAED,KAAK,CAAC,WAAW;QACf,OAAO,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC;IAClC,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,UAAkB,EAAE,QAAgB;QAC5C,OAAO,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC7D,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,UAAkB;QACpC,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YACtC,KAAK,EAAE,EAAE,UAAU,EAAE;YACrB,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;SAC3B,CAAC,CAAC;QACH,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACzC,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,OAAgC;QACzC,MAAM,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC,CAAC;IACpE,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,MAAkD;QAC7D,MAAM,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACvC,CAAC;CACF"}
@@ -7,6 +7,7 @@ import { VectorEmbeddingSubscriber } from './subscribers/VectorEmbeddingSubscrib
7
7
  import { getSmartRoutingConfig } from '../utils/smartRouting.js';
8
8
  import { createVectorIndex } from '../services/vectorSearchService.js';
9
9
  import { isRetryableDbError } from '../utils/dbRetry.js';
10
+ import { withTimeout } from '../utils/withTimeout.js';
10
11
  import { logger } from '../utils/logger.js';
11
12
  // Connection pool and retry configuration
12
13
  const CONNECTION_CONFIG = {
@@ -17,8 +18,18 @@ const CONNECTION_CONFIG = {
17
18
  // Automatic reconnection settings
18
19
  maxConnectionRetries: parseInt(process.env.DB_MAX_CONNECTION_RETRIES || '5', 10),
19
20
  connectionRetryDelayMs: parseInt(process.env.DB_CONNECTION_RETRY_DELAY || '3000', 10),
21
+ // Bounds pool teardown during reconnection. pg-pool's end() only completes
22
+ // once every client is returned, so a client stuck in an open transaction
23
+ // (idle in transaction) would otherwise wedge reconnection forever (#1205).
24
+ destroyTimeout: parseInt(process.env.DB_DESTROY_TIMEOUT || '15000', 10), // 15 seconds
25
+ // Postgres-side safety valves (0 = use PostgreSQL defaults / disabled). Set
26
+ // per-connection via the startup packet so a hung query or a session left in
27
+ // `idle in transaction` cannot hold a pooled connection indefinitely.
28
+ statementTimeout: parseInt(process.env.DB_STATEMENT_TIMEOUT || '0', 10),
29
+ idleInTransactionSessionTimeout: parseInt(process.env.DB_IDLE_IN_TRANSACTION_TIMEOUT || '0', 10),
20
30
  // Health check settings
21
31
  healthCheckIntervalMs: parseInt(process.env.DB_HEALTH_CHECK_INTERVAL || '30000', 10), // 30 seconds
32
+ healthCheckQueryTimeout: parseInt(process.env.DB_HEALTH_CHECK_QUERY_TIMEOUT || '5000', 10), // 5 seconds
22
33
  enableHealthCheck: process.env.DB_ENABLE_HEALTH_CHECK !== 'false',
23
34
  };
24
35
  // Health check state
@@ -94,6 +105,11 @@ const getDefaultConfig = async () => {
94
105
  // Keep-alive settings to detect dead connections faster
95
106
  keepAlive: true,
96
107
  keepAliveInitialDelayMillis: 10000,
108
+ // Postgres-side safety valves (0 = PostgreSQL default / disabled): a
109
+ // hung query or a session stuck in `idle in transaction` must not be
110
+ // able to hold a pooled connection indefinitely (#1205).
111
+ statement_timeout: CONNECTION_CONFIG.statementTimeout,
112
+ idle_in_transaction_session_timeout: CONNECTION_CONFIG.idleInTransactionSessionTimeout,
97
113
  },
98
114
  };
99
115
  };
@@ -394,8 +410,10 @@ export const checkDatabaseHealth = async () => {
394
410
  }
395
411
  }
396
412
  try {
397
- // Simple query to check connection
398
- await appDataSource.query('SELECT 1');
413
+ // Simple query to check connection. Bounded by a short timeout so a DB
414
+ // that stops accepting queries cannot stall the health probe for the full
415
+ // pool connection timeout (#1205).
416
+ await withTimeout(appDataSource.query('SELECT 1'), CONNECTION_CONFIG.healthCheckQueryTimeout, `DB health check failed: connection timeout after ${CONNECTION_CONFIG.healthCheckQueryTimeout}ms`);
399
417
  isHealthy = true;
400
418
  lastHealthCheckError = null;
401
419
  return true;
@@ -453,25 +471,33 @@ const attemptReconnection = () => {
453
471
  if (!appDataSource) {
454
472
  return await initializeDatabase();
455
473
  }
456
- const dataSource = appDataSource;
474
+ let dataSource = appDataSource;
457
475
  if (dataSource.isInitialized) {
458
476
  try {
459
477
  logger.log('[DB Reconnect] Closing existing connection...');
460
- await dataSource.destroy();
478
+ // pg-pool end() waits for every checked-out client. A client stuck
479
+ // in an open transaction would make destroy() hang forever, wedging
480
+ // the whole reconnection machinery — bound it and fall back to a
481
+ // fresh pool instead (#1205).
482
+ await withTimeout(dataSource.destroy(), CONNECTION_CONFIG.destroyTimeout, `[DB Reconnect] destroy() timed out after ${CONNECTION_CONFIG.destroyTimeout}ms`);
461
483
  }
462
484
  catch (closeError) {
463
485
  logger.warn('[DB Reconnect] Error closing connection:', closeError.message);
464
- // TypeORM only clears this flag after driver.disconnect() succeeds.
465
- // If the driver pool is already gone, destroy() throws first and leaves
466
- // the DataSource incorrectly marked as initialized.
467
- Object.assign(dataSource, { isInitialized: false });
486
+ // The old destroy may still finish later and mutate its driver and
487
+ // isInitialized flag. Never initialize that same instance again.
488
+ dataSource = new DataSource({ ...dataSource.options });
489
+ appDataSource = dataSource;
468
490
  }
469
491
  }
470
492
  let lastError = null;
471
493
  for (let attempt = 1; attempt <= CONNECTION_CONFIG.maxConnectionRetries; attempt++) {
472
494
  try {
473
495
  logger.log(`[DB Reconnect] Connection attempt ${attempt}/${CONNECTION_CONFIG.maxConnectionRetries}...`);
474
- await initializeWithGroupNameCheck(dataSource);
496
+ // Reconnect with skipSynchronize: schema DDL runs inside a
497
+ // transaction and can block on locks held by live traffic, leaving a
498
+ // connection stuck in `idle in transaction` and blocking pool
499
+ // teardown. The schema is already synchronized from startup (#1205).
500
+ await initializeWithGroupNameCheck(dataSource, { skipSynchronize: true });
475
501
  registerPostgresVectorType(dataSource);
476
502
  appDataSource = dataSource;
477
503
  logger.log('[DB Reconnect] Successfully reconnected to database');
@@ -484,11 +510,12 @@ const attemptReconnection = () => {
484
510
  logger.warn(`[DB Reconnect] Attempt ${attempt} failed: ${lastError.message}`);
485
511
  if (dataSource.isInitialized) {
486
512
  try {
487
- await dataSource.destroy();
513
+ await withTimeout(dataSource.destroy(), CONNECTION_CONFIG.destroyTimeout, `[DB Reconnect] Cleanup destroy() timed out after ${CONNECTION_CONFIG.destroyTimeout}ms`);
488
514
  }
489
515
  catch (destroyError) {
490
516
  logger.warn('[DB Reconnect] Error cleaning up partially initialized connection:', destroyError.message);
491
- Object.assign(dataSource, { isInitialized: false });
517
+ dataSource = new DataSource({ ...dataSource.options });
518
+ appDataSource = dataSource;
492
519
  }
493
520
  }
494
521
  if (attempt < CONNECTION_CONFIG.maxConnectionRetries) {