@1mcp/agent 0.1.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.
- package/LICENSE +201 -0
- package/README.md +115 -0
- package/build/capabilities/capabilityManager.js +41 -0
- package/build/client.js +18 -0
- package/build/clients/clientManager.js +79 -0
- package/build/config/configManager.js +108 -0
- package/build/config/transportConfig.js +39 -0
- package/build/constants.js +31 -0
- package/build/handlers/notificationHandlers.js +60 -0
- package/build/handlers/requestHandlers.js +218 -0
- package/build/index.js +149 -0
- package/build/logger.js +98 -0
- package/build/mcpTransport.js +122 -0
- package/build/server.js +61 -0
- package/build/services/configReloadService.js +98 -0
- package/build/utils/errorHandling.js +96 -0
- package/package.json +55 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { CancelledNotificationSchema, ProgressNotificationSchema, LoggingMessageNotificationSchema, ResourceUpdatedNotificationSchema, ResourceListChangedNotificationSchema, ToolListChangedNotificationSchema, PromptListChangedNotificationSchema, InitializedNotificationSchema, RootsListChangedNotificationSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import logger from '../logger.js';
|
|
3
|
+
import { withErrorHandling } from '../utils/errorHandling.js';
|
|
4
|
+
/**
|
|
5
|
+
* Sets up client-to-server notification handlers
|
|
6
|
+
* @param clients Record of client instances
|
|
7
|
+
* @param server The MCP server instance
|
|
8
|
+
*/
|
|
9
|
+
export function setupClientToServerNotifications(clients, server) {
|
|
10
|
+
const clientNotificationSchemas = [
|
|
11
|
+
CancelledNotificationSchema,
|
|
12
|
+
ProgressNotificationSchema,
|
|
13
|
+
LoggingMessageNotificationSchema,
|
|
14
|
+
ResourceUpdatedNotificationSchema,
|
|
15
|
+
ResourceListChangedNotificationSchema,
|
|
16
|
+
ToolListChangedNotificationSchema,
|
|
17
|
+
PromptListChangedNotificationSchema,
|
|
18
|
+
];
|
|
19
|
+
for (const [name, client] of Object.entries(clients)) {
|
|
20
|
+
clientNotificationSchemas.forEach((schema) => {
|
|
21
|
+
client.setNotificationHandler(schema, withErrorHandling(async (notification) => {
|
|
22
|
+
logger.info(`Received notification in client: ${name} ${JSON.stringify(notification)}`);
|
|
23
|
+
server.notification({
|
|
24
|
+
...notification,
|
|
25
|
+
params: {
|
|
26
|
+
...notification.params,
|
|
27
|
+
server: name,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
}, `Error handling client notification from ${name}`));
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Sets up server-to-client notification handlers
|
|
36
|
+
* @param clients Record of client instances
|
|
37
|
+
* @param server The MCP server instance
|
|
38
|
+
*/
|
|
39
|
+
export function setupServerToClientNotifications(clients, server) {
|
|
40
|
+
const serverNotificationSchemas = [
|
|
41
|
+
CancelledNotificationSchema,
|
|
42
|
+
ProgressNotificationSchema,
|
|
43
|
+
InitializedNotificationSchema,
|
|
44
|
+
RootsListChangedNotificationSchema,
|
|
45
|
+
];
|
|
46
|
+
for (const [name, client] of Object.entries(clients)) {
|
|
47
|
+
serverNotificationSchemas.forEach((schema) => {
|
|
48
|
+
server.setNotificationHandler(schema, withErrorHandling(async (notification) => {
|
|
49
|
+
logger.info(`Received notification in server: ${name} ${JSON.stringify(notification)}`);
|
|
50
|
+
client.notification({
|
|
51
|
+
...notification,
|
|
52
|
+
params: {
|
|
53
|
+
...notification.params,
|
|
54
|
+
client: name,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}, `Error handling server notification to ${name}`));
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourceTemplatesRequestSchema, SubscribeRequestSchema, UnsubscribeRequestSchema, SetLevelRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import logger from '../logger.js';
|
|
3
|
+
import { setLogLevel } from '../logger.js';
|
|
4
|
+
import { MCP_URI_SEPARATOR, MCP_SERVER_NAME } from '../constants.js';
|
|
5
|
+
import { executeClientOperation } from '../clients/clientManager.js';
|
|
6
|
+
import { ProxyError, parseUri, withErrorHandling } from '../utils/errorHandling.js';
|
|
7
|
+
/**
|
|
8
|
+
* Sends a partial failure notification to inform clients about backend failures
|
|
9
|
+
* @param server The MCP server instance
|
|
10
|
+
* @param operation The operation that partially failed
|
|
11
|
+
* @param failedClients Array of failed clients with error details
|
|
12
|
+
*/
|
|
13
|
+
function sendPartialFailureNotification(server, operation, failedClients) {
|
|
14
|
+
if (failedClients.length === 0)
|
|
15
|
+
return;
|
|
16
|
+
server.notification({
|
|
17
|
+
method: 'notifications/message',
|
|
18
|
+
params: {
|
|
19
|
+
level: 'warning',
|
|
20
|
+
message: `Partial failure during ${operation}`,
|
|
21
|
+
logger: MCP_SERVER_NAME,
|
|
22
|
+
data: {
|
|
23
|
+
operation,
|
|
24
|
+
failedClients,
|
|
25
|
+
timestamp: new Date().toISOString(),
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Registers all request handlers based on available capabilities
|
|
32
|
+
* @param clients Record of client instances
|
|
33
|
+
* @param server The MCP server instance
|
|
34
|
+
* @param capabilities The server capabilities
|
|
35
|
+
*/
|
|
36
|
+
export function registerRequestHandlers(clients, server, capabilities) {
|
|
37
|
+
// Register logging level handler
|
|
38
|
+
server.setRequestHandler(SetLevelRequestSchema, async (request) => {
|
|
39
|
+
setLogLevel(request.params.level);
|
|
40
|
+
return {};
|
|
41
|
+
});
|
|
42
|
+
// Register resource-related handlers if capability is available
|
|
43
|
+
if (capabilities.resources) {
|
|
44
|
+
registerResourceHandlers(clients, server);
|
|
45
|
+
}
|
|
46
|
+
// Register tool-related handlers if capability is available
|
|
47
|
+
if (capabilities.tools) {
|
|
48
|
+
registerToolHandlers(clients, server);
|
|
49
|
+
}
|
|
50
|
+
// Register prompt-related handlers if capability is available
|
|
51
|
+
if (capabilities.prompts) {
|
|
52
|
+
registerPromptHandlers(clients, server);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Registers resource-related request handlers
|
|
57
|
+
* @param clients Record of client instances
|
|
58
|
+
* @param server The MCP server instance
|
|
59
|
+
*/
|
|
60
|
+
function registerResourceHandlers(clients, server) {
|
|
61
|
+
// List Resources handler
|
|
62
|
+
server.setRequestHandler(ListResourcesRequestSchema, withErrorHandling(async (request) => {
|
|
63
|
+
const resources = [];
|
|
64
|
+
const failedClients = [];
|
|
65
|
+
for (const [name, client] of Object.entries(clients)) {
|
|
66
|
+
logger.info(`Listing resources for ${name}`);
|
|
67
|
+
try {
|
|
68
|
+
const result = await client.listResources(request.params);
|
|
69
|
+
resources.push(...result.resources.map((resource) => ({
|
|
70
|
+
uri: `${name}${MCP_URI_SEPARATOR}${resource.uri}`,
|
|
71
|
+
name: resource.name,
|
|
72
|
+
description: resource.description,
|
|
73
|
+
mimeType: resource.mimeType,
|
|
74
|
+
})));
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
logger.error(`Error listing resources for ${name}: ${error}`);
|
|
78
|
+
failedClients.push({ name, error: error instanceof Error ? error.message : String(error) });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// 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)));
|
|
84
|
+
}
|
|
85
|
+
// Send notification about partial failures
|
|
86
|
+
if (failedClients.length > 0) {
|
|
87
|
+
sendPartialFailureNotification(server, 'listResources', failedClients);
|
|
88
|
+
}
|
|
89
|
+
return { resources };
|
|
90
|
+
}, 'Error listing resources'));
|
|
91
|
+
// List Resource Templates handler
|
|
92
|
+
server.setRequestHandler(ListResourceTemplatesRequestSchema, withErrorHandling(async (request) => {
|
|
93
|
+
const resourceTemplates = [];
|
|
94
|
+
const failedClients = [];
|
|
95
|
+
for (const [name, client] of Object.entries(clients)) {
|
|
96
|
+
logger.info(`Listing resource templates for ${name}`);
|
|
97
|
+
try {
|
|
98
|
+
const result = await client.listResourceTemplates(request.params);
|
|
99
|
+
resourceTemplates.push(...result.resourceTemplates.map((template) => ({
|
|
100
|
+
uriTemplate: `${name}${MCP_URI_SEPARATOR}${template.uriTemplate}`,
|
|
101
|
+
name: template.name,
|
|
102
|
+
description: template.description,
|
|
103
|
+
mimeType: template.mimeType,
|
|
104
|
+
})));
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
logger.error(`Error listing resource templates for ${name}: ${error}`);
|
|
108
|
+
failedClients.push({ name, error: error instanceof Error ? error.message : String(error) });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// 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)));
|
|
114
|
+
}
|
|
115
|
+
// Send notification about partial failures
|
|
116
|
+
if (failedClients.length > 0) {
|
|
117
|
+
sendPartialFailureNotification(server, 'listResourceTemplates', failedClients);
|
|
118
|
+
}
|
|
119
|
+
return { resourceTemplates };
|
|
120
|
+
}, 'Error listing resource templates'));
|
|
121
|
+
// Subscribe Resource handler
|
|
122
|
+
server.setRequestHandler(SubscribeRequestSchema, withErrorHandling(async (request) => {
|
|
123
|
+
const { clientName, resourceName } = parseUri(request.params.uri, MCP_URI_SEPARATOR);
|
|
124
|
+
return executeClientOperation(clients, clientName, (client) => client.subscribeResource({ ...request.params, uri: resourceName }));
|
|
125
|
+
}, 'Error subscribing to resource'));
|
|
126
|
+
// Unsubscribe Resource handler
|
|
127
|
+
server.setRequestHandler(UnsubscribeRequestSchema, withErrorHandling(async (request) => {
|
|
128
|
+
const { clientName, resourceName } = parseUri(request.params.uri, MCP_URI_SEPARATOR);
|
|
129
|
+
return executeClientOperation(clients, clientName, (client) => client.unsubscribeResource({ ...request.params, uri: resourceName }));
|
|
130
|
+
}, 'Error unsubscribing from resource'));
|
|
131
|
+
// Read Resource handler
|
|
132
|
+
server.setRequestHandler(ReadResourceRequestSchema, withErrorHandling(async (request) => {
|
|
133
|
+
const { clientName, resourceName } = parseUri(request.params.uri, MCP_URI_SEPARATOR);
|
|
134
|
+
return executeClientOperation(clients, clientName, (client) => client.readResource({ ...request.params, uri: resourceName }));
|
|
135
|
+
}, 'Error reading resource'));
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Registers tool-related request handlers
|
|
139
|
+
* @param clients Record of client instances
|
|
140
|
+
* @param server The MCP server instance
|
|
141
|
+
*/
|
|
142
|
+
function registerToolHandlers(clients, server) {
|
|
143
|
+
// List Tools handler
|
|
144
|
+
server.setRequestHandler(ListToolsRequestSchema, withErrorHandling(async (request) => {
|
|
145
|
+
const tools = [];
|
|
146
|
+
const failedClients = [];
|
|
147
|
+
for (const [name, client] of Object.entries(clients)) {
|
|
148
|
+
logger.info(`Listing tools for ${name}`);
|
|
149
|
+
try {
|
|
150
|
+
const result = await client.listTools(request.params);
|
|
151
|
+
tools.push(...result.tools.map((tool) => ({
|
|
152
|
+
name: `${name}${MCP_URI_SEPARATOR}${tool.name}`,
|
|
153
|
+
description: tool.description,
|
|
154
|
+
inputSchema: tool.inputSchema,
|
|
155
|
+
})));
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
logger.error(`Error listing tools for ${name}: ${error}`);
|
|
159
|
+
failedClients.push({ name, error: error instanceof Error ? error.message : String(error) });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// 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)));
|
|
165
|
+
}
|
|
166
|
+
// Send notification about partial failures
|
|
167
|
+
if (failedClients.length > 0) {
|
|
168
|
+
sendPartialFailureNotification(server, 'listTools', failedClients);
|
|
169
|
+
}
|
|
170
|
+
return { tools };
|
|
171
|
+
}, 'Error listing tools'));
|
|
172
|
+
// Call Tool handler
|
|
173
|
+
server.setRequestHandler(CallToolRequestSchema, withErrorHandling(async (request) => {
|
|
174
|
+
const { clientName, resourceName: toolName } = parseUri(request.params.name, MCP_URI_SEPARATOR);
|
|
175
|
+
return executeClientOperation(clients, clientName, (client) => client.callTool({ ...request.params, name: toolName }));
|
|
176
|
+
}, 'Error calling tool'));
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Registers prompt-related request handlers
|
|
180
|
+
* @param clients Record of client instances
|
|
181
|
+
* @param server The MCP server instance
|
|
182
|
+
*/
|
|
183
|
+
function registerPromptHandlers(clients, server) {
|
|
184
|
+
// List Prompts handler
|
|
185
|
+
server.setRequestHandler(ListPromptsRequestSchema, withErrorHandling(async (request) => {
|
|
186
|
+
const prompts = [];
|
|
187
|
+
const failedClients = [];
|
|
188
|
+
for (const [name, client] of Object.entries(clients)) {
|
|
189
|
+
logger.info(`Listing prompts for ${name}`);
|
|
190
|
+
try {
|
|
191
|
+
const result = await client.listPrompts(request.params);
|
|
192
|
+
prompts.push(...result.prompts.map((prompt) => ({
|
|
193
|
+
name: `${name}${MCP_URI_SEPARATOR}${prompt.name}`,
|
|
194
|
+
description: prompt.description,
|
|
195
|
+
arguments: prompt.arguments,
|
|
196
|
+
})));
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
logger.error(`Error listing prompts for ${name}: ${error}`);
|
|
200
|
+
failedClients.push({ name, error: error instanceof Error ? error.message : String(error) });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// 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)));
|
|
206
|
+
}
|
|
207
|
+
// Send notification about partial failures
|
|
208
|
+
if (failedClients.length > 0) {
|
|
209
|
+
sendPartialFailureNotification(server, 'listPrompts', failedClients);
|
|
210
|
+
}
|
|
211
|
+
return { prompts };
|
|
212
|
+
}, 'Error listing prompts'));
|
|
213
|
+
// Get Prompt handler
|
|
214
|
+
server.setRequestHandler(GetPromptRequestSchema, withErrorHandling(async (request) => {
|
|
215
|
+
const { clientName, resourceName: promptName } = parseUri(request.params.name, MCP_URI_SEPARATOR);
|
|
216
|
+
return executeClientOperation(clients, clientName, (client) => client.getPrompt({ ...request.params, name: promptName }));
|
|
217
|
+
}, 'Error getting prompt'));
|
|
218
|
+
}
|
package/build/index.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import yargs from 'yargs';
|
|
4
|
+
import { hideBin } from 'yargs/helpers';
|
|
5
|
+
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
6
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
|
+
import { server } from './server.js';
|
|
8
|
+
import logger, { setMCPTransportConnected } from './logger.js';
|
|
9
|
+
import { PORT, SSE_ENDPOINT, MESSAGES_ENDPOINT, ERROR_CODES } from './constants.js';
|
|
10
|
+
import configReloadService from './services/configReloadService.js';
|
|
11
|
+
// Parse command line arguments
|
|
12
|
+
const argv = yargs(hideBin(process.argv))
|
|
13
|
+
.usage('Usage: $0 [options]')
|
|
14
|
+
.options({
|
|
15
|
+
transport: {
|
|
16
|
+
alias: 't',
|
|
17
|
+
describe: 'Transport type to use (stdio or http)',
|
|
18
|
+
type: 'string',
|
|
19
|
+
choices: ['stdio', 'http'],
|
|
20
|
+
default: 'http',
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
.help()
|
|
24
|
+
.alias('help', 'h')
|
|
25
|
+
.parseSync();
|
|
26
|
+
const app = express();
|
|
27
|
+
// app.use(express.json());
|
|
28
|
+
// app.use(express.urlencoded({ extended: true }));
|
|
29
|
+
const transportMap = new Map();
|
|
30
|
+
// Add error handling middleware
|
|
31
|
+
app.use((err, req, res, next) => {
|
|
32
|
+
logger.error('Express error:', err);
|
|
33
|
+
res.status(500).json({
|
|
34
|
+
error: {
|
|
35
|
+
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
|
36
|
+
message: 'Internal server error',
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
app.get(SSE_ENDPOINT, async (req, res) => {
|
|
41
|
+
try {
|
|
42
|
+
logger.info('sse', { query: req.query, headers: req.headers });
|
|
43
|
+
const transport = new SSEServerTransport(MESSAGES_ENDPOINT, res);
|
|
44
|
+
await server.connect(transport);
|
|
45
|
+
transportMap.set(transport.sessionId, transport);
|
|
46
|
+
// Update MCP transport connection status when a client connects
|
|
47
|
+
if (transportMap.size === 1) {
|
|
48
|
+
setMCPTransportConnected(true);
|
|
49
|
+
logger.info('First client connected, enabling MCP logging transport');
|
|
50
|
+
}
|
|
51
|
+
transport.onclose = () => {
|
|
52
|
+
transportMap.delete(transport.sessionId);
|
|
53
|
+
logger.info('transport closed', transport.sessionId);
|
|
54
|
+
// Update MCP transport connection status when all clients disconnect
|
|
55
|
+
if (transportMap.size === 0) {
|
|
56
|
+
setMCPTransportConnected(false);
|
|
57
|
+
logger.info('All clients disconnected, disabling MCP logging transport');
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
logger.error('SSE connection error:', error);
|
|
63
|
+
res.status(500).end();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
app.post(MESSAGES_ENDPOINT, async (req, res) => {
|
|
67
|
+
try {
|
|
68
|
+
const sessionId = req.query.sessionId;
|
|
69
|
+
if (!sessionId) {
|
|
70
|
+
res.status(400).json({
|
|
71
|
+
error: {
|
|
72
|
+
code: ERROR_CODES.INVALID_PARAMS,
|
|
73
|
+
message: 'Invalid params: sessionId is required',
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
logger.info('message', { body: req.body, sessionId });
|
|
79
|
+
const transport = transportMap.get(sessionId);
|
|
80
|
+
if (transport) {
|
|
81
|
+
await transport.handlePostMessage(req, res);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
res.status(404).json({
|
|
85
|
+
error: {
|
|
86
|
+
code: ERROR_CODES.TRANSPORT_NOT_FOUND,
|
|
87
|
+
message: 'Transport not found',
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
logger.error('Message handling error:', error);
|
|
93
|
+
res.status(500).json({
|
|
94
|
+
error: {
|
|
95
|
+
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
|
96
|
+
message: 'Internal server error',
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
/**
|
|
102
|
+
* Set up graceful shutdown handling
|
|
103
|
+
*/
|
|
104
|
+
function setupGracefulShutdown() {
|
|
105
|
+
const shutdown = async () => {
|
|
106
|
+
logger.info('Shutting down server...');
|
|
107
|
+
// Stop the configuration reload service
|
|
108
|
+
configReloadService.stop();
|
|
109
|
+
// Close all transports
|
|
110
|
+
for (const [sessionId, transport] of transportMap.entries()) {
|
|
111
|
+
try {
|
|
112
|
+
transport.close();
|
|
113
|
+
logger.info(`Closed transport: ${sessionId}`);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
logger.error(`Error closing transport ${sessionId}: ${error}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
logger.info('Server shutdown complete');
|
|
120
|
+
process.exit(0);
|
|
121
|
+
};
|
|
122
|
+
// Handle various signals for graceful shutdown
|
|
123
|
+
process.on('SIGINT', shutdown);
|
|
124
|
+
process.on('SIGTERM', shutdown);
|
|
125
|
+
process.on('SIGHUP', shutdown);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Start the server using the specified transport.
|
|
129
|
+
*/
|
|
130
|
+
async function main() {
|
|
131
|
+
// Set up graceful shutdown handling
|
|
132
|
+
setupGracefulShutdown();
|
|
133
|
+
if (argv.transport === 'stdio') {
|
|
134
|
+
// Use stdio transport
|
|
135
|
+
const transport = new StdioServerTransport();
|
|
136
|
+
await server.connect(transport);
|
|
137
|
+
logger.info('Server started with stdio transport');
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
// Use HTTP/SSE transport
|
|
141
|
+
app.listen(PORT, () => {
|
|
142
|
+
logger.info(`Server is running on port ${PORT} with HTTP/SSE transport`);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
main().catch((error) => {
|
|
147
|
+
logger.error('Server error:', error);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
});
|
package/build/logger.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import winston from 'winston';
|
|
4
|
+
import { MCPTransport } from './mcpTransport.js';
|
|
5
|
+
// Ensure logs directory exists
|
|
6
|
+
const logsDir = path.join(process.cwd(), 'logs');
|
|
7
|
+
if (!fs.existsSync(logsDir)) {
|
|
8
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
9
|
+
}
|
|
10
|
+
// Map MCP log levels to Winston log levels
|
|
11
|
+
const MCP_TO_WINSTON_LEVEL = {
|
|
12
|
+
debug: 'debug',
|
|
13
|
+
info: 'info',
|
|
14
|
+
notice: 'info',
|
|
15
|
+
warning: 'warn',
|
|
16
|
+
error: 'error',
|
|
17
|
+
critical: 'error',
|
|
18
|
+
alert: 'error',
|
|
19
|
+
emergency: 'error',
|
|
20
|
+
};
|
|
21
|
+
// Custom format for console and file output
|
|
22
|
+
const customFormat = winston.format.combine(winston.format.timestamp(), winston.format.printf(({ timestamp, level, message, ...meta }) => {
|
|
23
|
+
const metaStr = Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : '';
|
|
24
|
+
return `${timestamp} [${level.toUpperCase()}] ${message}${metaStr}`;
|
|
25
|
+
}));
|
|
26
|
+
// Create the logger without the MCP transport initially
|
|
27
|
+
const logger = winston.createLogger({
|
|
28
|
+
level: 'info',
|
|
29
|
+
format: customFormat,
|
|
30
|
+
defaultMeta: { service: '1mcp' },
|
|
31
|
+
transports: [
|
|
32
|
+
new winston.transports.Console({
|
|
33
|
+
format: customFormat,
|
|
34
|
+
}),
|
|
35
|
+
//
|
|
36
|
+
// - Write all logs with importance level of `error` or higher to `error.log`
|
|
37
|
+
// (i.e., error, fatal, but not other levels)
|
|
38
|
+
//
|
|
39
|
+
new winston.transports.File({
|
|
40
|
+
filename: path.join(logsDir, 'error.log'),
|
|
41
|
+
level: 'error',
|
|
42
|
+
format: customFormat,
|
|
43
|
+
}),
|
|
44
|
+
//
|
|
45
|
+
// - Write all logs with importance level of `info` or higher to `combined.log`
|
|
46
|
+
// (i.e., fatal, error, warn, and info, but not trace)
|
|
47
|
+
//
|
|
48
|
+
new winston.transports.File({
|
|
49
|
+
filename: path.join(logsDir, 'combined.log'),
|
|
50
|
+
format: customFormat,
|
|
51
|
+
}),
|
|
52
|
+
],
|
|
53
|
+
// Prevent logger from exiting on error
|
|
54
|
+
exitOnError: false,
|
|
55
|
+
});
|
|
56
|
+
// Store a reference to the MCP transport
|
|
57
|
+
let mcpTransport = null;
|
|
58
|
+
/**
|
|
59
|
+
* Adds the MCP transport to the logger
|
|
60
|
+
* @param server The MCP server instance
|
|
61
|
+
* @param loggerName Optional name for the logger in MCP notifications
|
|
62
|
+
*/
|
|
63
|
+
export function addMCPTransport(server, loggerName) {
|
|
64
|
+
// Add the MCP transport to the logger
|
|
65
|
+
mcpTransport = new MCPTransport({
|
|
66
|
+
server,
|
|
67
|
+
loggerName: loggerName || '1mcp-agent',
|
|
68
|
+
level: 'info',
|
|
69
|
+
});
|
|
70
|
+
logger.add(mcpTransport);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Set the connection status of the MCP transport
|
|
74
|
+
* @param connected Whether the server is connected
|
|
75
|
+
*/
|
|
76
|
+
export function setMCPTransportConnected(connected) {
|
|
77
|
+
if (mcpTransport) {
|
|
78
|
+
mcpTransport.setConnected(connected);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Set the log level for the logger
|
|
83
|
+
* @param mcpLevel The MCP log level to set
|
|
84
|
+
*/
|
|
85
|
+
export function setLogLevel(mcpLevel) {
|
|
86
|
+
// Convert MCP log level to Winston log level
|
|
87
|
+
const winstonLevel = MCP_TO_WINSTON_LEVEL[mcpLevel] || 'info';
|
|
88
|
+
// Set the log level for all transports
|
|
89
|
+
logger.level = winstonLevel;
|
|
90
|
+
logger.transports.forEach((transport) => {
|
|
91
|
+
transport.level = winstonLevel;
|
|
92
|
+
});
|
|
93
|
+
// Also set the level for the MCP transport if it exists
|
|
94
|
+
if (mcpTransport) {
|
|
95
|
+
mcpTransport.level = winstonLevel;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export default logger;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import Transport from 'winston-transport';
|
|
2
|
+
// Map Winston log levels to MCP log levels
|
|
3
|
+
const LOG_LEVEL_MAP = {
|
|
4
|
+
error: 'error',
|
|
5
|
+
warn: 'warning',
|
|
6
|
+
info: 'info',
|
|
7
|
+
verbose: 'debug',
|
|
8
|
+
debug: 'debug',
|
|
9
|
+
silly: 'debug',
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Winston transport for sending logs to MCP clients
|
|
13
|
+
*/
|
|
14
|
+
export class MCPTransport extends Transport {
|
|
15
|
+
server;
|
|
16
|
+
loggerName;
|
|
17
|
+
connected = false;
|
|
18
|
+
constructor(options) {
|
|
19
|
+
super(options);
|
|
20
|
+
this.server = options.server;
|
|
21
|
+
this.loggerName = options.loggerName || '1mcp-agent';
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Set the connection status of the transport
|
|
25
|
+
* @param connected Whether the server is connected
|
|
26
|
+
*/
|
|
27
|
+
setConnected(connected) {
|
|
28
|
+
this.connected = connected;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Send a log notification to the MCP client
|
|
32
|
+
* @param info The log info object
|
|
33
|
+
* @returns true if the notification was sent, false otherwise
|
|
34
|
+
*/
|
|
35
|
+
sendLogNotification(info) {
|
|
36
|
+
if (!this.server || !this.connected) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
// Map Winston log level to MCP log level
|
|
40
|
+
const level = LOG_LEVEL_MAP[info.level] || 'info';
|
|
41
|
+
const sanitizedData = this.sanitizeData(info);
|
|
42
|
+
// Extract message and metadata
|
|
43
|
+
const { message, level: _, ...meta } = sanitizedData;
|
|
44
|
+
// Create MCP logging notification
|
|
45
|
+
const notification = {
|
|
46
|
+
method: 'notifications/message',
|
|
47
|
+
params: {
|
|
48
|
+
level,
|
|
49
|
+
message: typeof message === 'string' ? message : JSON.stringify(message),
|
|
50
|
+
logger: this.loggerName,
|
|
51
|
+
data: Object.keys(meta).length > 0 ? meta : undefined,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
// Send notification to MCP clients
|
|
55
|
+
try {
|
|
56
|
+
this.server.notification(notification);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
console.error('Failed to send log notification to MCP client:', error);
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Sanitize log data to remove sensitive information
|
|
66
|
+
* @param data The data to sanitize
|
|
67
|
+
* @returns Sanitized data
|
|
68
|
+
*/
|
|
69
|
+
sanitizeData(data) {
|
|
70
|
+
if (typeof data !== 'object' || data === null) {
|
|
71
|
+
return data;
|
|
72
|
+
}
|
|
73
|
+
// List of potentially sensitive field names
|
|
74
|
+
const sensitiveFields = [
|
|
75
|
+
'password',
|
|
76
|
+
'token',
|
|
77
|
+
'secret',
|
|
78
|
+
'key',
|
|
79
|
+
'credential',
|
|
80
|
+
'auth',
|
|
81
|
+
'apiKey',
|
|
82
|
+
'api_key',
|
|
83
|
+
'accessToken',
|
|
84
|
+
'access_token',
|
|
85
|
+
'jwt',
|
|
86
|
+
'ssn',
|
|
87
|
+
'creditCard',
|
|
88
|
+
'credit_card',
|
|
89
|
+
'cvv',
|
|
90
|
+
'pin',
|
|
91
|
+
];
|
|
92
|
+
if (Array.isArray(data)) {
|
|
93
|
+
return data.map((item) => this.sanitizeData(item));
|
|
94
|
+
}
|
|
95
|
+
const sanitized = {};
|
|
96
|
+
for (const [key, value] of Object.entries(data)) {
|
|
97
|
+
// Check if this is a sensitive field
|
|
98
|
+
const isSensitive = sensitiveFields.some((field) => key.toLowerCase().includes(field.toLowerCase()));
|
|
99
|
+
if (isSensitive) {
|
|
100
|
+
sanitized[key] = '[REDACTED]';
|
|
101
|
+
}
|
|
102
|
+
else if (typeof value === 'object' && value !== null) {
|
|
103
|
+
sanitized[key] = this.sanitizeData(value);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
sanitized[key] = value;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return sanitized;
|
|
110
|
+
}
|
|
111
|
+
log(info, callback) {
|
|
112
|
+
setImmediate(() => {
|
|
113
|
+
this.emit('logged', info);
|
|
114
|
+
});
|
|
115
|
+
if (!this.connected) {
|
|
116
|
+
callback();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
this.sendLogNotification(info);
|
|
120
|
+
callback();
|
|
121
|
+
}
|
|
122
|
+
}
|