@toolplex/client 0.1.1

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 (91) hide show
  1. package/LICENSE +98 -0
  2. package/README.md +112 -0
  3. package/dist/mcp-server/clientContext.d.ts +35 -0
  4. package/dist/mcp-server/clientContext.js +107 -0
  5. package/dist/mcp-server/index.d.ts +1 -0
  6. package/dist/mcp-server/index.js +22 -0
  7. package/dist/mcp-server/logging/telemetryLogger.d.ts +18 -0
  8. package/dist/mcp-server/logging/telemetryLogger.js +54 -0
  9. package/dist/mcp-server/policy/callToolObserver.d.ts +9 -0
  10. package/dist/mcp-server/policy/callToolObserver.js +25 -0
  11. package/dist/mcp-server/policy/feedbackPolicy.d.ts +27 -0
  12. package/dist/mcp-server/policy/feedbackPolicy.js +39 -0
  13. package/dist/mcp-server/policy/installObserver.d.ts +11 -0
  14. package/dist/mcp-server/policy/installObserver.js +35 -0
  15. package/dist/mcp-server/policy/playbookPolicy.d.ts +29 -0
  16. package/dist/mcp-server/policy/playbookPolicy.js +81 -0
  17. package/dist/mcp-server/policy/policyEnforcer.d.ts +57 -0
  18. package/dist/mcp-server/policy/policyEnforcer.js +105 -0
  19. package/dist/mcp-server/policy/serverPolicy.d.ts +39 -0
  20. package/dist/mcp-server/policy/serverPolicy.js +61 -0
  21. package/dist/mcp-server/promptsCache.d.ts +25 -0
  22. package/dist/mcp-server/promptsCache.js +51 -0
  23. package/dist/mcp-server/registry.d.ts +34 -0
  24. package/dist/mcp-server/registry.js +109 -0
  25. package/dist/mcp-server/serversCache.d.ts +53 -0
  26. package/dist/mcp-server/serversCache.js +100 -0
  27. package/dist/mcp-server/staticPrompts.d.ts +6 -0
  28. package/dist/mcp-server/staticPrompts.js +6 -0
  29. package/dist/mcp-server/toolDefinitionsCache.d.ts +33 -0
  30. package/dist/mcp-server/toolDefinitionsCache.js +67 -0
  31. package/dist/mcp-server/toolHandlers/callToolHandler.d.ts +3 -0
  32. package/dist/mcp-server/toolHandlers/callToolHandler.js +79 -0
  33. package/dist/mcp-server/toolHandlers/getServerConfigHandler.d.ts +3 -0
  34. package/dist/mcp-server/toolHandlers/getServerConfigHandler.js +69 -0
  35. package/dist/mcp-server/toolHandlers/initHandler.d.ts +3 -0
  36. package/dist/mcp-server/toolHandlers/initHandler.js +117 -0
  37. package/dist/mcp-server/toolHandlers/installServerHandler.d.ts +3 -0
  38. package/dist/mcp-server/toolHandlers/installServerHandler.js +151 -0
  39. package/dist/mcp-server/toolHandlers/listServersHandler.d.ts +2 -0
  40. package/dist/mcp-server/toolHandlers/listServersHandler.js +81 -0
  41. package/dist/mcp-server/toolHandlers/listToolsHandler.d.ts +3 -0
  42. package/dist/mcp-server/toolHandlers/listToolsHandler.js +112 -0
  43. package/dist/mcp-server/toolHandlers/logPlaybookUsageHandler.d.ts +3 -0
  44. package/dist/mcp-server/toolHandlers/logPlaybookUsageHandler.js +65 -0
  45. package/dist/mcp-server/toolHandlers/lookupEntityHandler.d.ts +3 -0
  46. package/dist/mcp-server/toolHandlers/lookupEntityHandler.js +112 -0
  47. package/dist/mcp-server/toolHandlers/savePlaybookHandler.d.ts +3 -0
  48. package/dist/mcp-server/toolHandlers/savePlaybookHandler.js +65 -0
  49. package/dist/mcp-server/toolHandlers/searchHandler.d.ts +3 -0
  50. package/dist/mcp-server/toolHandlers/searchHandler.js +114 -0
  51. package/dist/mcp-server/toolHandlers/serverManagerUtils.d.ts +2 -0
  52. package/dist/mcp-server/toolHandlers/serverManagerUtils.js +20 -0
  53. package/dist/mcp-server/toolHandlers/submitFeedbackHandler.d.ts +3 -0
  54. package/dist/mcp-server/toolHandlers/submitFeedbackHandler.js +70 -0
  55. package/dist/mcp-server/toolHandlers/uninstallServerHandler.d.ts +3 -0
  56. package/dist/mcp-server/toolHandlers/uninstallServerHandler.js +83 -0
  57. package/dist/mcp-server/toolplexApi/service.d.ts +32 -0
  58. package/dist/mcp-server/toolplexApi/service.js +222 -0
  59. package/dist/mcp-server/toolplexApi/types.d.ts +124 -0
  60. package/dist/mcp-server/toolplexApi/types.js +1 -0
  61. package/dist/mcp-server/toolplexServer.d.ts +3 -0
  62. package/dist/mcp-server/toolplexServer.js +249 -0
  63. package/dist/mcp-server/tools.d.ts +2 -0
  64. package/dist/mcp-server/tools.js +13 -0
  65. package/dist/mcp-server/utils/initServerManagers.d.ts +6 -0
  66. package/dist/mcp-server/utils/initServerManagers.js +31 -0
  67. package/dist/mcp-server/utils/resultAnnotators.d.ts +23 -0
  68. package/dist/mcp-server/utils/resultAnnotators.js +50 -0
  69. package/dist/mcp-server/utils/runtimeCheck.d.ts +4 -0
  70. package/dist/mcp-server/utils/runtimeCheck.js +30 -0
  71. package/dist/server-manager/index.d.ts +1 -0
  72. package/dist/server-manager/index.js +8 -0
  73. package/dist/server-manager/serverManager.d.ts +37 -0
  74. package/dist/server-manager/serverManager.js +419 -0
  75. package/dist/server-manager/stdioServer.d.ts +9 -0
  76. package/dist/server-manager/stdioServer.js +136 -0
  77. package/dist/server-manager/stdioTransportProtocol.d.ts +31 -0
  78. package/dist/server-manager/stdioTransportProtocol.js +67 -0
  79. package/dist/shared/enhancedPath.d.ts +7 -0
  80. package/dist/shared/enhancedPath.js +52 -0
  81. package/dist/shared/fileLogger.d.ts +13 -0
  82. package/dist/shared/fileLogger.js +66 -0
  83. package/dist/shared/mcpServerTypes.d.ts +398 -0
  84. package/dist/shared/mcpServerTypes.js +148 -0
  85. package/dist/shared/serverManagerTypes.d.ts +179 -0
  86. package/dist/shared/serverManagerTypes.js +73 -0
  87. package/dist/shared/stdioServerManagerClient.d.ts +12 -0
  88. package/dist/shared/stdioServerManagerClient.js +96 -0
  89. package/dist/version.d.ts +1 -0
  90. package/dist/version.js +1 -0
  91. package/package.json +70 -0
