@1mcp/agent 0.1.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.
@@ -18,14 +18,30 @@ export class MCPTransport extends Transport {
18
18
  constructor(options) {
19
19
  super(options);
20
20
  this.server = options.server;
21
- this.loggerName = options.loggerName || '1mcp-agent';
21
+ this.loggerName = options.loggerName || '1mcp';
22
+ // Set up connection status handlers
23
+ this.setupConnectionHandlers();
24
+ }
25
+ /**
26
+ * Set up handlers for connection status changes
27
+ */
28
+ setupConnectionHandlers() {
29
+ // Handle connection close
30
+ this.server.onclose = () => {
31
+ this.setConnected(false);
32
+ };
22
33
  }
23
34
  /**
24
35
  * Set the connection status of the transport
25
36
  * @param connected Whether the server is connected
26
37
  */
27
38
  setConnected(connected) {
39
+ const previousState = this.connected;
28
40
  this.connected = connected;
41
+ // Log connection state changes
42
+ if (previousState !== connected) {
43
+ console.debug(`MCP Transport connection state changed to: ${connected ? 'connected' : 'disconnected'}`);
44
+ }
29
45
  }
30
46
  /**
31
47
  * Send a log notification to the MCP client
@@ -112,11 +128,23 @@ export class MCPTransport extends Transport {
112
128
  setImmediate(() => {
113
129
  this.emit('logged', info);
114
130
  });
115
- if (!this.connected) {
116
- callback();
117
- return;
131
+ // Only try to send if connected
132
+ if (this.connected) {
133
+ try {
134
+ this.sendLogNotification(info);
135
+ }
136
+ catch (_) {
137
+ // Ignore any errors during log sending
138
+ }
118
139
  }
119
- this.sendLogNotification(info);
140
+ // Always call the callback to prevent blocking
120
141
  callback();
121
142
  }
143
+ /**
144
+ * Clean up resources when the transport is closed
145
+ */
146
+ close() {
147
+ this.setConnected(false);
148
+ this.server.onclose = undefined;
149
+ }
122
150
  }
