@loop_ouroboros/mcp-hub-lite 1.2.1 → 1.2.2
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.
- package/CHANGELOG.md +20 -0
- package/README.md +4 -0
- package/dist/client/assets/{HomeView-BBwvy1oj.js → HomeView-V1fKvWQ8.js} +1 -1
- package/dist/client/assets/{ResourceDetailView-CZ2aB73w.js → ResourceDetailView-DHGHssrh.js} +1 -1
- package/dist/client/assets/{ResourcesView-CN1NlhWs.js → ResourcesView-B1bvkmQD.js} +1 -1
- package/dist/client/assets/{ServerDashboard-k652Vw4Z.js → ServerDashboard-CZCByd7y.js} +1 -1
- package/dist/client/assets/{ServerDetail-BLQ-a4cO.js → ServerDetail-CI5UD8gj.js} +1 -1
- package/dist/client/assets/{ServerListView-BHrsFD5i.js → ServerListView-D8qv-xYg.js} +1 -1
- package/dist/client/assets/SettingsView-C-ae0-zz.js +1 -0
- package/dist/client/assets/{ToolCallDialog-BfPjLxfV.js → ToolCallDialog-BudOyGvS.js} +1 -1
- package/dist/client/assets/{ToolsView-CyuhYAE2.js → ToolsView-CbQkgTAu.js} +1 -1
- package/dist/client/assets/{_baseClone-DO5qfalW.js → _baseClone-e9R6V78L.js} +1 -1
- package/dist/client/assets/{el-form-item-CcGsD2K_.js → el-form-item-Dyx5MiWB.js} +1 -1
- package/dist/client/assets/{el-input-tYgeiaCT.js → el-input-CL9HPfdW.js} +1 -1
- package/dist/client/assets/{el-loading-Dwl9E_Vr.js → el-loading-2TW6JdfY.js} +1 -1
- package/dist/client/assets/{el-overlay-kqX_BABo.js → el-overlay-B5ZGCmXY.js} +1 -1
- package/dist/client/assets/{el-radio-group-D8aWBVOT.js → el-radio-group-Cr2ScjjJ.js} +1 -1
- package/dist/client/assets/{el-skeleton-item-BRwIFspE.js → el-skeleton-item-CdAfEgVR.js} +1 -1
- package/dist/client/assets/{el-switch-BF8c-xeU.js → el-switch-DnN1s0Wb.js} +1 -1
- package/dist/client/assets/{el-tab-pane-C4Ep94cd.js → el-tab-pane-BebZh0XF.js} +1 -1
- package/dist/client/assets/{el-table-column-Cog6uCh-.js → el-table-column-CV2zp3yI.js} +1 -1
- package/dist/client/assets/{index-ByNBhPAR.js → index-Ci5n5dA9.js} +1 -1
- package/dist/client/assets/index-DTZ9o3XO.js +2 -0
- package/dist/client/assets/{omit-CUnDT6sS.js → omit-DlmW8Yd6.js} +1 -1
- package/dist/client/assets/{raf-CmzeRPMd.js → raf-CeCd08aN.js} +1 -1
- package/dist/client/index.html +1 -1
- package/dist/server/shared/models/server.model.d.ts +22 -0
- package/dist/server/shared/models/server.model.d.ts.map +1 -1
- package/dist/server/shared/models/server.model.js +41 -5
- package/dist/server/src/api/mcp/gateway.d.ts.map +1 -1
- package/dist/server/src/api/mcp/gateway.js +13 -5
- package/dist/server/src/api/web/resources.d.ts.map +1 -1
- package/dist/server/src/api/web/resources.js +26 -2
- package/dist/server/src/config/config-loader.js +1 -1
- package/dist/server/src/config/config-migrator.d.ts.map +1 -1
- package/dist/server/src/config/config-migrator.js +1 -0
- package/dist/server/src/index.js +2 -7
- package/dist/server/src/server/dev-server.js +9 -48
- package/dist/server/src/server/runner.d.ts +12 -24
- package/dist/server/src/server/runner.d.ts.map +1 -1
- package/dist/server/src/server/runner.js +29 -109
- package/dist/server/src/server/startup.d.ts +43 -0
- package/dist/server/src/server/startup.d.ts.map +1 -0
- package/dist/server/src/server/startup.js +89 -0
- package/dist/server/src/services/connection/connection-manager.d.ts +52 -0
- package/dist/server/src/services/connection/connection-manager.d.ts.map +1 -1
- package/dist/server/src/services/connection/connection-manager.js +283 -193
- package/dist/server/src/services/gateway/gateway.service.d.ts.map +1 -1
- package/dist/server/src/services/gateway/gateway.service.js +31 -10
- package/dist/server/src/services/gateway/global-transport.d.ts.map +1 -1
- package/dist/server/src/services/gateway/global-transport.js +11 -5
- package/dist/server/src/services/hub-manager.service.d.ts +2 -0
- package/dist/server/src/services/hub-manager.service.d.ts.map +1 -1
- package/dist/server/src/services/hub-manager.service.js +3 -16
- package/dist/server/src/utils/json-utils.d.ts +7 -0
- package/dist/server/src/utils/json-utils.d.ts.map +1 -1
- package/dist/server/src/utils/json-utils.js +17 -0
- package/dist/server/src/utils/logger/log-modules.d.ts +3 -0
- package/dist/server/src/utils/logger/log-modules.d.ts.map +1 -1
- package/dist/server/src/utils/logger/log-modules.js +2 -0
- package/dist/server/src/utils/port-checker.d.ts +18 -0
- package/dist/server/src/utils/port-checker.d.ts.map +1 -1
- package/dist/server/src/utils/port-checker.js +38 -0
- package/dist/server/src/utils/transports/stdio-transport.d.ts +12 -0
- package/dist/server/src/utils/transports/stdio-transport.d.ts.map +1 -1
- package/dist/server/src/utils/transports/stdio-transport.js +51 -2
- package/dist/server/src/utils/transports/transport-factory.d.ts +5 -1
- package/dist/server/src/utils/transports/transport-factory.d.ts.map +1 -1
- package/dist/server/src/utils/transports/transport-factory.js +5 -2
- package/dist/server/tests/integration/gateway/fault-tolerance.test.js +13 -2
- package/dist/server/tests/integration/gateway/mcp-connection.test.js +13 -2
- package/dist/server/tests/unit/config/config-migrator.test.js +4 -2
- package/dist/server/tests/unit/config/config.schema.test.js +2 -1
- package/dist/server/tests/unit/server/runner.test.js +77 -92
- package/dist/server/tests/unit/services/hub-manager-service.test.js +3 -2
- package/dist/server/tests/unit/utils/config.test.js +14 -7
- package/dist/server/tests/unit/utils/json-utils.test.js +28 -14
- package/dist/server/vite.config.d.ts.map +1 -1
- package/dist/server/vite.config.js +1 -0
- package/package.json +1 -1
- package/dist/client/assets/SettingsView-CUOFNXrz.js +0 -1
- package/dist/client/assets/index-CTB6oe-9.js +0 -2
|
@@ -7,6 +7,7 @@ import { logStorage } from '../log-storage.service.js';
|
|
|
7
7
|
import { eventBus, EventTypes } from '../event-bus.service.js';
|
|
8
8
|
import { hubManager } from '../hub-manager.service.js';
|
|
9
9
|
import { MCP_HUB_LITE_SERVER } from '../../models/system-tools.constants.js';
|
|
10
|
+
import { configManager } from '../../config/config-manager.js';
|
|
10
11
|
import { ToolCache } from './tool-cache.js';
|
|
11
12
|
import { getCompositeKey } from '../../utils/composite-key.js';
|
|
12
13
|
/**
|
|
@@ -94,212 +95,301 @@ export class McpConnectionManager {
|
|
|
94
95
|
* ```
|
|
95
96
|
*/
|
|
96
97
|
async connect(serverName, serverIndex, server) {
|
|
97
|
-
let serverInfo;
|
|
98
|
-
// Extract serverId at the very beginning for consistent usage in both try and catch blocks
|
|
99
98
|
const serverId = server.id || 'unknown';
|
|
100
99
|
const compositeKey = getCompositeKey(serverName, serverIndex);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
100
|
+
const { maxRetries, baseRetryDelay } = this.getRetryConfig();
|
|
101
|
+
// Retry loop for connection attempts
|
|
102
|
+
let lastError;
|
|
103
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
104
|
+
try {
|
|
105
|
+
// 1. Validate server configuration
|
|
106
|
+
this.validateServerConfig(server);
|
|
107
|
+
// 2. Log connection attempt
|
|
108
|
+
this.logConnectionAttempt(compositeKey, attempt, maxRetries);
|
|
109
|
+
// 3. Initialize starting status
|
|
110
|
+
this.initializeServerStatus(compositeKey);
|
|
111
|
+
// 4. Get server info
|
|
112
|
+
const serverInfo = this.getServerInfo(serverId);
|
|
113
|
+
// 5. Create transport and set up callbacks
|
|
114
|
+
const { transport, pid } = this.initializeTransport(server, serverInfo, compositeKey, serverName, serverIndex);
|
|
115
|
+
// 6. Establish client connection
|
|
116
|
+
const client = await this.establishClientConnection(transport);
|
|
117
|
+
// 7. Register connection
|
|
118
|
+
this.registerConnection(compositeKey, serverName, client, transport);
|
|
119
|
+
// 8. Update connected status
|
|
120
|
+
this.updateConnectedStatus(compositeKey, client, pid);
|
|
121
|
+
// 9. Publish connection events
|
|
122
|
+
this.publishConnectionEvents(serverName, serverIndex);
|
|
123
|
+
// 10. Refresh resources
|
|
124
|
+
await this.refreshServerResources(serverName, serverIndex, server.type);
|
|
125
|
+
return true;
|
|
118
126
|
}
|
|
119
|
-
|
|
120
|
-
|
|
127
|
+
catch (error) {
|
|
128
|
+
lastError = await this.handleConnectionError(error, compositeKey, attempt, maxRetries, baseRetryDelay);
|
|
121
129
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
130
|
+
}
|
|
131
|
+
// All retries failed
|
|
132
|
+
this.handleFinalFailure(compositeKey, serverName, serverIndex, lastError);
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Gets retry configuration from system settings.
|
|
137
|
+
*/
|
|
138
|
+
getRetryConfig() {
|
|
139
|
+
const maxRetries = configManager.getConfig().system.startup?.maxConnectRetries ?? 3;
|
|
140
|
+
const baseRetryDelay = configManager.getConfig().system.startup?.connectRetryDelay ?? 5000;
|
|
141
|
+
return { maxRetries, baseRetryDelay };
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Validates server configuration before connection.
|
|
145
|
+
*/
|
|
146
|
+
validateServerConfig(server) {
|
|
147
|
+
if (!server.id) {
|
|
148
|
+
throw new Error('Server ID is required');
|
|
149
|
+
}
|
|
150
|
+
if (server.type === 'stdio' && (!server.command || server.command.trim() === '')) {
|
|
151
|
+
throw new Error('STDIO server requires a valid command');
|
|
152
|
+
}
|
|
153
|
+
if ((server.type === 'sse' || server.type === 'streamable-http' || server.type === 'http') &&
|
|
154
|
+
(!server.url || server.url.trim() === '')) {
|
|
155
|
+
const displayType = server.type === 'http' ? 'streamable-http' : server.type;
|
|
156
|
+
throw new Error(`${displayType.toUpperCase()} server requires a valid URL`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Logs connection attempt information.
|
|
161
|
+
*/
|
|
162
|
+
logConnectionAttempt(compositeKey, attempt, maxRetries) {
|
|
163
|
+
logger.info(`Connecting to server [${compositeKey}] (attempt ${attempt}/${maxRetries})...`, LOG_MODULES.CONNECTION_MANAGER);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Initializes server status to starting state.
|
|
167
|
+
*/
|
|
168
|
+
initializeServerStatus(compositeKey) {
|
|
169
|
+
this.serverStatus.set(compositeKey, {
|
|
170
|
+
connected: false,
|
|
171
|
+
lastCheck: Date.now(),
|
|
172
|
+
toolsCount: 0,
|
|
173
|
+
resourcesCount: 0
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Gets server info by ID from hub manager.
|
|
178
|
+
*/
|
|
179
|
+
getServerInfo(serverId) {
|
|
180
|
+
const serverInfo = hubManager.getServerById(serverId);
|
|
181
|
+
if (!serverInfo) {
|
|
182
|
+
throw new Error(`Server not found for instance: ${serverId}`);
|
|
183
|
+
}
|
|
184
|
+
return serverInfo;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Creates transport and sets up all callbacks.
|
|
188
|
+
*/
|
|
189
|
+
initializeTransport(server, serverInfo, compositeKey, serverName, serverIndex) {
|
|
190
|
+
const readyPatterns = server.type === 'stdio' ? (serverInfo.config.template.readyPatterns ?? []) : undefined;
|
|
191
|
+
const readyTimeout = configManager.getConfig().system.startup?.readyTimeout ?? 120000;
|
|
192
|
+
const transport = TransportFactory.createTransport({
|
|
193
|
+
...server,
|
|
194
|
+
name: serverName
|
|
195
|
+
}, compositeKey, {
|
|
196
|
+
readyPatterns,
|
|
197
|
+
readyTimeout
|
|
198
|
+
});
|
|
199
|
+
// Set up message handler for notifications/message
|
|
200
|
+
transport.onmessage = (message) => {
|
|
201
|
+
if (getMcpCommDebugSetting()) {
|
|
202
|
+
const logMessage = formatMcpMessageForLogging(message);
|
|
203
|
+
logger.debug(`MCP message received: ${logMessage}`, LOG_MODULES.CONNECTION_MANAGER);
|
|
126
204
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// Communication debug logs: controlled by MCP_COMM_DEBUG environment variable
|
|
135
|
-
if (getMcpCommDebugSetting()) {
|
|
205
|
+
logNotificationMessage(message, serverName, compositeKey);
|
|
206
|
+
};
|
|
207
|
+
// Wrap send method for debug logging (if enabled)
|
|
208
|
+
if (getMcpCommDebugSetting()) {
|
|
209
|
+
const originalSend = transport.send;
|
|
210
|
+
transport.send = async (message, options) => {
|
|
211
|
+
try {
|
|
136
212
|
const logMessage = formatMcpMessageForLogging(message);
|
|
137
|
-
logger.debug(`MCP message
|
|
213
|
+
logger.debug(`MCP message sent: ${logMessage}`, LOG_MODULES.CONNECTION_MANAGER);
|
|
138
214
|
}
|
|
139
|
-
|
|
140
|
-
|
|
215
|
+
catch {
|
|
216
|
+
logger.debug(`MCP message sent: [Error formatting response]`, LOG_MODULES.CONNECTION_MANAGER);
|
|
217
|
+
}
|
|
218
|
+
return await originalSend.call(transport, message, options);
|
|
141
219
|
};
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
// Add log listeners
|
|
183
|
-
if ('onstdout' in transport) {
|
|
184
|
-
transport.onstdout = (data) => {
|
|
185
|
-
// Skip JSON-RPC communication to avoid log noise
|
|
186
|
-
const trimmedData = data.trim();
|
|
187
|
-
if (trimmedData) {
|
|
188
|
-
// Check if it's a valid JSON-RPC message
|
|
189
|
-
let isJsonRpc = false;
|
|
190
|
-
if (trimmedData.startsWith('{')) {
|
|
191
|
-
try {
|
|
192
|
-
const parsed = JSON.parse(trimmedData);
|
|
193
|
-
// Only consider it JSON-RPC if it has valid jsonrpc field
|
|
194
|
-
isJsonRpc =
|
|
195
|
-
typeof parsed.jsonrpc === 'string' &&
|
|
196
|
-
(parsed.jsonrpc === '2.0' || parsed.jsonrpc === '1.0');
|
|
197
|
-
}
|
|
198
|
-
catch {
|
|
199
|
-
// Not valid JSON, treat as log output
|
|
200
|
-
isJsonRpc = false;
|
|
201
|
-
}
|
|
220
|
+
}
|
|
221
|
+
// Handle transport close events
|
|
222
|
+
if ('onclose' in transport) {
|
|
223
|
+
transport.onclose = () => {
|
|
224
|
+
logger.info(`Transport closed for server [${compositeKey}]`, LOG_MODULES.CONNECTION_MANAGER);
|
|
225
|
+
const currentStatus = this.serverStatus.get(compositeKey);
|
|
226
|
+
if (currentStatus && (currentStatus.connected || !currentStatus.error)) {
|
|
227
|
+
this.serverStatus.set(compositeKey, {
|
|
228
|
+
connected: false,
|
|
229
|
+
lastCheck: Date.now(),
|
|
230
|
+
toolsCount: 0,
|
|
231
|
+
resourcesCount: 0,
|
|
232
|
+
error: 'Connection closed unexpectedly'
|
|
233
|
+
});
|
|
234
|
+
eventBus.publish(EventTypes.SERVER_STATUS_CHANGE, {
|
|
235
|
+
serverName,
|
|
236
|
+
serverIndex,
|
|
237
|
+
status: 'error',
|
|
238
|
+
error: 'Connection closed unexpectedly',
|
|
239
|
+
timestamp: Date.now()
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
// Add log listeners
|
|
245
|
+
if ('onstdout' in transport) {
|
|
246
|
+
transport.onstdout = (data) => {
|
|
247
|
+
const trimmedData = data.trim();
|
|
248
|
+
if (trimmedData) {
|
|
249
|
+
let isJsonRpc = false;
|
|
250
|
+
if (trimmedData.startsWith('{')) {
|
|
251
|
+
try {
|
|
252
|
+
const parsed = JSON.parse(trimmedData);
|
|
253
|
+
isJsonRpc =
|
|
254
|
+
typeof parsed.jsonrpc === 'string' &&
|
|
255
|
+
(parsed.jsonrpc === '2.0' || parsed.jsonrpc === '1.0');
|
|
202
256
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
logStorage.append(compositeKey, 'info', `[${serverName}] [STDOUT] ${data}`);
|
|
257
|
+
catch {
|
|
258
|
+
isJsonRpc = false;
|
|
206
259
|
}
|
|
207
260
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
logStorage.append(compositeKey, 'error', `[${serverName}] [STDERR] ${data}`);
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
const client = new Client({
|
|
217
|
-
name: MCP_HUB_LITE_SERVER,
|
|
218
|
-
version: getAppVersion()
|
|
219
|
-
}, {
|
|
220
|
-
capabilities: {}
|
|
221
|
-
});
|
|
222
|
-
await client.connect(transport);
|
|
223
|
-
this.clients.set(compositeKey, client);
|
|
224
|
-
this.transports.set(compositeKey, transport);
|
|
225
|
-
this._toolCache.setNameMapping(serverName, compositeKey);
|
|
226
|
-
// Register composite key for this server name
|
|
227
|
-
if (!this.serverNameToCompositeKeys.has(serverName)) {
|
|
228
|
-
this.serverNameToCompositeKeys.set(serverName, new Set());
|
|
229
|
-
}
|
|
230
|
-
this.serverNameToCompositeKeys.get(serverName).add(compositeKey);
|
|
231
|
-
// Get PID if available (only for stdio transport)
|
|
232
|
-
let pid;
|
|
233
|
-
if ('pid' in transport && typeof transport.pid === 'number') {
|
|
234
|
-
pid = transport.pid;
|
|
235
|
-
}
|
|
236
|
-
// Get server version
|
|
237
|
-
const clientServerInfo = client.getServerVersion();
|
|
238
|
-
const serverVersion = clientServerInfo?.version || clientServerInfo?.name;
|
|
239
|
-
this.serverStatus.set(compositeKey, {
|
|
240
|
-
connected: true,
|
|
241
|
-
lastCheck: Date.now(),
|
|
242
|
-
toolsCount: 0,
|
|
243
|
-
resourcesCount: 0,
|
|
244
|
-
pid: pid,
|
|
245
|
-
startTime: Date.now(),
|
|
246
|
-
version: serverVersion
|
|
247
|
-
});
|
|
248
|
-
logger.info(`Connected to server [${compositeKey}]`, LOG_MODULES.CONNECTION_MANAGER);
|
|
249
|
-
// Publish server connected event
|
|
250
|
-
eventBus.publish(EventTypes.SERVER_CONNECTED, {
|
|
251
|
-
serverName,
|
|
252
|
-
serverIndex,
|
|
253
|
-
status: 'online',
|
|
254
|
-
timestamp: Date.now()
|
|
255
|
-
});
|
|
256
|
-
// Publish server status change event
|
|
257
|
-
eventBus.publish(EventTypes.SERVER_STATUS_CHANGE, {
|
|
258
|
-
serverName,
|
|
259
|
-
serverIndex,
|
|
260
|
-
status: 'online',
|
|
261
|
-
timestamp: Date.now()
|
|
262
|
-
});
|
|
263
|
-
// Fetch tools and resources immediately (only for bidirectional transports)
|
|
264
|
-
if (server.type !== 'sse') {
|
|
265
|
-
const tools = await this.refreshTools(serverName, serverIndex);
|
|
266
|
-
const resources = await this.refreshResources(serverName, serverIndex);
|
|
267
|
-
// Publish tools and resources updated event
|
|
268
|
-
eventBus.publish(EventTypes.TOOLS_UPDATED, {
|
|
269
|
-
serverName,
|
|
270
|
-
serverIndex,
|
|
271
|
-
tools
|
|
272
|
-
});
|
|
273
|
-
eventBus.publish(EventTypes.RESOURCES_UPDATED, {
|
|
274
|
-
serverName,
|
|
275
|
-
serverIndex,
|
|
276
|
-
resources
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
else {
|
|
280
|
-
logger.info('SSE transport is unidirectional, skipping tool/resource refresh', LOG_MODULES.CONNECTION_MANAGER);
|
|
281
|
-
}
|
|
282
|
-
return true;
|
|
261
|
+
if (!isJsonRpc) {
|
|
262
|
+
logStorage.append(compositeKey, 'info', `[${serverName}] [STDOUT] ${data}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
};
|
|
283
266
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
267
|
+
if ('onstderr' in transport) {
|
|
268
|
+
transport.onstderr = (data) => {
|
|
269
|
+
logStorage.append(compositeKey, 'error', `[${serverName}] [STDERR] ${data}`);
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
// Get PID if available (only for stdio transport)
|
|
273
|
+
let pid;
|
|
274
|
+
if ('pid' in transport && typeof transport.pid === 'number') {
|
|
275
|
+
pid = transport.pid;
|
|
276
|
+
}
|
|
277
|
+
return { transport, pid };
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Creates Client and connects to transport.
|
|
281
|
+
*/
|
|
282
|
+
async establishClientConnection(transport) {
|
|
283
|
+
const client = new Client({
|
|
284
|
+
name: MCP_HUB_LITE_SERVER,
|
|
285
|
+
version: getAppVersion()
|
|
286
|
+
}, {
|
|
287
|
+
capabilities: {}
|
|
288
|
+
});
|
|
289
|
+
await client.connect(transport);
|
|
290
|
+
return client;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Registers client, transport, and updates name mappings.
|
|
294
|
+
*/
|
|
295
|
+
registerConnection(compositeKey, serverName, client, transport) {
|
|
296
|
+
this.clients.set(compositeKey, client);
|
|
297
|
+
this.transports.set(compositeKey, transport);
|
|
298
|
+
this._toolCache.setNameMapping(serverName, compositeKey);
|
|
299
|
+
if (!this.serverNameToCompositeKeys.has(serverName)) {
|
|
300
|
+
this.serverNameToCompositeKeys.set(serverName, new Set());
|
|
301
|
+
}
|
|
302
|
+
this.serverNameToCompositeKeys.get(serverName).add(compositeKey);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Updates server status to connected state.
|
|
306
|
+
*/
|
|
307
|
+
updateConnectedStatus(compositeKey, client, pid) {
|
|
308
|
+
const clientServerInfo = client.getServerVersion();
|
|
309
|
+
const serverVersion = clientServerInfo?.version || clientServerInfo?.name;
|
|
310
|
+
this.serverStatus.set(compositeKey, {
|
|
311
|
+
connected: true,
|
|
312
|
+
lastCheck: Date.now(),
|
|
313
|
+
toolsCount: 0,
|
|
314
|
+
resourcesCount: 0,
|
|
315
|
+
pid: pid,
|
|
316
|
+
startTime: Date.now(),
|
|
317
|
+
version: serverVersion
|
|
318
|
+
});
|
|
319
|
+
logger.info(`Connected to server [${compositeKey}]`, LOG_MODULES.CONNECTION_MANAGER);
|
|
320
|
+
return serverVersion;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Publishes connection events.
|
|
324
|
+
*/
|
|
325
|
+
publishConnectionEvents(serverName, serverIndex) {
|
|
326
|
+
eventBus.publish(EventTypes.SERVER_CONNECTED, {
|
|
327
|
+
serverName,
|
|
328
|
+
serverIndex,
|
|
329
|
+
status: 'online',
|
|
330
|
+
timestamp: Date.now()
|
|
331
|
+
});
|
|
332
|
+
eventBus.publish(EventTypes.SERVER_STATUS_CHANGE, {
|
|
333
|
+
serverName,
|
|
334
|
+
serverIndex,
|
|
335
|
+
status: 'online',
|
|
336
|
+
timestamp: Date.now()
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Refreshes server tools and resources (only for bidirectional transports).
|
|
341
|
+
*/
|
|
342
|
+
async refreshServerResources(serverName, serverIndex, serverType) {
|
|
343
|
+
if (serverType === 'sse') {
|
|
344
|
+
logger.info('SSE transport is unidirectional, skipping tool/resource refresh', LOG_MODULES.CONNECTION_MANAGER);
|
|
345
|
+
return;
|
|
302
346
|
}
|
|
347
|
+
const tools = await this.refreshTools(serverName, serverIndex);
|
|
348
|
+
const resources = await this.refreshResources(serverName, serverIndex);
|
|
349
|
+
eventBus.publish(EventTypes.TOOLS_UPDATED, {
|
|
350
|
+
serverName,
|
|
351
|
+
serverIndex,
|
|
352
|
+
tools
|
|
353
|
+
});
|
|
354
|
+
eventBus.publish(EventTypes.RESOURCES_UPDATED, {
|
|
355
|
+
serverName,
|
|
356
|
+
serverIndex,
|
|
357
|
+
resources
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Handles connection error with logging and retry delay.
|
|
362
|
+
*/
|
|
363
|
+
async handleConnectionError(error, compositeKey, attempt, maxRetries, baseRetryDelay) {
|
|
364
|
+
const lastError = error instanceof Error ? error : new Error(String(error));
|
|
365
|
+
logger.warn(`Connection attempt ${attempt}/${maxRetries} failed for ${compositeKey}: ${lastError.message}`, LOG_MODULES.CONNECTION_MANAGER);
|
|
366
|
+
if (attempt < maxRetries) {
|
|
367
|
+
const retryDelay = baseRetryDelay * Math.pow(2, attempt - 1);
|
|
368
|
+
logger.info(`Retrying connection to ${compositeKey} in ${retryDelay}ms...`, LOG_MODULES.CONNECTION_MANAGER);
|
|
369
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
370
|
+
}
|
|
371
|
+
return lastError;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Handles final failure after all retries exhausted.
|
|
375
|
+
*/
|
|
376
|
+
handleFinalFailure(compositeKey, serverName, serverIndex, lastError) {
|
|
377
|
+
const errorMessage = lastError?.message || 'Connection failed after all retries';
|
|
378
|
+
logger.error(`Failed to connect to server ${compositeKey} after retries:`, lastError, LOG_MODULES.CONNECTION_MANAGER);
|
|
379
|
+
this.serverStatus.set(compositeKey, {
|
|
380
|
+
connected: false,
|
|
381
|
+
error: errorMessage,
|
|
382
|
+
lastCheck: Date.now(),
|
|
383
|
+
toolsCount: 0,
|
|
384
|
+
resourcesCount: 0
|
|
385
|
+
});
|
|
386
|
+
eventBus.publish(EventTypes.SERVER_STATUS_CHANGE, {
|
|
387
|
+
serverName,
|
|
388
|
+
serverIndex,
|
|
389
|
+
status: 'error',
|
|
390
|
+
error: errorMessage,
|
|
391
|
+
timestamp: Date.now()
|
|
392
|
+
});
|
|
303
393
|
}
|
|
304
394
|
/**
|
|
305
395
|
* Disconnects from an MCP server and cleans up all associated resources.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gateway.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/gateway/gateway.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"gateway.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/gateway/gateway.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAepE,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE5D,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,SAAS,CAAqC;IACtD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;;IAOpC,OAAO,CAAC,wBAAwB;IA2BhC,OAAO,CAAC,gBAAgB;IAuDxB;;;;;OAKG;IACI,wBAAwB,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,GAAG,KAAK,CAAC,WAAW,CAAC;IAIvF;;;;;OAKG;IACI,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM;IAI5C;;;;;OAKG;IACI,kBAAkB,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM;IAIpD;;;;OAIG;IACI,sBAAsB,IAAI,SAAS;IAI1C;;;;OAIG;IACU,KAAK;CAKnB;AAED,eAAO,MAAM,OAAO,gBAAuB,CAAC"}
|
|
@@ -20,6 +20,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
20
20
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
21
21
|
import { logger } from '../../utils/index.js';
|
|
22
22
|
import { LOG_MODULES } from '../../utils/logger/log-modules.js';
|
|
23
|
+
import { getGatewayDebugSetting } from '../../utils/json-utils.js';
|
|
23
24
|
import { MCP_HUB_LITE_SERVER } from '../../models/system-tools.constants.js';
|
|
24
25
|
import { registerInitializeHandlers, registerResourcesHandlers, registerSystemToolsHandlers, registerCallToolHandler } from './request-handlers/index.js';
|
|
25
26
|
import { getAppVersion } from '../../utils/version.js';
|
|
@@ -34,7 +35,9 @@ export class GatewayService {
|
|
|
34
35
|
this.server = this.createServerWithHandlers();
|
|
35
36
|
}
|
|
36
37
|
createServerWithHandlers() {
|
|
37
|
-
|
|
38
|
+
if (getGatewayDebugSetting()) {
|
|
39
|
+
logger.debug('Creating new MCP server with handlers', LOG_MODULES.GATEWAY_SERVICE);
|
|
40
|
+
}
|
|
38
41
|
const server = new McpServer({
|
|
39
42
|
name: MCP_HUB_LITE_SERVER,
|
|
40
43
|
version: this.appVersion
|
|
@@ -44,19 +47,29 @@ export class GatewayService {
|
|
|
44
47
|
resources: {}
|
|
45
48
|
}
|
|
46
49
|
});
|
|
47
|
-
|
|
50
|
+
if (getGatewayDebugSetting()) {
|
|
51
|
+
logger.debug('MCP server created successfully', LOG_MODULES.GATEWAY_SERVICE);
|
|
52
|
+
}
|
|
48
53
|
this.registerHandlers(server);
|
|
49
|
-
|
|
54
|
+
if (getGatewayDebugSetting()) {
|
|
55
|
+
logger.debug('Handlers registered successfully on MCP server', LOG_MODULES.GATEWAY_SERVICE);
|
|
56
|
+
}
|
|
50
57
|
return server;
|
|
51
58
|
}
|
|
52
59
|
registerHandlers(server) {
|
|
53
|
-
|
|
60
|
+
if (getGatewayDebugSetting()) {
|
|
61
|
+
logger.debug('Registering handlers on MCP server', LOG_MODULES.GATEWAY_SERVICE);
|
|
62
|
+
}
|
|
54
63
|
// Local toolMap for this connection
|
|
55
64
|
const toolMap = new Map();
|
|
56
|
-
|
|
65
|
+
if (getGatewayDebugSetting()) {
|
|
66
|
+
logger.debug('Created local toolMap for connection', LOG_MODULES.GATEWAY_SERVICE);
|
|
67
|
+
}
|
|
57
68
|
try {
|
|
58
69
|
registerInitializeHandlers(server);
|
|
59
|
-
|
|
70
|
+
if (getGatewayDebugSetting()) {
|
|
71
|
+
logger.debug('Initialize handlers registered successfully', LOG_MODULES.GATEWAY_SERVICE);
|
|
72
|
+
}
|
|
60
73
|
}
|
|
61
74
|
catch (error) {
|
|
62
75
|
logger.error('Failed to register initialize handlers:', error, LOG_MODULES.GATEWAY_SERVICE);
|
|
@@ -64,7 +77,9 @@ export class GatewayService {
|
|
|
64
77
|
}
|
|
65
78
|
try {
|
|
66
79
|
registerResourcesHandlers(server);
|
|
67
|
-
|
|
80
|
+
if (getGatewayDebugSetting()) {
|
|
81
|
+
logger.debug('Resources handlers registered successfully', LOG_MODULES.GATEWAY_SERVICE);
|
|
82
|
+
}
|
|
68
83
|
}
|
|
69
84
|
catch (error) {
|
|
70
85
|
logger.error('Failed to register resources handlers:', error, LOG_MODULES.GATEWAY_SERVICE);
|
|
@@ -72,7 +87,9 @@ export class GatewayService {
|
|
|
72
87
|
}
|
|
73
88
|
try {
|
|
74
89
|
registerSystemToolsHandlers(server);
|
|
75
|
-
|
|
90
|
+
if (getGatewayDebugSetting()) {
|
|
91
|
+
logger.debug('System tools handlers registered successfully', LOG_MODULES.GATEWAY_SERVICE);
|
|
92
|
+
}
|
|
76
93
|
}
|
|
77
94
|
catch (error) {
|
|
78
95
|
logger.error('Failed to register system tools handlers:', error, LOG_MODULES.GATEWAY_SERVICE);
|
|
@@ -80,13 +97,17 @@ export class GatewayService {
|
|
|
80
97
|
}
|
|
81
98
|
try {
|
|
82
99
|
registerCallToolHandler(server, toolMap);
|
|
83
|
-
|
|
100
|
+
if (getGatewayDebugSetting()) {
|
|
101
|
+
logger.debug('Call tool handler registered successfully', LOG_MODULES.GATEWAY_SERVICE);
|
|
102
|
+
}
|
|
84
103
|
}
|
|
85
104
|
catch (error) {
|
|
86
105
|
logger.error('Failed to register call tool handler:', error, LOG_MODULES.GATEWAY_SERVICE);
|
|
87
106
|
throw error;
|
|
88
107
|
}
|
|
89
|
-
|
|
108
|
+
if (getGatewayDebugSetting()) {
|
|
109
|
+
logger.debug('All handlers registered successfully', LOG_MODULES.GATEWAY_SERVICE);
|
|
110
|
+
}
|
|
90
111
|
}
|
|
91
112
|
/**
|
|
92
113
|
* Generates a unified list of gateway tools with proper naming and collision resolution.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"global-transport.d.ts","sourceRoot":"","sources":["../../../../../src/services/gateway/global-transport.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,6BAA6B,EAAE,MAAM,oDAAoD,CAAC;
|
|
1
|
+
{"version":3,"file":"global-transport.d.ts","sourceRoot":"","sources":["../../../../../src/services/gateway/global-transport.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,6BAA6B,EAAE,MAAM,oDAAoD,CAAC;AAUnG;;;;;;GAMG;AACH,wBAAsB,sBAAsB;;;GAqE3C"}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
8
8
|
import { gateway } from './gateway.service.js';
|
|
9
9
|
import { logger, LOG_MODULES } from '../../utils/logger/index.js';
|
|
10
|
-
import { stringifyForLogging, getMcpCommDebugSetting } from '../../utils/json-utils.js';
|
|
10
|
+
import { stringifyForLogging, getMcpCommDebugSetting, getGatewayDebugSetting } from '../../utils/json-utils.js';
|
|
11
11
|
import { formatMcpMessageForLogging, logNotificationMessage } from '../../utils/logger/log-output.js';
|
|
12
12
|
/**
|
|
13
13
|
* Creates a new MCP transport and server instance for a single request session.
|
|
@@ -21,14 +21,16 @@ export async function createSessionTransport() {
|
|
|
21
21
|
const server = gateway.createConnectionServer();
|
|
22
22
|
// Set up message logging (use empty string for sessionId in per-request mode)
|
|
23
23
|
transport.onmessage = (message) => {
|
|
24
|
-
logger.debug(`Session transport onmessage called with: ${stringifyForLogging(message)}`, LOG_MODULES.GATEWAY);
|
|
25
24
|
try {
|
|
26
25
|
if (getMcpCommDebugSetting()) {
|
|
26
|
+
logger.debug(`Session transport onmessage called with: ${stringifyForLogging(message)}`, LOG_MODULES.COMMUNICATION);
|
|
27
27
|
const logMessage = formatMcpMessageForLogging(message);
|
|
28
28
|
logger.debug(`MCP message received: ${logMessage}`, LOG_MODULES.COMMUNICATION);
|
|
29
29
|
}
|
|
30
30
|
logNotificationMessage(message, ''); // Empty sessionId for per-request
|
|
31
|
-
|
|
31
|
+
if (getGatewayDebugSetting()) {
|
|
32
|
+
logger.debug(`Session transport onmessage completed successfully`, LOG_MODULES.GATEWAY);
|
|
33
|
+
}
|
|
32
34
|
}
|
|
33
35
|
catch (error) {
|
|
34
36
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -51,10 +53,14 @@ export async function createSessionTransport() {
|
|
|
51
53
|
};
|
|
52
54
|
}
|
|
53
55
|
// Connect server to transport
|
|
54
|
-
|
|
56
|
+
if (getGatewayDebugSetting()) {
|
|
57
|
+
logger.debug('About to connect session server to transport', LOG_MODULES.GATEWAY);
|
|
58
|
+
}
|
|
55
59
|
try {
|
|
56
60
|
await server.connect(transport);
|
|
57
|
-
|
|
61
|
+
if (getGatewayDebugSetting()) {
|
|
62
|
+
logger.debug('MCP session transport initialized (per-request mode)', LOG_MODULES.GATEWAY);
|
|
63
|
+
}
|
|
58
64
|
}
|
|
59
65
|
catch (error) {
|
|
60
66
|
const errorMessage = error instanceof Error ? error.message : String(error);
|