@1mcp/agent 0.2.0 → 0.4.0

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.
@@ -1,19 +1,21 @@
1
- import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourceTemplatesRequestSchema, SubscribeRequestSchema, UnsubscribeRequestSchema, SetLevelRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
1
+ import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourceTemplatesRequestSchema, SubscribeRequestSchema, UnsubscribeRequestSchema, SetLevelRequestSchema, CallToolResultSchema, } from '@modelcontextprotocol/sdk/types.js';
2
2
  import logger from '../logger/logger.js';
3
3
  import { setLogLevel } from '../logger/logger.js';
4
- import { MCP_URI_SEPARATOR, MCP_SERVER_NAME } from '../constants.js';
4
+ import { MCP_URI_SEPARATOR, MCP_SERVER_NAME, ERROR_CODES } from '../constants.js';
5
5
  import { executeClientOperation } from '../clients/clientManager.js';
6
- import { ProxyError, parseUri, withErrorHandling } from '../utils/errorHandling.js';
6
+ import { parseUri, withErrorHandling } from '../utils/errorHandling.js';
7
+ import { MCPError } from '../utils/errorTypes.js';
8
+ import { filterClientsByTags } from '../utils/clientFiltering.js';
7
9
  /**
8
10
  * Sends a partial failure notification to inform clients about backend failures
9
11
  * @param server The MCP server instance
10
12
  * @param operation The operation that partially failed
11
13
  * @param failedClients Array of failed clients with error details
12
14
  */