@@ -0,0 +1,114 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+ import logger from '../logger/logger.js';
3
+ const activeRequests = new Map();
4
+ /**
5
+ * Logs MCP request details
6
+ */
7
+ function logRequest(requestId, method, params) {
8
+ logger.info('MCP Request', {
9
+ requestId,
10
+ method,
11
+ params: JSON.stringify(params),
12
+ timestamp: new Date().toISOString(),
13
+ });
14
+ }
15
+ /**
16
+ * Logs MCP response details
17
+ */
18
+ function logResponse(requestId, result, duration) {
19
+ logger.info('MCP Response', {
20
+ requestId,
21
+ duration,
22
+ timestamp: new Date().toISOString(),
23
+ });
24
+ }
25
+ /**
26
+ * Logs MCP error details
27
+ */
28
+ function logError(requestId, error, duration) {
29
+ logger.error('MCP Error', {
30
+ requestId,
31
+ error: error instanceof Error ? error.message : JSON.stringify(error),
32
+ stack: error instanceof Error ? error.stack : undefined,
33
+ duration,
34
+ timestamp: new Date().toISOString(),
35
+ });
36
+ }
37
+ /**
38
+ * Logs MCP notification details
39
+ */
40
+ function logNotification(method, params) {
41
+ logger.info('MCP Notification', {
42
+ method,
43
+ params: JSON.stringify(params),
44
+ timestamp: new Date().toISOString(),
45
+ });
46
+ }
47
+ /**
48
+ * Wraps the original request handler with logging
49
+ */
50
+ function wrapRequestHandler(originalHandler, method) {
51
+ return async (request, extra) => {
52
+ const requestId = uuidv4();
53
+ const startTime = Date.now();
54
+ // Store request context
55
+ activeRequests.set(requestId, {
56
+ requestId,
57
+ method,
58
+ startTime,
59
+ });
60
+ // Log request
61
+ logRequest(requestId, method, request.params);
62
+ try {
63
+ // Execute original handler
64
+ const result = await originalHandler(request, extra);
65
+ // Log response
66
+ const duration = Date.now() - startTime;
67
+ logResponse(requestId, result, duration);
68
+ return result;
69
+ }
70
+ catch (error) {
71
+ // Log error
72
+ const duration = Date.now() - startTime;
73
+ logError(requestId, error, duration);
74
+ throw error;
75
+ }
76
+ finally {
77
+ // Clean up request context
78
+ activeRequests.delete(requestId);
79
+ }
80
+ };
81
+ }
82
+ /**
83
+ * Wraps the original notification handler with logging
84
+ */
85
+ function wrapNotificationHandler(originalHandler, method) {
86
+ return async (notification) => {
87
+ // Log notification
88
+ logNotification(method, notification.params);
89
+ // Execute original handler
90
+ await originalHandler(notification);
91
+ };
92
+ }
93
+ /**
94
+ * Enhances an MCP server with request/response logging
95
+ */
96
+ export function enhanceServerWithLogging(server) {
97
+ // Store original methods
98
+ const originalSetRequestHandler = server.setRequestHandler.bind(server);
99
+ const originalSetNotificationHandler = server.setNotificationHandler.bind(server);
100
+ const originalNotification = server.notification.bind(server);
101
+ // Override request handler registration
102
+ server.setRequestHandler = (schema, handler) => {
103
+ return originalSetRequestHandler(schema, wrapRequestHandler(handler, schema._def.shape().method._def.value));
104
+ };
105
+ // Override notification handler registration
106
+ server.setNotificationHandler = (schema, handler) => {
107
+ return originalSetNotificationHandler(schema, wrapNotificationHandler(handler, schema._def.shape().method._def.value));
108
+ };
109
+ // Override notification sending
110
+ server.notification = (notification) => {
111
+ logNotification(notification.method, notification.params);
112
+ return originalNotification(notification);
113
+ };
114
+ }
@@ -0,0 +1,109 @@
1
+ import express from 'express';
2
+ import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
3
+ import logger, { setMCPTransportConnected } from '../logger/logger.js';
4
+ import { SSE_ENDPOINT, MESSAGES_ENDPOINT, ERROR_CODES } from '../constants.js';
5
+ export class ExpressServer {
6
+ app;
7
+ serverManager;
8
+ constructor(serverManager) {
9
+ this.app = express();
10
+ this.serverManager = serverManager;
11
+ this.setupMiddleware();
12
+ this.setupRoutes();
13
+ }
14
+ setupMiddleware() {
15
+ // Add error handling middleware
16
+ this.app.use((err, req, res, _next) => {
17
+ logger.error('Express error:', err);
18
+ res.status(500).json({
19
+ error: {
20
+ code: ERROR_CODES.INTERNAL_SERVER_ERROR,
21
+ message: 'Internal server error',
22
+ },
23
+ });
24
+ });
25
+ }
26
+ setupRoutes() {
27
+ this.app.get(SSE_ENDPOINT, async (req, res) => {
28
+ try {
29
+ logger.info('sse', { query: req.query, headers: req.headers });
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
+ }
46
+ // Connect the transport using the server manager
47
+ await this.serverManager.connectTransport(transport, transport.sessionId, tags);
48
+ // Update MCP transport connection status when a client connects
49
+ if (this.serverManager.getActiveTransportsCount() === 1) {
50
+ setMCPTransportConnected(true);
51
+ logger.info('First client connected, enabling MCP logging transport');
52
+ }
53
+ transport.onclose = () => {
54
+ this.serverManager.disconnectTransport(transport.sessionId);
55
+ logger.info('transport closed', transport.sessionId);
56
+ // Update MCP transport connection status when all clients disconnect
57
+ if (this.serverManager.getActiveTransportsCount() === 0) {
58
+ setMCPTransportConnected(false);
59
+ logger.info('All clients disconnected, disabling MCP logging transport');
60
+ }
61
+ };
62
+ }
63
+ catch (error) {
64
+ logger.error('SSE connection error:', error);
65
+ res.status(500).end();
66
+ }
67
+ });
68
+ this.app.post(MESSAGES_ENDPOINT, async (req, res) => {
69
+ try {
70
+ const sessionId = req.query.sessionId;
71
+ if (!sessionId) {
72
+ res.status(400).json({
73
+ error: {
74
+ code: ERROR_CODES.INVALID_PARAMS,
75
+ message: 'Invalid params: sessionId is required',
76
+ },
77
+ });
78
+ return;
79
+ }
80
+ logger.info('message', { body: req.body, sessionId });
81
+ const transport = this.serverManager.getTransport(sessionId);
82
+ if (transport instanceof SSEServerTransport) {
83
+ await transport.handlePostMessage(req, res);
84
+ return;
85
+ }
86
+ res.status(404).json({
87
+ error: {
88
+ code: ERROR_CODES.TRANSPORT_NOT_FOUND,
89
+ message: 'Transport not found',
90
+ },
91
+ });
92
+ }
93
+ catch (error) {
94
+ logger.error('Message handling error:', error);
95
+ res.status(500).json({
96
+ error: {
97
+ code: ERROR_CODES.INTERNAL_SERVER_ERROR,
98
+ message: 'Internal server error',
99
+ },
100
+ });
101
+ }
102
+ });
103
+ }
104
+ start(port, host) {
105
+ this.app.listen(port, host, () => {
106
+ logger.info(`Server is running on port ${port} with HTTP/SSE transport`);
107
+ });
108
+ }
109
+ }
package/build/server.js CHANGED
@@ -1,61 +1,28 @@
1
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
- import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from './constants.js';
3
- import logger, { addMCPTransport } from './logger.js';
1
+ import { MCP_SERVER_CAPABILITIES, MCP_SERVER_NAME, MCP_SERVER_VERSION } from './constants.js';
2
+ import logger from './logger/logger.js';
4
3
  import { createTransports } from './config/transportConfig.js';