@@ -0,0 +1,419 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
+ import { StdioClientTransport, } from '@modelcontextprotocol/sdk/client/stdio.js';
3
+ import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
4
+ import * as fs from 'fs/promises';
5
+ import * as path from 'path';
6
+ import which from 'which';
7
+ import { FileLogger } from '../shared/fileLogger.js';
8
+ import envPaths from 'env-paths';
9
+ import { getEnhancedPath } from '../shared/enhancedPath.js';
10
+ const logger = FileLogger;
11
+ export class ServerManager {
12
+ constructor() {
13
+ this.config = {};
14
+ // Track ongoing installations to prevent race conditions
15
+ this.installationPromises = new Map();
16
+ // Add a file lock mechanism to prevent concurrent writes
17
+ this.configLock = Promise.resolve();
18
+ this.sessions = new Map();
19
+ this.tools = new Map();
20
+ this.serverNames = new Map();
21
+ const paths = envPaths('ToolPlex', { suffix: '' });
22
+ this.configPath = path.join(paths.data, 'server_config.json');
23
+ }
24
+ async loadConfig() {
25
+ try {
26
+ const data = await fs.readFile(this.configPath, 'utf-8');
27
+ await logger.debug(`Loaded config from ${this.configPath}`);
28
+ const allConfig = JSON.parse(data);
29
+ // Validate the config structure
30
+ if (typeof allConfig !== 'object' || allConfig === null) {
31
+ await logger.warn('Invalid config format, using empty config');
32
+ return {};
33
+ }
34
+ const config = {};
35
+ for (const [serverId, serverConfig] of Object.entries(allConfig)) {
36
+ if (typeof serverConfig === 'object' && serverConfig !== null) {
37
+ config[serverId] = serverConfig;
38
+ }
39
+ else {
40
+ await logger.warn(`Invalid server config for ${serverId}, skipping`);
41
+ }
42
+ }
43
+ return config;
44
+ }
45
+ catch (error) {
46
+ let errorMessage = 'Unknown error occurred';
47
+ if (error instanceof Error)
48
+ errorMessage = error.message;
49
+ await logger.debug(`No existing config found at ${this.configPath}: ${errorMessage}`);
50
+ // If the file exists but is malformed, back it up and start fresh
51
+ try {
52
+ await fs.access(this.configPath);
53
+ const backupPath = this.configPath + '.backup.' + Date.now();
54
+ await fs.copyFile(this.configPath, backupPath);
55
+ await logger.warn(`Malformed config backed up to ${backupPath}`);
56
+ }
57
+ catch {
58
+ // File doesn't exist, which is fine
59
+ }
60
+ return {};
61
+ }
62
+ }
63
+ async saveConfig(config) {
64
+ // Use a lock to prevent concurrent writes
65
+ this.configLock = this.configLock.then(async () => {
66
+ let existingConfig = {};
67
+ try {
68
+ const data = await fs.readFile(this.configPath, 'utf-8');
69
+ existingConfig = JSON.parse(data);
70
+ // Validate the existing config structure
71
+ if (typeof existingConfig !== 'object' || existingConfig === null) {
72
+ await logger.warn('Invalid existing config format, using empty config');
73
+ existingConfig = {};
74
+ }
75
+ }
76
+ catch (error) {
77
+ // Config file doesn't exist or is invalid, use empty config
78
+ await logger.debug(`Could not read existing config: ${error}`);
79
+ existingConfig = {};
80
+ }
81
+ const mergedConfig = {
82
+ ...existingConfig,
83
+ ...config,
84
+ };
85
+ // Validate the merged config before writing
86
+ try {
87
+ const testJson = JSON.stringify(mergedConfig, null, 2);
88
+ JSON.parse(testJson); // This will throw if invalid
89
+ }
90
+ catch (error) {
91
+ throw new Error(`Invalid config structure would be written: ${error}`);
92
+ }
93
+ await fs.mkdir(path.dirname(this.configPath), { recursive: true });
94
+ // Write to a temporary file first, then rename (atomic operation)
95
+ const tempPath = this.configPath + '.tmp';
96
+ try {
97
+ await fs.writeFile(tempPath, JSON.stringify(mergedConfig, null, 2));
98
+ await fs.rename(tempPath, this.configPath);
99
+ await logger.debug(`Saved config to ${this.configPath}`);
100
+ }
101
+ catch (error) {
102
+ // Clean up temp file if it exists
103
+ try {
104
+ await fs.unlink(tempPath);
105
+ }
106
+ catch {
107
+ // Ignore cleanup errors
108
+ }
109
+ throw error;
110
+ }
111
+ });
112
+ await this.configLock;
113
+ }
114
+ async initialize() {
115
+ await this.cleanup();
116
+ const succeeded = [];
117
+ const failures = {};
118
+ try {
119
+ await logger.info('Initializing ServerManager');
120
+ this.config = await this.loadConfig();
121
+ await logger.debug(`Loaded ${Object.keys(this.config).length} server configs`);
122
+ for (const [serverId, serverConfig] of Object.entries(this.config)) {
123
+ succeeded.push({
124
+ server_id: serverId,
125
+ server_name: serverConfig.server_name ?? serverId,
126
+ description: serverConfig.description ?? '',
127
+ });
128
+ }
129
+ }
130
+ catch (err) {
131
+ const errorMessage = err.message || String(err);
132
+ await logger.error(`Failed to initialize: ${errorMessage}`);
133
+ }
134
+ return { succeeded, failures };
135
+ }
136
+ async getServerName(serverId) {
137
+ await logger.debug(`Getting name for server ${serverId}`);
138
+ return this.serverNames.get(serverId) || serverId;
139
+ }
140
+ async connectWithHandshakeTimeout(client, transport, ms = 30000) {
141
+ let connectTimeout;
142
+ let listToolsTimeout;
143
+ try {
144
+ // Race connect() with timeout
145
+ await Promise.race([
146
+ client.connect(transport),
147
+ new Promise((_, reject) => {
148
+ connectTimeout = setTimeout(() => reject(new Error(`connect() timed out in ${ms} ms`)), ms);
149
+ }),
150
+ ]);
151
+ // Clear the connect timeout since it succeeded
152
+ clearTimeout(connectTimeout);
153
+ // Race listTools() with timeout
154
+ const result = await Promise.race([
155
+ client.listTools(),
156
+ new Promise((_, reject) => {
157
+ listToolsTimeout = setTimeout(() => reject(new Error(`listTools() timed out in ${ms} ms`)), ms);
158
+ }),
159
+ ]);
160
+ clearTimeout(listToolsTimeout);
161
+ return result;
162
+ }
163
+ catch (error) {
164
+ // Clean up timeouts on error
165
+ if (connectTimeout)
166
+ clearTimeout(connectTimeout);
167
+ if (listToolsTimeout)
168
+ clearTimeout(listToolsTimeout);
169
+ throw error;
170
+ }
171
+ }
172
+ async install(serverId, serverName, description, config) {
173
+ await logger.info(`Installing server ${serverId} (${serverName})`);
174
+ await logger.debug(`Server config: ${JSON.stringify(config)}`);
175
+ // Check if there's already an ongoing installation for this server
176
+ const existingInstall = this.installationPromises.get(serverId);
177
+ if (existingInstall) {
178
+ await logger.debug(`Installation already in progress for ${serverId}, waiting...`);
179
+ await existingInstall;
180
+ return;
181
+ }
182
+ // Create the installation promise
183
+ const installPromise = this.performInstall(serverId, serverName, description, config);
184
+ this.installationPromises.set(serverId, installPromise);
185
+ try {
186
+ await installPromise;
187
+ }
188
+ finally {
189
+ // Always clean up the promise from the map
190
+ this.installationPromises.delete(serverId);
191
+ }
192
+ }
193
+ async performInstall(serverId, serverName, description, config) {
194
+ if (this.sessions.has(serverId)) {
195
+ await logger.debug(`Server ${serverId} already exists, removing first`);
196
+ await this.removeServer(serverId);
197
+ }
198
+ let transport;
199
+ if (config.transport === 'sse') {
200
+ if (!config.url)
201
+ throw new Error('URL is required for SSE transport');
202
+ transport = new SSEClientTransport(new URL(config.url));
203
+ }
204
+ else if (config.transport === 'stdio') {
205
+ if (!config.command)
206
+ throw new Error('Command is required for stdio transport');
207
+ const enhancedPath = getEnhancedPath();
208
+ let resolvedCommand = which.sync(config.command, {
209
+ path: enhancedPath,
210
+ nothrow: true,
211
+ });
212
+ if (!resolvedCommand) {
213
+ // Fallback to supplied command
214
+ resolvedCommand = config.command;
215
+ }
216
+ const serverParams = {
217
+ command: resolvedCommand,
218
+ args: config.args || [],
219
+ env: {
220
+ ...process.env,
221
+ PATH: enhancedPath,
222
+ ...(config.env || {}),
223
+ },
224
+ stderr: 'pipe',
225
+ };
226
+ transport = new StdioClientTransport(serverParams);
227
+ }
228
+ else {
229
+ throw new Error(`Invalid transport type: ${config.transport}`);
230
+ }
231
+ const client = new Client({ name: serverId, version: '1.0.0' }, { capabilities: { prompts: {}, resources: {}, tools: {} } });
232
+ try {
233
+ const toolsResponse = await this.connectWithHandshakeTimeout(client, transport, 30000);
234
+ const tools = toolsResponse.tools || [];
235
+ this.sessions.set(serverId, client);
236
+ this.tools.set(serverId, tools);
237
+ this.serverNames.set(serverId, serverName);
238
+ const updatedEntry = {
239
+ ...config,
240
+ server_name: serverName,
241
+ description,
242
+ };
243
+ const currentConfig = await this.loadConfig();
244
+ await this.saveConfig({
245
+ ...currentConfig,
246
+ [serverId]: updatedEntry,
247
+ });
248
+ this.config[serverId] = updatedEntry;
249
+ await logger.info(`Successfully installed server ${serverId} with ${tools.length} tools`);
250
+ }
251
+ catch (err) {
252
+ // Clean up on failure
253
+ this.sessions.delete(serverId);
254
+ this.tools.delete(serverId);
255
+ this.serverNames.delete(serverId);
256
+ // Close transport if it was created
257
+ if (client && client.transport) {
258
+ try {
259
+ await client.transport.close();
260
+ }
261
+ catch (closeErr) {
262
+ await logger.warn(`Failed to close transport during cleanup: ${closeErr}`);
263
+ }
264
+ }
265
+ throw err;
266
+ }
267
+ }
268
+ async callTool(serverId, toolName,
269
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
270
+ arguments_, timeout = 60000
271
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
272
+ ) {
273
+ // Check for ongoing installation before attempting to install
274
+ const existingInstall = this.installationPromises.get(serverId);
275
+ if (existingInstall) {
276
+ await logger.debug(`Waiting for ongoing installation of ${serverId}...`);
277
+ await existingInstall;
278
+ }
279
+ if (!this.sessions.has(serverId)) {
280
+ const config = this.config[serverId];
281
+ if (!config)
282
+ throw new Error(`No config found for server ${serverId}`);
283
+ const name = config.server_name || serverId;
284
+ const description = config.description || '';
285
+ await this.install(serverId, name, description, config);
286
+ }
287
+ const client = this.sessions.get(serverId);
288
+ if (!client)
289
+ throw new Error(`Server ${serverId} is not initialized`);
290
+ let watchdogTimer;
291
+ let didTimeout = false;
292
+ const watchdog = new Promise((_, reject) => {
293
+ watchdogTimer = setTimeout(async () => {
294
+ didTimeout = true;
295
+ await logger.error(`[WATCHDOG] Tool call to ${toolName} on server ${serverId} timed out after ${timeout}ms. Removing server.`);
296
+ await this.removeServer(serverId);
297
+ reject(new Error(`Tool call timed out after ${timeout}ms`));
298
+ }, timeout);
299
+ });
300
+ try {
301
+ const result = (await Promise.race([
302
+ client.callTool({ name: toolName, arguments: arguments_ }),
303
+ watchdog,
304
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
305
+ ]));
306
+ if (watchdogTimer) {
307
+ clearTimeout(watchdogTimer);
308
+ watchdogTimer = undefined;
309
+ }
310
+ return result.content;
311
+ }
312
+ catch (err) {
313
+ if (watchdogTimer) {
314
+ clearTimeout(watchdogTimer);
315
+ watchdogTimer = undefined;
316
+ }
317
+ if (!didTimeout) {
318
+ await logger.error(`callTool failed for ${toolName} on ${serverId}: ${String(err)}`);
319
+ await this.removeServer(serverId);
320
+ }
321
+ throw err;
322
+ }
323
+ }
324
+ async uninstall(serverId) {
325
+ // Wait for any ongoing installation to complete before uninstalling
326
+ const existingInstall = this.installationPromises.get(serverId);
327
+ if (existingInstall) {
328
+ await logger.debug(`Waiting for ongoing installation of ${serverId} before uninstalling...`);
329
+ try {
330
+ await existingInstall;
331
+ }
332
+ catch (err) {
333
+ // Installation failed, continue with uninstall
334
+ await logger.debug(`Installation failed, continuing with uninstall: ${err}`);
335
+ }
336
+ }
337
+ // Remove the server from memory
338
+ await this.removeServer(serverId);
339
+ // Remove the server from the config file
340
+ let config = {};
341
+ try {
342
+ const data = await fs.readFile(this.configPath, 'utf-8');
343
+ config = JSON.parse(data);
344
+ }
345
+ catch (error) {
346
+ // If config file doesn't exist, nothing to do
347
+ await logger.debug(`Could not read existing config for uninstall: ${error}`);
348
+ return;
349
+ }
350
+ if (Object.prototype.hasOwnProperty.call(config, serverId)) {
351
+ delete config[serverId];
352
+ await fs.writeFile(this.configPath, JSON.stringify(config, null, 2));
353
+ await logger.debug(`Removed server ${serverId} from config at ${this.configPath}`);
354
+ }
355
+ // Remove from in-memory config as well
356
+ delete this.config[serverId];
357
+ }
358
+ async removeServer(serverId) {
359
+ const client = this.sessions.get(serverId);
360
+ if (client && client.transport) {
361
+ try {
362
+ await client.transport.close();
363
+ }
364
+ catch (err) {
365
+ await logger.warn(`Failed to close transport for ${serverId}: ${err}`);
366
+ }
367
+ }
368
+ this.sessions.delete(serverId);
369
+ this.tools.delete(serverId);
370
+ this.serverNames.delete(serverId);
371
+ }
372
+ async listServers() {
373
+ const config = await this.loadConfig();
374
+ return Object.entries(config).map(([id, cfg]) => ({
375
+ server_id: id,
376
+ server_name: cfg.server_name || id,
377
+ tool_count: this.tools.get(id)?.length || 0,
378
+ description: cfg.description || '',
379
+ }));
380
+ }
381
+ async listTools(serverId) {
382
+ // Check for ongoing installation
383
+ const existingInstall = this.installationPromises.get(serverId);
384
+ if (existingInstall) {
385
+ await logger.debug(`Waiting for ongoing installation of ${serverId}...`);
386
+ await existingInstall;
387
+ }
388
+ if (!this.tools.has(serverId)) {
389
+ const config = this.config[serverId];
390
+ if (!config)
391
+ throw new Error(`No config for server ${serverId}`);
392
+ await this.install(serverId, config.server_name || serverId, config.description || '', config);
393
+ }
394
+ return this.tools.get(serverId) || [];
395
+ }
396
+ async getServerConfig(serverId) {
397
+ // Always reload config from disk to ensure up-to-date
398
+ const config = await this.loadConfig();
399
+ const serverConfig = config[serverId];
400
+ if (!serverConfig) {
401
+ throw new Error(`No config found for server ${serverId}`);
402
+ }
403
+ return serverConfig;
404
+ }
405
+ async cleanup() {
406
+ // Wait for all ongoing installations to complete
407
+ const ongoingInstalls = Array.from(this.installationPromises.values());
408
+ if (ongoingInstalls.length > 0) {
409
+ await logger.debug(`Waiting for ${ongoingInstalls.length} ongoing installations to complete...`);
410
+ await Promise.allSettled(ongoingInstalls);
411
+ }
412
+ // Clean up all sessions
413
+ for (const serverId of this.sessions.keys()) {
414
+ await this.removeServer(serverId);
415
+ }
416
+ // Clear the installation promises map
417
+ this.installationPromises.clear();
418
+ }
419
+ }
@@ -0,0 +1,9 @@
1
+ export declare class ServerManagerProtocol {
2
+ private transport;
3
+ private serverManager;
4
+ constructor();
5
+ start(): Promise<void>;
6
+ private safeSend;
7
+ private handleMessage;
8
+ private callMethod;
9
+ }
@@ -0,0 +1,136 @@
1
+ // stdioServer.ts
2
+ import { ServerManager } from './serverManager.js';
3
+ import { StdioTransport, } from './stdioTransportProtocol.js';
4
+ import { FileLogger } from '../shared/fileLogger.js';
5
+ import { CallToolParamsSchema, InstallParamsSchema, ListToolsParamsSchema, UninstallParamsSchema, } from '../shared/mcpServerTypes.js';
6
+ const logger = FileLogger;
7
+ export class ServerManagerProtocol {
8
+ constructor() {
9
+ this.serverManager = new ServerManager();
10
+ this.transport = new StdioTransport();
11
+ this.transport.setOnMessage(this.handleMessage.bind(this));
12
+ // Clean up on process exit
13
+ process.on('exit', async () => {
14
+ await logger.info('Process exit - cleaning up server manager');
15
+ await logger.flush();
16
+ await this.serverManager.cleanup();
17
+ });
18
+ process.on('SIGINT', async () => {
19
+ await logger.warn('SIGINT received - cleaning up server manager');
20
+ await logger.flush();
21
+ await this.serverManager.cleanup();
22
+ process.exit();
23
+ });
24
+ process.on('SIGTERM', async () => {
25
+ await logger.warn('SIGTERM received - cleaning up server manager');
26
+ await logger.flush();
27
+ await this.serverManager.cleanup();
28
+ process.exit();
29
+ });
30
+ }
31
+ async start() {
32
+ await logger.info('Starting ServerManagerProtocol transport');
33
+ await this.transport.start();
34
+ await logger.info('ServerManagerProtocol transport started successfully');
35
+ }
36
+ async safeSend(msg) {
37
+ try {
38
+ await this.transport.send(msg);
39
+ }
40
+ catch (e) {
41
+ process.stderr.write(`transport.send failed: ${e}\n`);
42
+ }
43
+ }
44
+ async handleMessage(message) {
45
+ try {
46
+ if (!('id' in message) || !('method' in message)) {
47
+ await this.safeSend({
48
+ jsonrpc: '2.0',
49
+ error: { code: -1001, message: 'Invalid Request' },
50
+ id: null,
51
+ });
52
+ return;
53
+ }
54
+ const req = message;
55
+ let result;
56
+ try {
57
+ result = await this.callMethod(req.method, req.params ?? {});
58
+ }
59
+ catch (err) {
60
+ await logger.error(`callMethod failed (${req.method}): ${err instanceof Error ? err.message : 'Unknown error'}`);
61
+ await this.safeSend({
62
+ jsonrpc: '2.0',
63
+ error: { code: -1000, message: err instanceof Error ? err.message : 'Server error' },
64
+ id: req.id,
65
+ });
66
+ return;
67
+ }
68
+ await this.safeSend({ jsonrpc: '2.0', result, id: req.id });
69
+ }
70
+ catch (fatal) {
71
+ process.stderr.write(`handleMessage fatal: ${String(fatal)}\n`);
72
+ }
73
+ }
74
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
+ async callMethod(method, params) {
76
+ switch (method) {
77
+ case 'initialize': {
78
+ await logger.info('Calling ServerManager.initialize()');
79
+ const { succeeded, failures } = await this.serverManager.initialize();
80
+ return { succeeded, failures };
81
+ }
82
+ case 'install': {
83
+ const install_params = InstallParamsSchema.parse(params);
84
+ await logger.info(`Installing server ${install_params.server_id}`);
85
+ await this.serverManager.install(install_params.server_id, install_params.server_name, install_params.description, install_params.config);
86
+ const server_name = await this.serverManager.getServerName(install_params.server_id);
87
+ return { server_id: install_params.server_id, server_name };
88
+ }
89
+ case 'list_servers': {
90
+ await logger.debug('Listing servers');
91
+ const servers = await this.serverManager.listServers();
92
+ return { servers };
93
+ }
94
+ case 'list_tools': {
95
+ const list_tools_params = ListToolsParamsSchema.parse(params);
96
+ if (!list_tools_params.server_id)
97
+ throw new Error('Missing server_id');
98
+ await logger.debug(`Listing tools for server ${list_tools_params.server_id}`);
99
+ const tools = await this.serverManager.listTools(list_tools_params.server_id);
100
+ const server_name = await this.serverManager.getServerName(list_tools_params.server_id);
101
+ return { server_id: list_tools_params.server_id, server_name, tools };
102
+ }
103
+ case 'get_server_config': {
104
+ if (!params || typeof params.server_id !== 'string') {
105
+ throw new Error('Missing or invalid server_id');
106
+ }
107
+ await logger.debug(`Getting config for server ${params.server_id}`);
108
+ // Just return the config directly
109
+ const config = await this.serverManager.getServerConfig(params.server_id);
110
+ return config;
111
+ }
112
+ case 'call_tool': {
113
+ const call_tool_params = CallToolParamsSchema.parse(params);
114
+ await logger.debug(`Calling tool ${call_tool_params.tool_name} on server ${call_tool_params.server_id}`);
115
+ const result = await this.serverManager.callTool(call_tool_params.server_id, call_tool_params.tool_name, call_tool_params.arguments, 60000 // 60s timeout
116
+ );
117
+ return { result };
118
+ }
119
+ case 'uninstall': {
120
+ const uninstall_params = UninstallParamsSchema.parse(params);
121
+ await logger.info(`Uninstalling server ${uninstall_params.server_id}`);
122
+ const server_name = await this.serverManager.getServerName(uninstall_params.server_id);
123
+ await this.serverManager.uninstall(uninstall_params.server_id);
124
+ return { server_id: uninstall_params.server_id, server_name };
125
+ }
126
+ case 'cleanup': {
127
+ await logger.info('Cleaning up server manager');
128
+ await this.serverManager.cleanup();
129
+ return {};
130
+ }
131
+ default:
132
+ await logger.error(`Method ${method} not found`);
133
+ throw new Error(`Method ${method} not found`);
134
+ }
135
+ }
136
+ }
@@ -0,0 +1,31 @@
1
+ import { Readable, Writable } from 'node:stream';
2
+ export interface JSONRPCMessage {
3
+ jsonrpc: '2.0';
4
+ error?: {
5
+ code: number;
6
+ message: string;
7
+ };
8
+ id: string | number | null;
9
+ }
10
+ export interface JSONRPCRequest extends JSONRPCMessage {
11
+ method: string;
12
+ params?: any;
13
+ }
14
+ export interface JSONRPCResponse extends JSONRPCMessage {
15
+ result?: any;
16
+ }
17
+ export declare class StdioTransport {
18
+ private _stdin;
19
+ private _stdout;
20
+ private rl;
21
+ private onmessage?;
22
+ private bufferChunks;
23
+ private isStarted;
24
+ private readonly dataHandler;
25
+ constructor(_stdin?: Readable, _stdout?: Writable);
26
+ start(): Promise<void>;
27
+ private processBuffer;
28
+ send(message: JSONRPCRequest | JSONRPCResponse): Promise<void>;
29
+ close(): Promise<void>;
30
+ setOnMessage(handler: (message: JSONRPCMessage) => void): void;
31
+ }
@@ -0,0 +1,67 @@
1
+ import * as readline from 'readline';
2
+ export class StdioTransport {
3
+ constructor(_stdin = process.stdin, _stdout = process.stdout) {
4
+ this._stdin = _stdin;
5
+ this._stdout = _stdout;
6
+ this.bufferChunks = [];
7
+ this.isStarted = false;
8
+ // Store bound methods to ensure proper cleanup
9
+ this.dataHandler = (chunk) => {
10
+ this.bufferChunks.push(chunk.toString());
11
+ this.processBuffer();
12
+ };
13
+ this.rl = readline.createInterface({
14
+ input: this._stdin,
15
+ output: this._stdout,
16
+ terminal: false,
17
+ });
18
+ }
19
+ async start() {
20
+ if (this.isStarted) {
21
+ return; // Prevent double initialization
22
+ }
23
+ this.isStarted = true;
24
+ this._stdin.on('data', this.dataHandler);
25
+ }
26
+ processBuffer() {
27
+ // Join all chunks and split by lines - more efficient than string concatenation
28
+ const fullBuffer = this.bufferChunks.join('');
29
+ const lines = fullBuffer.split('\n');
30
+ // Keep the last line in buffer if it's incomplete
31
+ const incompleteLine = lines.pop() || '';
32
+ // Clear chunks and store incomplete line
33
+ this.bufferChunks = incompleteLine ? [incompleteLine] : [];
34
+ for (const line of lines) {
35
+ if (!line.trim())
36
+ continue;
37
+ try {
38
+ const message = JSON.parse(line.trim());
39
+ if (message.error) {
40
+ process.stderr.write(`Server error: ${JSON.stringify(message.error)}\n`);
41
+ }
42
+ this.onmessage?.(message);
43
+ }
44
+ catch (error) {
45
+ process.stderr.write(`Failed to parse line: ${line}\n`);
46
+ process.stderr.write(`Parse error: ${error}\n`);
47
+ process.stderr.write(`Current buffer chunks: ${this.bufferChunks.length}\n`);
48
+ }
49
+ }
50
+ }
51
+ async send(message) {
52
+ const messageStr = JSON.stringify(message) + '\n';
53
+ this._stdout.write(messageStr);
54
+ }
55
+ async close() {
56
+ if (this.isStarted) {
57
+ this._stdin.removeListener('data', this.dataHandler);
58
+ this.isStarted = false;
59
+ }
60
+ this.bufferChunks = [];
61
+ this.onmessage = undefined;
62
+ this.rl.close();
63
+ }
64
+ setOnMessage(handler) {
65
+ this.onmessage = handler;
66
+ }
67
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Returns an enhanced PATH string by prepending common binary directories
3
+ * across platforms (Linux, macOS, Windows).
4
+ *
5
+ * Ensures no duplicates and checks for existence.
6
+ */
7
+ export declare function getEnhancedPath(): string;