@myatkyawthu/mcp-connect 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.
- package/LICENSE +21 -0
- package/README.md +308 -0
- package/package.json +63 -0
- package/src/cli.js +170 -0
- package/src/defineMCP.js +72 -0
- package/src/index.js +35 -0
- package/src/server/mcpServer.js +236 -0
- package/src/types/mcp.js +108 -0
- package/src/utils/configValidation.js +381 -0
- package/src/utils/logger.js +290 -0
- package/src/utils/validation.js +78 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
import { sanitizeErrorMessage, validateToolArguments } from '../utils/validation.js';
|
|
6
|
+
|
|
7
|
+
// Default timeout for tool execution (30 seconds)
|
|
8
|
+
const DEFAULT_TOOL_TIMEOUT = 30000;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* MCP Connect Server class
|
|
12
|
+
* Handles MCP protocol communication and tool execution
|
|
13
|
+
*/
|
|
14
|
+
export class MCPConnectServer {
|
|
15
|
+
/**
|
|
16
|
+
* Create MCP Connect Server
|
|
17
|
+
* @param {import('../types/mcp.js').MCPConfig} config - MCP configuration
|
|
18
|
+
*/
|
|
19
|
+
constructor(config) {
|
|
20
|
+
/** @type {import('../types/mcp.js').MCPConfig} */
|
|
21
|
+
this.config = config;
|
|
22
|
+
/** @type {boolean} */
|
|
23
|
+
this.isShuttingDown = false;
|
|
24
|
+
|
|
25
|
+
// Create MCP SDK server for STDIO transport
|
|
26
|
+
/** @type {Server} */
|
|
27
|
+
this.server = new Server(
|
|
28
|
+
{
|
|
29
|
+
name: config.name,
|
|
30
|
+
version: config.version,
|
|
31
|
+
description: config.description,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
capabilities: {
|
|
35
|
+
tools: {},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
this.setupHandlers();
|
|
41
|
+
this.setupGracefulShutdown();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Set up graceful shutdown handlers
|
|
46
|
+
*/
|
|
47
|
+
setupGracefulShutdown() {
|
|
48
|
+
/**
|
|
49
|
+
* Shutdown handler
|
|
50
|
+
* @param {string} signal - Shutdown signal
|
|
51
|
+
*/
|
|
52
|
+
const shutdown = async (signal) => {
|
|
53
|
+
if (this.isShuttingDown) return;
|
|
54
|
+
this.isShuttingDown = true;
|
|
55
|
+
|
|
56
|
+
logger.serverShutdown(signal);
|
|
57
|
+
try {
|
|
58
|
+
// Give ongoing operations a chance to complete
|
|
59
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
60
|
+
process.exit(0);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
logger.error("Error during shutdown", error);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
68
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
69
|
+
process.on("uncaughtException", (error) => {
|
|
70
|
+
logger.error("Uncaught exception", error);
|
|
71
|
+
shutdown("uncaughtException");
|
|
72
|
+
});
|
|
73
|
+
process.on("unhandledRejection", (reason) => {
|
|
74
|
+
logger.error("Unhandled rejection", reason);
|
|
75
|
+
shutdown("unhandledRejection");
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Execute promise with timeout
|
|
81
|
+
* @template T
|
|
82
|
+
* @param {Promise<T>} promise - Promise to execute
|
|
83
|
+
* @param {number} timeoutMs - Timeout in milliseconds
|
|
84
|
+
* @param {string} operation - Operation name for error messages
|
|
85
|
+
* @returns {Promise<T>} Promise that resolves or rejects with timeout
|
|
86
|
+
*/
|
|
87
|
+
async withTimeout(promise, timeoutMs, operation) {
|
|
88
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
89
|
+
setTimeout(() => {
|
|
90
|
+
reject(new Error(`${operation} timed out after ${timeoutMs}ms`));
|
|
91
|
+
}, timeoutMs);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return Promise.race([promise, timeoutPromise]);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Set up MCP request handlers
|
|
99
|
+
*/
|
|
100
|
+
setupHandlers() {
|
|
101
|
+
// List available tools
|
|
102
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
103
|
+
return {
|
|
104
|
+
tools: this.config.tools.map((tool) => ({
|
|
105
|
+
name: tool.name,
|
|
106
|
+
description: tool.description,
|
|
107
|
+
inputSchema: tool.inputSchema,
|
|
108
|
+
})),
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Execute tool calls with proper error handling and timeout
|
|
113
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
114
|
+
const requestId = Math.random().toString(36).substring(2, 8);
|
|
115
|
+
|
|
116
|
+
if (this.isShuttingDown) {
|
|
117
|
+
throw new Error("Server is shutting down");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const { name, arguments: args } = request.params;
|
|
121
|
+
|
|
122
|
+
// Log incoming request
|
|
123
|
+
logger.traceRequest("tools/call", { name, args }, requestId);
|
|
124
|
+
|
|
125
|
+
// Validate request
|
|
126
|
+
if (!name || typeof name !== "string") {
|
|
127
|
+
const error = new Error("Tool name is required and must be a string");
|
|
128
|
+
logger.traceError("tools/call", error, requestId);
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const tool = this.config.tools.find((t) => t.name === name);
|
|
133
|
+
if (!tool) {
|
|
134
|
+
const error = new Error(`Tool "${name}" not found. Available tools: ${this.config.tools.map(t => t.name).join(", ")}`);
|
|
135
|
+
logger.traceError("tools/call", error, requestId);
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const startTime = Date.now();
|
|
140
|
+
logger.toolExecutionStart(name, args, requestId);
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
// Validate and sanitize arguments
|
|
144
|
+
const validatedArgs = validateToolArguments(args);
|
|
145
|
+
|
|
146
|
+
// Execute tool with timeout and performance tracking
|
|
147
|
+
const toolPromise = Promise.resolve(tool.handler(validatedArgs));
|
|
148
|
+
const result = await logger.timeAsync(
|
|
149
|
+
`Tool "${name}" execution`,
|
|
150
|
+
this.withTimeout(toolPromise, DEFAULT_TOOL_TIMEOUT, `Tool "${name}" execution`),
|
|
151
|
+
name
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// Handle different result types
|
|
155
|
+
let content;
|
|
156
|
+
if (typeof result === "string") {
|
|
157
|
+
content = result;
|
|
158
|
+
} else if (result === null || result === undefined) {
|
|
159
|
+
content = "null";
|
|
160
|
+
} else {
|
|
161
|
+
try {
|
|
162
|
+
content = JSON.stringify(result, null, 2);
|
|
163
|
+
} catch (jsonError) {
|
|
164
|
+
content = String(result);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const duration = Date.now() - startTime;
|
|
169
|
+
logger.toolExecutionEnd(name, duration, requestId);
|
|
170
|
+
logger.traceResponse("tools/call", { content }, requestId);
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
content: [
|
|
174
|
+
{
|
|
175
|
+
type: "text",
|
|
176
|
+
text: content,
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
};
|
|
180
|
+
} catch (error) {
|
|
181
|
+
const duration = Date.now() - startTime;
|
|
182
|
+
logger.toolExecutionError(name, error, duration, requestId);
|
|
183
|
+
logger.traceError("tools/call", error, requestId);
|
|
184
|
+
|
|
185
|
+
// Return MCP-compliant error with sanitized message
|
|
186
|
+
const sanitizedMessage = sanitizeErrorMessage(error);
|
|
187
|
+
throw new Error(`Tool "${name}" failed: ${sanitizedMessage}`);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Start the MCP server with STDIO transport
|
|
194
|
+
* @returns {Promise<void>} Promise that resolves when server starts
|
|
195
|
+
* @throws {Error} If server fails to start
|
|
196
|
+
*/
|
|
197
|
+
async start() {
|
|
198
|
+
try {
|
|
199
|
+
const transport = new StdioServerTransport();
|
|
200
|
+
|
|
201
|
+
// Add error handling for transport
|
|
202
|
+
transport.onclose = () => {
|
|
203
|
+
if (!this.isShuttingDown) {
|
|
204
|
+
logger.warn("STDIO transport closed unexpectedly");
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
transport.onerror = (error) => {
|
|
209
|
+
logger.error("STDIO transport error", error);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
await this.server.connect(transport);
|
|
213
|
+
logger.serverStarted(this.config.name, "STDIO", this.config.tools.length);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
logger.error("Failed to start MCP server", error);
|
|
216
|
+
|
|
217
|
+
if (error instanceof Error && error.message.includes("EACCES")) {
|
|
218
|
+
logger.error("Permission denied. Check file/directory permissions.");
|
|
219
|
+
} else if (error instanceof Error && error.message.includes("ENOENT")) {
|
|
220
|
+
logger.error("File or directory not found. Check your configuration.");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Stop the MCP server
|
|
229
|
+
* @returns {Promise<void>} Promise that resolves when server stops
|
|
230
|
+
*/
|
|
231
|
+
async stop() {
|
|
232
|
+
this.isShuttingDown = true;
|
|
233
|
+
// STDIO transport doesn't need explicit stopping
|
|
234
|
+
logger.info("MCP server stopped");
|
|
235
|
+
}
|
|
236
|
+
}
|
package/src/types/mcp.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP-compliant types using official SDK
|
|
3
|
+
* This file contains JSDoc type definitions for MCP-Connect
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {Object} MCPTool
|
|
8
|
+
* @property {string} name - Tool name
|
|
9
|
+
* @property {string} description - Tool description
|
|
10
|
+
* @property {Record<string, any>} inputSchema - JSON Schema for input validation
|
|
11
|
+
* @property {function(Record<string, any>): Promise<any>|any} handler - Tool execution function
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} MCPConfig
|
|
16
|
+
* @property {string} name - Server name
|
|
17
|
+
* @property {string} version - Server version
|
|
18
|
+
* @property {string} [description] - Server description
|
|
19
|
+
* @property {MCPTool[]} tools - Array of MCP tools
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} MCPServerOptions
|
|
24
|
+
* @property {MCPConfig} config - MCP configuration
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Legacy types for backward compatibility
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {function(...any[]): Promise<any>|any} ToolFunction
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Tool definition - supports both tuple and object formats
|
|
37
|
+
* @typedef {[string, ToolFunction]|{name: string, handler: ToolFunction, description?: string, schema?: Record<string, any>}} ToolDefinition
|
|
38
|
+
*
|
|
39
|
+
* Tuple format: [name, handler]
|
|
40
|
+
* @example ['hello', async (args) => `Hello ${args.name}!`]
|
|
41
|
+
*
|
|
42
|
+
* Object format: {name, handler, description?, schema?}
|
|
43
|
+
* @example {
|
|
44
|
+
* name: 'hello',
|
|
45
|
+
* handler: async (args) => `Hello ${args.name}!`,
|
|
46
|
+
* description: 'Greet a user',
|
|
47
|
+
* schema: { type: 'object', properties: { name: { type: 'string' } } }
|
|
48
|
+
* }
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
// Runtime validation functions for type checking
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Validate MCPTool object
|
|
55
|
+
* @param {any} tool - Tool to validate
|
|
56
|
+
* @returns {boolean} Whether the tool is valid
|
|
57
|
+
*/
|
|
58
|
+
export function isValidMCPTool(tool) {
|
|
59
|
+
return (
|
|
60
|
+
tool &&
|
|
61
|
+
typeof tool === 'object' &&
|
|
62
|
+
typeof tool.name === 'string' &&
|
|
63
|
+
typeof tool.description === 'string' &&
|
|
64
|
+
typeof tool.inputSchema === 'object' &&
|
|
65
|
+
typeof tool.handler === 'function'
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Validate MCPConfig object
|
|
71
|
+
* @param {any} config - Config to validate
|
|
72
|
+
* @returns {boolean} Whether the config is valid
|
|
73
|
+
*/
|
|
74
|
+
export function isValidMCPConfig(config) {
|
|
75
|
+
return (
|
|
76
|
+
config &&
|
|
77
|
+
typeof config === 'object' &&
|
|
78
|
+
typeof config.name === 'string' &&
|
|
79
|
+
typeof config.version === 'string' &&
|
|
80
|
+
Array.isArray(config.tools) &&
|
|
81
|
+
config.tools.every(isValidMCPTool)
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Validate ToolDefinition (tuple or object format)
|
|
87
|
+
* @param {any} toolDef - Tool definition to validate
|
|
88
|
+
* @returns {boolean} Whether the tool definition is valid
|
|
89
|
+
*/
|
|
90
|
+
export function isValidToolDefinition(toolDef) {
|
|
91
|
+
if (Array.isArray(toolDef)) {
|
|
92
|
+
// Tuple format: [name, handler]
|
|
93
|
+
return (
|
|
94
|
+
toolDef.length === 2 &&
|
|
95
|
+
typeof toolDef[0] === 'string' &&
|
|
96
|
+
typeof toolDef[1] === 'function'
|
|
97
|
+
);
|
|
98
|
+
} else if (toolDef && typeof toolDef === 'object') {
|
|
99
|
+
// Object format: {name, handler, description?, schema?}
|
|
100
|
+
return (
|
|
101
|
+
typeof toolDef.name === 'string' &&
|
|
102
|
+
typeof toolDef.handler === 'function' &&
|
|
103
|
+
(toolDef.description === undefined || typeof toolDef.description === 'string') &&
|
|
104
|
+
(toolDef.schema === undefined || (typeof toolDef.schema === 'object' && toolDef.schema !== null))
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|