5
4
  import { createClients } from './clients/clientManager.js';
6
- import { collectAndRegisterCapabilities } from './capabilities/capabilityManager.js';
7
- import configReloadService from './services/configReloadService.js';
8
- /**
9
- * Initialize and configure the MCP server
10
- * @returns The configured MCP server instance
11
- */
12
- async function initializeServer() {
13
- try {
14
- // Create the server instance
15
- const server = new Server({
16
- name: MCP_SERVER_NAME,
17
- version: MCP_SERVER_VERSION,
18
- }, {
19
- capabilities: {
20
- logging: {},
21
- },
22
- });
23
- // Initialize the MCP transport for logging
24
- addMCPTransport(server, MCP_SERVER_NAME);
25
- logger.info('Server created and MCP transport initialized for logging');
26
- return server;
27
- }
28
- catch (error) {
29
- logger.error(`Failed to initialize server: ${error}`);
30
- throw error;
31
- }
32
- }
5
+ import { ServerManager } from './serverManager.js';
6
+ import { ConfigManager } from './config/configManager.js';
33
7
  /**
34
8
  * Main function to set up the MCP server
35
9
  */
36
10
  async function setupServer() {
37
11
  try {
38
- // Initialize the server
39
- const server = await initializeServer();
12
+ const mcpConfig = ConfigManager.getInstance().getTransportConfig();
40
13
  // Create transports from configuration
41
- const transports = createTransports();
14
+ const transports = createTransports(mcpConfig);
42
15
  logger.info(`Created ${Object.keys(transports).length} transports`);
43
16
  // Create clients for each transport
44
17
  const clients = await createClients(transports);
45
18
  logger.info(`Created ${Object.keys(clients).length} clients`);
46
- // Collect capabilities and register handlers
47
- await collectAndRegisterCapabilities(clients, server);
48
- // Initialize the configuration reload service
49
- configReloadService.initialize(server, transports);
50
- logger.info('Configuration reload service initialized');
19
+ const serverManager = ServerManager.getInstance({ name: MCP_SERVER_NAME, version: MCP_SERVER_VERSION }, { capabilities: MCP_SERVER_CAPABILITIES }, clients, transports);
51
20
  logger.info('Server setup completed successfully');
52
- return server;
21
+ return serverManager;
53
22
  }
54
23
  catch (error) {
55
24
  logger.error(`Failed to set up server: ${error}`);
56
25
  throw error;
57
26
  }
58
27
  }