13
- function sendPartialFailureNotification(server, operation, failedClients) {
15
+ function sendPartialFailureNotification(serverInfo, operation, failedClients) {
14
16
  if (failedClients.length === 0)
15
17
  return;
16
- server.notification({
18
+ serverInfo.server.notification({
17
19
  method: 'notifications/message',
18
20
  params: {
19
21
  level: 'warning',
@@ -32,40 +34,44 @@ function sendPartialFailureNotification(server, operation, failedClients) {
32
34
  * @param clients Record of client instances
33
35
  * @param server The MCP server instance
34
36
  * @param capabilities The server capabilities
37
+ * @param tags Array of tags to filter clients by
35
38
  */
36
- export function registerRequestHandlers(clients, server, capabilities) {
39
+ export function registerRequestHandlers(clients, serverInfo, capabilities) {
37
40
  // Register logging level handler
38
- server.setRequestHandler(SetLevelRequestSchema, async (request) => {
41
+ serverInfo.server.setRequestHandler(SetLevelRequestSchema, async (request) => {
39
42
  setLogLevel(request.params.level);
40
43
  return {};
41
44
  });
42
45
  // Register resource-related handlers if capability is available
43
46
  if (capabilities.resources) {
44
- registerResourceHandlers(clients, server);
47
+ registerResourceHandlers(clients, serverInfo);
45
48
  }
46
49
  // Register tool-related handlers if capability is available
47
50
  if (capabilities.tools) {
48
- registerToolHandlers(clients, server);
51
+ registerToolHandlers(clients, serverInfo);
49
52
  }
50
53
  // Register prompt-related handlers if capability is available
51
54
  if (capabilities.prompts) {
52
- registerPromptHandlers(clients, server);
55
+ registerPromptHandlers(clients, serverInfo);
53
56
  }
54
57
  }
55
58
  /**
56
59
  * Registers resource-related request handlers
57
60
  * @param clients Record of client instances
58
- * @param server The MCP server instance
61
+ * @param serverInfo The MCP server instance
59
62
  */
60
- function registerResourceHandlers(clients, server) {
63
+ function registerResourceHandlers(clients, serverInfo) {
61
64
  // List Resources handler
62
- server.setRequestHandler(ListResourcesRequestSchema, withErrorHandling(async (request) => {
65
+ serverInfo.server.setRequestHandler(ListResourcesRequestSchema, withErrorHandling(async (request) => {
63
66
  const resources = [];
64
67
  const failedClients = [];
65
- for (const [name, client] of Object.entries(clients)) {
68
+ const filteredClients = filterClientsByTags(clients, serverInfo.tags);
69
+ for (const [name, clientInfo] of Object.entries(filteredClients)) {
66
70
  logger.info(`Listing resources for ${name}`);
67
71
  try {
68
- const result = await client.listResources(request.params);
72
+ const result = await clientInfo.client.listResources(request.params, {
73
+ timeout: clientInfo.transport.timeout,
74
+ });
69
75
  resources.push(...result.resources.map((resource) => ({
70
76
  uri: `${name}${MCP_URI_SEPARATOR}${resource.uri}`,
71
77
  name: resource.name,
@@ -79,23 +85,28 @@ function registerResourceHandlers(clients, server) {
79
85
  }
80
86
  }
81
87
  // If all clients failed, throw an error
82
- if (failedClients.length === Object.keys(clients).length && Object.keys(clients).length > 0) {
83
- throw new ProxyError('Failed to list resources from all clients', new Error(JSON.stringify(failedClients)));
88
+ if (failedClients.length === Object.keys(filteredClients).length && Object.keys(filteredClients).length > 0) {
89
+ throw new MCPError('Failed to list resources from all clients', ERROR_CODES.INTERNAL_SERVER_ERROR, {
90
+ failedClients,
91
+ });
84
92
  }
85
93
  // Send notification about partial failures
86
94
  if (failedClients.length > 0) {
87
- sendPartialFailureNotification(server, 'listResources', failedClients);
95
+ sendPartialFailureNotification(serverInfo, 'listResources', failedClients);
88
96
  }
89
97
  return { resources };
90
98
  }, 'Error listing resources'));
91
99
  // List Resource Templates handler
92
- server.setRequestHandler(ListResourceTemplatesRequestSchema, withErrorHandling(async (request) => {
100
+ serverInfo.server.setRequestHandler(ListResourceTemplatesRequestSchema, withErrorHandling(async (request) => {
93
101
  const resourceTemplates = [];
94
102
  const failedClients = [];
95
- for (const [name, client] of Object.entries(clients)) {
103
+ const filteredClients = filterClientsByTags(clients, serverInfo.tags);
104
+ for (const [name, clientInfo] of Object.entries(filteredClients)) {
96
105
  logger.info(`Listing resource templates for ${name}`);
97
106
  try {
98
- const result = await client.listResourceTemplates(request.params);
107
+ const result = await clientInfo.client.listResourceTemplates(request.params, {
108
+ timeout: clientInfo.transport.timeout,
109
+ });
99
110
  resourceTemplates.push(...result.resourceTemplates.map((template) => ({
100
111
  uriTemplate: `${name}${MCP_URI_SEPARATOR}${template.uriTemplate}`,
101
112
  name: template.name,
@@ -109,45 +120,56 @@ function registerResourceHandlers(clients, server) {
109
120
  }
110
121
  }
111
122
  // If all clients failed, throw an error
112
- if (failedClients.length === Object.keys(clients).length && Object.keys(clients).length > 0) {
113
- throw new ProxyError('Failed to list resource templates from all clients', new Error(JSON.stringify(failedClients)));
123
+ if (failedClients.length === Object.keys(filteredClients).length && Object.keys(filteredClients).length > 0) {
124
+ throw new MCPError('Failed to list resource templates from all clients', ERROR_CODES.INTERNAL_SERVER_ERROR, {
125
+ failedClients,
126
+ });
114
127
  }
115
128
  // Send notification about partial failures
116
129
  if (failedClients.length > 0) {
117
- sendPartialFailureNotification(server, 'listResourceTemplates', failedClients);
130
+ sendPartialFailureNotification(serverInfo, 'listResourceTemplates', failedClients);
118
131
  }
119
132
  return { resourceTemplates };
120
133
  }, 'Error listing resource templates'));
121
134
  // Subscribe Resource handler
122
- server.setRequestHandler(SubscribeRequestSchema, withErrorHandling(async (request) => {
135
+ serverInfo.server.setRequestHandler(SubscribeRequestSchema, withErrorHandling(async (request) => {
123
136
  const { clientName, resourceName } = parseUri(request.params.uri, MCP_URI_SEPARATOR);
124
- return executeClientOperation(clients, clientName, (client) => client.subscribeResource({ ...request.params, uri: resourceName }));
137
+ return executeClientOperation(clients, clientName, (clientInfo) => clientInfo.client.subscribeResource({ ...request.params, uri: resourceName }, {
138
+ timeout: clientInfo.transport.timeout,
139
+ }));
125
140
  }, 'Error subscribing to resource'));
126
141
  // Unsubscribe Resource handler
127
- server.setRequestHandler(UnsubscribeRequestSchema, withErrorHandling(async (request) => {
142
+ serverInfo.server.setRequestHandler(UnsubscribeRequestSchema, withErrorHandling(async (request) => {
128
143
  const { clientName, resourceName } = parseUri(request.params.uri, MCP_URI_SEPARATOR);
129
- return executeClientOperation(clients, clientName, (client) => client.unsubscribeResource({ ...request.params, uri: resourceName }));
144
+ return executeClientOperation(clients, clientName, (clientInfo) => clientInfo.client.unsubscribeResource({ ...request.params, uri: resourceName }, {
145
+ timeout: clientInfo.transport.timeout,
146
+ }));
130
147
  }, 'Error unsubscribing from resource'));
131
148
  // Read Resource handler
132
- server.setRequestHandler(ReadResourceRequestSchema, withErrorHandling(async (request) => {
149
+ serverInfo.server.setRequestHandler(ReadResourceRequestSchema, withErrorHandling(async (request) => {
133
150
  const { clientName, resourceName } = parseUri(request.params.uri, MCP_URI_SEPARATOR);
134
- return executeClientOperation(clients, clientName, (client) => client.readResource({ ...request.params, uri: resourceName }));
151
+ return executeClientOperation(clients, clientName, (clientInfo) => clientInfo.client.readResource({ ...request.params, uri: resourceName }, {
152
+ timeout: clientInfo.transport.timeout,
153
+ }));
135
154
  }, 'Error reading resource'));
136
155
  }
137
156
  /**
138
157
  * Registers tool-related request handlers
139
158
  * @param clients Record of client instances
140
- * @param server The MCP server instance
159
+ * @param serverInfo The MCP server instance
141
160
  */
142
- function registerToolHandlers(clients, server) {
161
+ function registerToolHandlers(clients, serverInfo) {
143
162
  // List Tools handler
144
- server.setRequestHandler(ListToolsRequestSchema, withErrorHandling(async (request) => {
163
+ serverInfo.server.setRequestHandler(ListToolsRequestSchema, withErrorHandling(async (request) => {
145
164
  const tools = [];
146
165
  const failedClients = [];
147
- for (const [name, client] of Object.entries(clients)) {
166
+ const filteredClients = filterClientsByTags(clients, serverInfo.tags);
167
+ for (const [name, clientInfo] of Object.entries(filteredClients)) {
148
168
  logger.info(`Listing tools for ${name}`);
149
169
  try {
150
- const result = await client.listTools(request.params);
170
+ const result = await clientInfo.client.listTools(request.params, {
171
+ timeout: clientInfo.transport.timeout,
172
+ });
151
173
  tools.push(...result.tools.map((tool) => ({
152
174
  name: `${name}${MCP_URI_SEPARATOR}${tool.name}`,
153
175
  description: tool.description,
@@ -160,35 +182,40 @@ function registerToolHandlers(clients, server) {
160
182
  }
161
183
  }
162
184
  // If all clients failed, throw an error
163
- if (failedClients.length === Object.keys(clients).length && Object.keys(clients).length > 0) {
164
- throw new ProxyError('Failed to list tools from all clients', new Error(JSON.stringify(failedClients)));
185
+ if (failedClients.length === Object.keys(filteredClients).length && Object.keys(filteredClients).length > 0) {
186
+ throw new MCPError('Failed to list tools from all clients', ERROR_CODES.INTERNAL_SERVER_ERROR, {
187
+ failedClients,
188
+ });
165
189
  }
166
190
  // Send notification about partial failures
167
191
  if (failedClients.length > 0) {
168
- sendPartialFailureNotification(server, 'listTools', failedClients);
192
+ sendPartialFailureNotification(serverInfo, 'listTools', failedClients);
169
193
  }
170
194
  return { tools };
171
195
  }, 'Error listing tools'));
172
196
  // Call Tool handler
173
- server.setRequestHandler(CallToolRequestSchema, withErrorHandling(async (request) => {
197
+ serverInfo.server.setRequestHandler(CallToolRequestSchema, withErrorHandling(async (request) => {
174
198
  const { clientName, resourceName: toolName } = parseUri(request.params.name, MCP_URI_SEPARATOR);
175
- return executeClientOperation(clients, clientName, (client) => client.callTool({ ...request.params, name: toolName }));
199
+ return executeClientOperation(clients, clientName, (clientInfo) => clientInfo.client.callTool({ ...request.params, name: toolName }, CallToolResultSchema, {
200
+ timeout: clientInfo.transport.timeout,
201
+ }));
176
202
  }, 'Error calling tool'));
177
203
  }
178
204
  /**
179
205
  * Registers prompt-related request handlers
180
206
  * @param clients Record of client instances
181
- * @param server The MCP server instance
207
+ * @param serverInfo The MCP server instance
182
208
  */
183
- function registerPromptHandlers(clients, server) {
209
+ function registerPromptHandlers(clients, serverInfo) {
184
210
  // List Prompts handler
185
- server.setRequestHandler(ListPromptsRequestSchema, withErrorHandling(async (request) => {
211
+ serverInfo.server.setRequestHandler(ListPromptsRequestSchema, withErrorHandling(async (request) => {
186
212
  const prompts = [];
187
213
  const failedClients = [];
188
- for (const [name, client] of Object.entries(clients)) {
214
+ const filteredClients = filterClientsByTags(clients, serverInfo.tags);
215
+ for (const [name, clientInfo] of Object.entries(filteredClients)) {
189
216
  logger.info(`Listing prompts for ${name}`);
190
217
  try {
191
- const result = await client.listPrompts(request.params);
218
+ const result = await clientInfo.client.listPrompts(request.params);
192
219
  prompts.push(...result.prompts.map((prompt) => ({
193
220
  name: `${name}${MCP_URI_SEPARATOR}${prompt.name}`,
194
221
  description: prompt.description,
@@ -201,18 +228,20 @@ function registerPromptHandlers(clients, server) {
201
228
  }
202
229
  }
203
230
  // If all clients failed, throw an error
204
- if (failedClients.length === Object.keys(clients).length && Object.keys(clients).length > 0) {
205
- throw new ProxyError('Failed to list prompts from all clients', new Error(JSON.stringify(failedClients)));
231
+ if (failedClients.length === Object.keys(filteredClients).length && Object.keys(filteredClients).length > 0) {
232
+ throw new MCPError('Failed to list prompts from all clients', ERROR_CODES.INTERNAL_SERVER_ERROR, {
233
+ failedClients,
234
+ });
206
235
  }
207
236
  // Send notification about partial failures
208
237
  if (failedClients.length > 0) {
209
- sendPartialFailureNotification(server, 'listPrompts', failedClients);
238
+ sendPartialFailureNotification(serverInfo, 'listPrompts', failedClients);
210
239
  }
211
240
  return { prompts };
212
241
  }, 'Error listing prompts'));
213
242
  // Get Prompt handler
214
- server.setRequestHandler(GetPromptRequestSchema, withErrorHandling(async (request) => {
243
+ serverInfo.server.setRequestHandler(GetPromptRequestSchema, withErrorHandling(async (request) => {
215
244
  const { clientName, resourceName: promptName } = parseUri(request.params.name, MCP_URI_SEPARATOR);
216
- return executeClientOperation(clients, clientName, (client) => client.getPrompt({ ...request.params, name: promptName }));
245
+ return executeClientOperation(clients, clientName, (clientInfo) => clientInfo.client.getPrompt({ ...request.params, name: promptName }));
217
246
  }, 'Error getting prompt'));
218
247
  }
package/build/index.js CHANGED
@@ -7,6 +7,7 @@ import logger from './logger/logger.js';
7
7
  import configReloadService from './services/configReloadService.js';
8
8
  import { ConfigManager } from './config/configManager.js';
9
9
  import { ExpressServer } from './server/expressServer.js';
10
+ import { PORT, HOST } from './constants.js';
10
11
  // Parse command line arguments
11
12
  const argv = yargs(hideBin(process.argv))
12
13
  .usage('Usage: $0 [options]')
@@ -18,12 +19,30 @@ const argv = yargs(hideBin(process.argv))
18
19
  choices: ['stdio', 'sse'],
19
20
  default: 'sse',
20
21
  },
22
+ port: {
23
+ alias: 'P',
24
+ describe: 'SSE port to listen on, applicable when transport is sse',
25
+ type: 'number',
26
+ default: PORT,
27
+ },
28
+ host: {
29
+ alias: 'H',
30
+ describe: 'SSE host to listen on, applicable when transport is sse',
31
+ type: 'string',
32
+ default: HOST,
33
+ },
21
34
  config: {
22
35
  alias: 'c',
23
36
  describe: 'Path to the config file',
24
37
  type: 'string',
25
38
  default: undefined,
26
39
  },
40
+ tags: {
41
+ alias: 'g',
42
+ describe: 'Tags to filter clients (comma-separated)',
43
+ type: 'string',
44
+ default: undefined,
45
+ },
27
46
  })
28
47
  .help()
29
48
  .alias('help', 'h')
@@ -67,13 +86,22 @@ async function main() {
67
86
  if (argv.transport === 'stdio') {
68
87
  // Use stdio transport
69
88
  const transport = new StdioServerTransport();
70
- await serverManager.connectTransport(transport, 'stdio');
89
+ // Parse and validate tags from CLI if provided
90
+ let tags;
91
+ if (argv.tags) {
92
+ tags = argv.tags.split(',').filter((tag) => tag.trim().length > 0);
93
+ if (tags.length === 0) {
94
+ logger.warn('No valid tags provided, ignoring tags parameter');
95
+ tags = undefined;
96
+ }
97
+ }
98
+ await serverManager.connectTransport(transport, 'stdio', tags);
71
99
  logger.info('Server started with stdio transport');
72
100
  }
73
101
  else {
74
102
  // Use HTTP/SSE transport
75
103
  const expressServer = new ExpressServer(serverManager);
76
- expressServer.start();
104
+ expressServer.start(argv.port, argv.host);
77
105
  }
78
106
  }
79
107
  catch (error) {
@@ -36,23 +36,6 @@ const logger = winston.createLogger({
36
36
  new winston.transports.Console({
37
37
  format: consoleFormat,
38
38
  }),
39
- //
40
- // - Write all logs with importance level of `error` or higher to `error.log`
41
- // (i.e., error, fatal, but not other levels)
42
- //
43
- new winston.transports.File({
44
- filename: path.join(logsDir, 'error.log'),
45
- level: 'error',
46
- format: customFormat,
47
- }),
48
- //
49
- // - Write all logs with importance level of `info` or higher to `combined.log`
50
- // (i.e., fatal, error, warn, and info, but not trace)
51
- //
52
- new winston.transports.File({
53
- filename: path.join(logsDir, 'combined.log'),
54
- format: customFormat,
55
- }),
56
39
  ],
57
40
  // Prevent logger from exiting on error
58
41
  exitOnError: false,
@@ -133,7 +133,7 @@ export class MCPTransport extends Transport {
133
133
  try {
134
134
  this.sendLogNotification(info);
135
135
  }
136
- catch (error) {
136
+ catch (_) {
137
137
  // Ignore any errors during log sending
138
138
  }
139
139
  }
@@ -1,7 +1,7 @@
1
1
  import express from 'express';
2
2
  import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
3
3
  import logger, { setMCPTransportConnected } from '../logger/logger.js';
4
- import { PORT, SSE_ENDPOINT, MESSAGES_ENDPOINT, ERROR_CODES } from '../constants.js';
4
+ import { SSE_ENDPOINT, MESSAGES_ENDPOINT, ERROR_CODES } from '../constants.js';
5
5
  export class ExpressServer {
6
6
  app;
7
7
  serverManager;
@@ -13,7 +13,7 @@ export class ExpressServer {
13
13
  }
14
14
  setupMiddleware() {
15
15
  // Add error handling middleware
16
- this.app.use((err, req, res, next) => {
16
+ this.app.use((err, req, res, _next) => {
17
17
  logger.error('Express error:', err);
18
18
  res.status(500).json({
19
19
  error: {
@@ -28,8 +28,23 @@ export class ExpressServer {
28
28
  try {
29
29
  logger.info('sse', { query: req.query, headers: req.headers });
30
30
  const transport = new SSEServerTransport(MESSAGES_ENDPOINT, res);
31
+ // Extract and validate tags from query parameters
32
+ let tags;
33
+ if (req.query.tags) {
34
+ const tagsStr = req.query.tags;
35
+ if (typeof tagsStr !== 'string') {
36
+ res.status(400).json({
37
+ error: {
38
+ code: ERROR_CODES.INVALID_PARAMS,
39
+ message: 'Invalid params: tags must be a string',
40
+ },
41
+ });
42
+ return;
43
+ }
44
+ tags = tagsStr.split(',').filter((tag) => tag.trim().length > 0);
45
+ }
31
46
  // Connect the transport using the server manager
32
- await this.serverManager.connectTransport(transport, transport.sessionId);
47
+ await this.serverManager.connectTransport(transport, transport.sessionId, tags);
33
48
  // Update MCP transport connection status when a client connects
34
49
  if (this.serverManager.getActiveTransportsCount() === 1) {
35
50
  setMCPTransportConnected(true);
@@ -86,9 +101,9 @@ export class ExpressServer {
86
101
  }
87
102
  });
88
103
  }
89
- start() {
90
- this.app.listen(PORT, () => {
91
- logger.info(`Server is running on port ${PORT} with HTTP/SSE transport`);
104
+ start(port, host) {
105
+ this.app.listen(port, host, () => {
106
+ logger.info(`Server is running on port ${port} with HTTP/SSE transport`);
92
107
  });
93
108
  }
94
109
  }
@@ -9,33 +9,37 @@ export class ServerManager {
9
9
  serverConfig;
10
10
  serverCapabilities;
11
11
  clients = {};
12
- clientTransports = {};
13
- constructor(config, capabilities, clients, clientTransports) {
12
+ transports = {};
13
+ constructor(config, capabilities, clients, transports) {
14
14
  this.serverConfig = config;
15
15
  this.serverCapabilities = capabilities;
16
16
  this.clients = clients;
17
- this.clientTransports = clientTransports;
17
+ this.transports = transports;
18
18
  }
19
- static getInstance(config, capabilities, clients, clientTransports) {
19
+ static getInstance(config, capabilities, clients, transports) {
20
20
  if (!ServerManager.instance) {
21
- ServerManager.instance = new ServerManager(config, capabilities, clients, clientTransports);
21
+ ServerManager.instance = new ServerManager(config, capabilities, clients, transports);
22
22
  }
23
23
  return ServerManager.instance;
24
24
  }
25
- async connectTransport(transport, sessionId) {
25
+ async connectTransport(transport, sessionId, tags) {
26
26
  try {
27
27
  // Create a new server instance for this transport
28
28
  const server = new Server(this.serverConfig, this.serverCapabilities);
29
29
  // Enhance server with logging middleware
30
30
  enhanceServerWithLogging(server);
31
- // Collect capabilities and register handlers
32
- await setupCapabilities(this.clients, server);
31
+ const serverInfo = {
32
+ server,
33
+ tags,
34
+ };
35
+ // Set up capabilities for this server instance
36
+ await setupCapabilities(this.clients, serverInfo);
33
37
  // Initialize the configuration reload service
34
- configReloadService.initialize(server, this.clientTransports);
38
+ configReloadService.initialize(serverInfo, this.transports);
39
+ // Store the server instance
40
+ this.servers.set(sessionId, serverInfo);
35
41
  // Connect the transport to the new server instance
36
42
  await server.connect(transport);
37
- // Store the server instance
38
- this.servers.set(sessionId, server);
39
43
  logger.info(`Connected transport for session ${sessionId}`);
40
44
  }
41
45
  catch (error) {
@@ -51,17 +55,20 @@ export class ServerManager {
51
55
  }
52
56
  }
53
57
  getTransport(sessionId) {
54
- return this.servers.get(sessionId)?.transport;
58
+ return this.servers.get(sessionId)?.server.transport;
55
59
  }
56
60
  getTransports() {
57
61
  const transports = new Map();
58
62
  for (const [id, server] of this.servers.entries()) {
59
- if (server.transport) {
60
- transports.set(id, server.transport);
63
+ if (server.server.transport) {
64
+ transports.set(id, server.server.transport);
61
65
  }
62
66
  }
63
67
  return transports;
64
68
  }
69
+ getClientTransports() {
70
+ return this.transports;
71
+ }
65
72
  getActiveTransportsCount() {
66
73
  return this.servers.size;
67
74
  }
@@ -0,0 +1,158 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { ServerManager } from './serverManager.js';
3
+ import logger from './logger/logger.js';
4
+ import configReloadService from './services/configReloadService.js';
5
+ import { setupCapabilities } from './capabilities/capabilityManager.js';
6
+ import { enhanceServerWithLogging } from './middleware/loggingMiddleware.js';
7
+ // Mock dependencies
8
+ jest.mock('@modelcontextprotocol/sdk/server/index.js', () => ({
9
+ Server: jest.fn(),
10
+ }));
11
+ jest.mock('@modelcontextprotocol/sdk/shared/transport.js', () => ({
12
+ Transport: jest.fn(),
13
+ }));
14
+ jest.mock('./logger/logger.js', () => {
15
+ const mockLogger = {
16
+ info: jest.fn(),
17
+ error: jest.fn(),
18
+ };
19
+ return {
20
+ __esModule: true,
21
+ default: mockLogger,
22
+ };
23
+ });
24
+ jest.mock('./services/configReloadService.js', () => ({
25
+ __esModule: true,
26
+ default: {
27
+ initialize: jest.fn(),
28
+ },
29
+ }));
30
+ jest.mock('./capabilities/capabilityManager.js', () => ({
31
+ setupCapabilities: jest.fn(),
32
+ }));
33
+ jest.mock('./middleware/loggingMiddleware.js', () => ({
34
+ enhanceServerWithLogging: jest.fn(),
35
+ }));
36
+ describe('ServerManager', () => {
37
+ let mockConfig;
38
+ let mockCapabilities;
39
+ let mockClients;
40
+ let mockTransports;
41
+ let mockTransport;
42
+ let mockServer;
43
+ beforeEach(() => {
44
+ // Reset all mocks
45
+ jest.clearAllMocks();
46
+ // Setup test data
47
+ mockConfig = { name: 'test-server', version: '1.0.0' };
48
+ mockCapabilities = { capabilities: { test: true } };
49
+ mockClients = {};
50
+ mockTransports = {};
51
+ mockTransport = {
52
+ // Add any required Transport properties here
53
+ };
54
+ mockServer = {
55
+ connect: jest.fn().mockResolvedValue(undefined),
56
+ transport: mockTransport,
57
+ };
58
+ // Setup mocks
59
+ Server.mockImplementation(() => mockServer);
60
+ setupCapabilities.mockResolvedValue(undefined);
61
+ enhanceServerWithLogging.mockReturnValue(undefined);
62
+ configReloadService.initialize.mockImplementation(() => undefined);
63
+ });
64
+ describe('getInstance', () => {
65
+ it('should create a singleton instance', () => {
66
+ const instance1 = ServerManager.getInstance(mockConfig, mockCapabilities, mockClients, mockTransports);
67
+ const instance2 = ServerManager.getInstance(mockConfig, mockCapabilities, mockClients, mockTransports);
68
+ expect(instance1).toBe(instance2);
69
+ });
70
+ });
71
+ describe('connectTransport', () => {
72
+ let serverManager;
73
+ const sessionId = 'test-session';
74
+ const tags = ['tag1', 'tag2'];
75
+ beforeEach(() => {
76
+ serverManager = ServerManager.getInstance(mockConfig, mockCapabilities, mockClients, mockTransports);
77
+ });
78
+ it('should successfully connect a transport', async () => {
79
+ await serverManager.connectTransport(mockTransport, sessionId, tags);
80
+ expect(Server).toHaveBeenCalledWith(mockConfig, mockCapabilities);
81
+ expect(enhanceServerWithLogging).toHaveBeenCalledWith(mockServer);
82
+ expect(setupCapabilities).toHaveBeenCalled();
83
+ expect(configReloadService.initialize).toHaveBeenCalled();
84
+ expect(mockServer.connect).toHaveBeenCalledWith(mockTransport);
85
+ expect(logger.info).toHaveBeenCalledWith(`Connected transport for session ${sessionId}`);
86
+ });
87
+ it('should handle connection errors', async () => {
88
+ const error = new Error('Connection failed');
89
+ mockServer.connect.mockRejectedValueOnce(error);
90
+ await expect(serverManager.connectTransport(mockTransport, sessionId, tags)).rejects.toThrow('Connection failed');
91
+ expect(logger.error).toHaveBeenCalled();
92
+ });
93
+ });
94
+ describe('disconnectTransport', () => {
95
+ let serverManager;
96
+ const sessionId = 'test-session';
97
+ beforeEach(() => {
98
+ serverManager = ServerManager.getInstance(mockConfig, mockCapabilities, mockClients, mockTransports);
99
+ });
100
+ it('should successfully disconnect a transport', async () => {
101
+ await serverManager.connectTransport(mockTransport, sessionId);
102
+ jest.clearAllMocks(); // Clear the logs from connectTransport
103
+ serverManager.disconnectTransport(sessionId);
104
+ expect(logger.info).toHaveBeenCalledWith(`Disconnected transport for session ${sessionId}`);
105
+ });
106
+ it('should handle non-existent session gracefully', () => {
107
+ serverManager.disconnectTransport('non-existent');
108
+ expect(logger.info).not.toHaveBeenCalled();
109
+ });
110
+ });
111
+ describe('transport management methods', () => {
112
+ let serverManager;
113
+ const sessionId = 'test-session';
114
+ beforeEach(async () => {
115
+ serverManager = ServerManager.getInstance(mockConfig, mockCapabilities, mockClients, mockTransports);
116
+ await serverManager.connectTransport(mockTransport, sessionId);
117
+ });
118
+ it('should get transport by session id', () => {
119
+ const transport = serverManager.getTransport(sessionId);
120
+ expect(transport).toBe(mockTransport);
121
+ });
122
+ it('should return undefined for non-existent session', () => {
123
+ const transport = serverManager.getTransport('non-existent');
124
+ expect(transport).toBeUndefined();
125
+ });
126
+ it('should get all transports', () => {
127
+ const transports = serverManager.getTransports();
128
+ expect(transports.size).toBe(1);
129
+ expect(transports.get(sessionId)).toBe(mockTransport);
130
+ });
131
+ it('should get client transports', () => {
132
+ const clientTransports = serverManager.getClientTransports();
133
+ expect(clientTransports).toEqual(mockTransports);
134
+ });
135
+ it('should get active transports count', () => {
136
+ expect(serverManager.getActiveTransportsCount()).toBe(1);
137
+ });
138
+ });
139
+ describe('getServer', () => {
140
+ let serverManager;
141
+ const sessionId = 'test-session';
142
+ const tags = ['tag1', 'tag2'];
143
+ beforeEach(async () => {
144
+ serverManager = ServerManager.getInstance(mockConfig, mockCapabilities, mockClients, mockTransports);
145
+ await serverManager.connectTransport(mockTransport, sessionId, tags);
146
+ });
147
+ it('should return server info for existing session', () => {
148
+ const serverInfo = serverManager.getServer(sessionId);
149
+ expect(serverInfo).toBeDefined();
150
+ expect(serverInfo?.server).toBe(mockServer);
151
+ expect(serverInfo?.tags).toEqual(tags);
152
+ });
153
+ it('should return undefined for non-existent session', () => {
154
+ const serverInfo = serverManager.getServer('non-existent');
155
+ expect(serverInfo).toBeUndefined();
156
+ });
157
+ });
158
+ });