59
- // Set up the server and export it
60
- const server = await setupServer();
61
- export { server };
28
+ export { setupServer };
@@ -0,0 +1,78 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import logger from './logger/logger.js';
3
+ import configReloadService from './services/configReloadService.js';
4
+ import { setupCapabilities } from './capabilities/capabilityManager.js';
5
+ import { enhanceServerWithLogging } from './middleware/loggingMiddleware.js';
6
+ export class ServerManager {
7
+ static instance;
8
+ servers = new Map();
9
+ serverConfig;
10
+ serverCapabilities;
11
+ clients = {};
12
+ transports = {};
13
+ constructor(config, capabilities, clients, transports) {
14
+ this.serverConfig = config;
15
+ this.serverCapabilities = capabilities;
16
+ this.clients = clients;
17
+ this.transports = transports;
18
+ }
19
+ static getInstance(config, capabilities, clients, transports) {
20
+ if (!ServerManager.instance) {
21
+ ServerManager.instance = new ServerManager(config, capabilities, clients, transports);
22
+ }
23
+ return ServerManager.instance;
24
+ }
25
+ async connectTransport(transport, sessionId, tags) {
26
+ try {
27
+ // Create a new server instance for this transport
28
+ const server = new Server(this.serverConfig, this.serverCapabilities);
29
+ // Enhance server with logging middleware
30
+ enhanceServerWithLogging(server);
31
+ const serverInfo = {
32
+ server,
33
+ tags,
34
+ };
35
+ // Set up capabilities for this server instance
36
+ await setupCapabilities(this.clients, serverInfo);
37
+ // Initialize the configuration reload service
38
+ configReloadService.initialize(serverInfo, this.transports);
39
+ // Store the server instance
40
+ this.servers.set(sessionId, serverInfo);
41
+ // Connect the transport to the new server instance
42
+ await server.connect(transport);
43
+ logger.info(`Connected transport for session ${sessionId}`);
44
+ }
45
+ catch (error) {
46
+ logger.error(`Failed to connect transport for session ${sessionId}:`, error);
47
+ throw error;
48
+ }
49
+ }
50
+ disconnectTransport(sessionId) {
51
+ const server = this.servers.get(sessionId);
52
+ if (server) {
53
+ this.servers.delete(sessionId);
54
+ logger.info(`Disconnected transport for session ${sessionId}`);
55
+ }
56
+ }
57
+ getTransport(sessionId) {
58
+ return this.servers.get(sessionId)?.server.transport;
59
+ }
60
+ getTransports() {
61
+ const transports = new Map();
62
+ for (const [id, server] of this.servers.entries()) {
63
+ if (server.server.transport) {
64
+ transports.set(id, server.server.transport);
65
+ }
66
+ }
67
+ return transports;
68
+ }
69
+ getClientTransports() {
70
+ return this.transports;
71
+ }
72
+ getActiveTransportsCount() {
73
+ return this.servers.size;
74
+ }
75
+ getServer(sessionId) {
76
+ return this.servers.get(sessionId);
77
+ }
78
+ }
@@ -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
+ });