converse-mcp-server 1.0.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/src/router.js ADDED
@@ -0,0 +1,497 @@
1
+ /**
2
+ * Central Request Router
3
+ *
4
+ * Single orchestration point that dispatches MCP requests to tools with dependency injection.
5
+ * Handles tool lookup, error management, and consistent response formatting.
6
+ * Follows functional architecture with comprehensive error handling.
7
+ */
8
+
9
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
10
+ import { getContinuationStore } from './continuationStore.js';
11
+ import { getTools } from './tools/index.js';
12
+ import { getProviders } from './providers/index.js';
13
+ import { processUnifiedContext } from './utils/contextProcessor.js';
14
+ import { createLogger, startTimer } from './utils/logger.js';
15
+ import { debugError } from './utils/console.js';
16
+ import {
17
+ ConverseMCPError,
18
+ ToolError,
19
+ ValidationError,
20
+ createMCPErrorResponse,
21
+ withErrorHandler,
22
+ ERROR_CODES
23
+ } from './utils/errorHandler.js';
24
+
25
+ const logger = createLogger('router');
26
+
27
+ /**
28
+ * Router-specific error class
29
+ */
30
+ export class RouterError extends ConverseMCPError {
31
+ constructor(message, code = ERROR_CODES.ROUTER_ERROR, details = {}) {
32
+ super(message, code, details, 500);
33
+ this.name = 'RouterError';
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Standard error response format for consistent error handling
39
+ * @param {Error} error - The error that occurred
40
+ * @param {string} toolName - Name of the tool that failed
41
+ * @param {object} context - Additional context information
42
+ * @returns {object} Standardized error response
43
+ */
44
+ export function createErrorResponse(error, toolName = 'unknown', context = {}) {
45
+ return createMCPErrorResponse(error, toolName, context);
46
+ }
47
+
48
+ /**
49
+ * Validate tool exists and is callable
50
+ * @param {string} toolName - Name of the tool to validate
51
+ * @param {object} tools - Available tools registry
52
+ * @returns {object} Validation result
53
+ */
54
+ function validateTool(toolName, tools) {
55
+ if (!toolName || typeof toolName !== 'string') {
56
+ throw new RouterError(
57
+ 'Tool name must be a non-empty string',
58
+ 'INVALID_TOOL_NAME'
59
+ );
60
+ }
61
+
62
+ if (!tools[toolName]) {
63
+ const availableTools = Object.keys(tools);
64
+ throw new RouterError(
65
+ `Tool error: Unknown tool '${toolName}'. Available tools: ${availableTools.join(', ')}`,
66
+ 'UNKNOWN_TOOL',
67
+ { requestedTool: toolName, availableTools }
68
+ );
69
+ }
70
+
71
+ if (typeof tools[toolName] !== 'function') {
72
+ throw new RouterError(
73
+ `Tool ${toolName} is not callable`,
74
+ 'INVALID_TOOL_HANDLER',
75
+ { toolName, toolType: typeof tools[toolName] }
76
+ );
77
+ }
78
+
79
+ return {
80
+ isValid: true,
81
+ tool: tools[toolName]
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Enhanced dependency injection with error handling
87
+ * @param {object} config - Configuration object
88
+ * @returns {object} Dependencies object for tool injection
89
+ */
90
+ async function createDependencies(config) {
91
+ try {
92
+ const continuationStore = getContinuationStore();
93
+ const tools = getTools();
94
+ const providers = getProviders();
95
+
96
+ // Validate that we have the necessary dependencies
97
+ if (!continuationStore) {
98
+ throw new RouterError(
99
+ 'Failed to initialize continuation store',
100
+ 'DEPENDENCY_ERROR'
101
+ );
102
+ }
103
+
104
+ if (!tools || Object.keys(tools).length === 0) {
105
+ throw new RouterError(
106
+ 'No tools available - tools registry is empty',
107
+ 'NO_TOOLS_AVAILABLE'
108
+ );
109
+ }
110
+
111
+ if (!providers || Object.keys(providers).length === 0) {
112
+ throw new RouterError(
113
+ 'No providers available - providers registry is empty',
114
+ 'NO_PROVIDERS_AVAILABLE'
115
+ );
116
+ }
117
+
118
+ return {
119
+ config,
120
+ continuationStore,
121
+ providers,
122
+ contextProcessor: { processUnifiedContext },
123
+ router: {
124
+ createErrorResponse,
125
+ validateToolArguments,
126
+ },
127
+ };
128
+
129
+ } catch (error) {
130
+ debugError('Failed to create dependencies:', error);
131
+ throw error;
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Creates and configures the central router for handling MCP requests
137
+ * @param {object} server - MCP Server instance
138
+ * @param {object} config - Configuration object with provider settings
139
+ * @returns {Promise<void>}
140
+ */
141
+ export async function createRouter(server, config) {
142
+ const createRouterLogger = logger.operation('createRouter');
143
+ const timer = startTimer('router-initialization', 'router');
144
+
145
+ try {
146
+ createRouterLogger.info('Initializing router');
147
+
148
+ // Initialize dependencies with validation
149
+ const dependencies = await createDependencies(config);
150
+ const tools = getTools();
151
+
152
+ createRouterLogger.info(`Router initialized with ${Object.keys(tools).length} tools`);
153
+
154
+ // Register unified tool call handler with enhanced error handling
155
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
156
+ const toolTimer = startTimer('tool-execution', 'router');
157
+ const toolName = request.params?.name;
158
+ const toolArgs = request.params?.arguments || {};
159
+ const requestLogger = logger.operation(`tool-call:${toolName}`);
160
+
161
+ try {
162
+ requestLogger.info('Tool execution started', {
163
+ data: { toolName, argCount: Object.keys(toolArgs).length }
164
+ });
165
+
166
+ // Validate tool existence and callability
167
+ const { tool } = validateTool(toolName, tools);
168
+
169
+ // Validate tool arguments if schema is provided
170
+ if (tool.inputSchema) {
171
+ const isValidArgs = validateToolArguments(toolArgs, tool.inputSchema);
172
+ if (!isValidArgs) {
173
+ throw new ValidationError(
174
+ `Invalid arguments for tool ${toolName}`,
175
+ ERROR_CODES.INVALID_TOOL_ARGS,
176
+ {
177
+ providedArgs: Object.keys(toolArgs),
178
+ expectedSchema: tool.inputSchema
179
+ }
180
+ );
181
+ }
182
+ }
183
+
184
+ // Execute the tool with dependency injection
185
+ const result = await tool(toolArgs, dependencies);
186
+
187
+ const executionTime = toolTimer('completed');
188
+ requestLogger.info('Tool execution completed', {
189
+ data: { executionTime: `${executionTime}ms` }
190
+ });
191
+
192
+ // Ensure result has proper format
193
+ if (!result || !result.content) {
194
+ throw new ToolError(
195
+ `Tool ${toolName} returned invalid result format`,
196
+ ERROR_CODES.TOOL_EXECUTION_FAILED,
197
+ { result },
198
+ toolName
199
+ );
200
+ }
201
+
202
+ return result;
203
+
204
+ } catch (error) {
205
+ const executionTime = toolTimer('failed');
206
+ requestLogger.error('Tool execution failed', {
207
+ error,
208
+ data: { executionTime: `${executionTime}ms`, argCount: Object.keys(toolArgs).length }
209
+ });
210
+
211
+ return createErrorResponse(error, toolName, {
212
+ executionTime,
213
+ arguments: Object.keys(toolArgs),
214
+ requestId: request.id || 'unknown'
215
+ });
216
+ }
217
+ });
218
+
219
+ // Register enhanced list_tools handler
220
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
221
+ try {
222
+ const toolList = Object.entries(tools).map(([name, handler]) => {
223
+ const toolInfo = {
224
+ name,
225
+ description: handler.description || `${name} tool - no description provided`,
226
+ inputSchema: handler.inputSchema || {
227
+ type: 'object',
228
+ properties: {},
229
+ description: 'No input schema defined'
230
+ },
231
+ };
232
+
233
+ // Add additional metadata if available
234
+ if (handler.version) {
235
+ toolInfo.version = handler.version;
236
+ }
237
+ if (handler.category) {
238
+ toolInfo.category = handler.category;
239
+ }
240
+
241
+ return toolInfo;
242
+ });
243
+
244
+ return {
245
+ tools: toolList,
246
+ metadata: {
247
+ totalTools: toolList.length,
248
+ timestamp: new Date().toISOString(),
249
+ routerVersion: '1.0.0'
250
+ }
251
+ };
252
+
253
+ } catch (error) {
254
+ debugError('Error listing tools:', error);
255
+ throw new RouterError(
256
+ 'Failed to list available tools',
257
+ 'TOOLS_LIST_ERROR',
258
+ { error: error.message }
259
+ );
260
+ }
261
+ });
262
+
263
+ // Note: Custom health endpoint removed - MCP uses standard protocol methods only
264
+
265
+ timer('completed');
266
+ createRouterLogger.info('Router configured successfully', {
267
+ data: {
268
+ tools: Object.keys(tools).length,
269
+ providers: Object.keys(dependencies.providers).length,
270
+ continuationStore: dependencies.continuationStore.constructor.name,
271
+ environment: config.environment.nodeEnv
272
+ }
273
+ });
274
+
275
+ // Return router interface for testing purposes
276
+ return {
277
+ listTools: async () => {
278
+ const tools = getTools();
279
+ return {
280
+ tools: Object.entries(tools).map(([name, tool]) => {
281
+ const toolSchema = {
282
+ name,
283
+ description: tool.description || 'No description available'
284
+ };
285
+
286
+ if (tool.inputSchema) {
287
+ toolSchema.inputSchema = tool.inputSchema;
288
+ }
289
+
290
+ return toolSchema;
291
+ })
292
+ };
293
+ },
294
+
295
+ callTool: async (toolCall) => {
296
+ const toolName = toolCall.name;
297
+ const toolArgs = toolCall.arguments || {};
298
+
299
+ try {
300
+ // Validate tool existence and callability
301
+ const { tool } = validateTool(toolName, tools);
302
+
303
+ // Validate tool arguments if schema is provided
304
+ if (tool.inputSchema) {
305
+ const isValidArgs = validateToolArguments(toolArgs, tool.inputSchema);
306
+ if (!isValidArgs) {
307
+ throw new ValidationError(
308
+ `Invalid arguments for tool ${toolName}`,
309
+ ERROR_CODES.INVALID_TOOL_ARGS,
310
+ {
311
+ providedArgs: Object.keys(toolArgs),
312
+ expectedSchema: tool.inputSchema
313
+ }
314
+ );
315
+ }
316
+ }
317
+
318
+ // Execute the tool with dependency injection
319
+ return await tool(toolArgs, dependencies);
320
+ } catch (error) {
321
+ return createErrorResponse(error, toolName, {
322
+ arguments: toolArgs,
323
+ executionTime: 0,
324
+ requestId: 'test'
325
+ });
326
+ }
327
+ }
328
+ };
329
+
330
+ } catch (error) {
331
+ timer('failed');
332
+ createRouterLogger.error('Router initialization failed', { error });
333
+ throw new RouterError(
334
+ 'Router initialization failed',
335
+ ERROR_CODES.ROUTER_ERROR,
336
+ { originalError: error.message }
337
+ );
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Get JSON Schema type for a JavaScript value
343
+ * @param {any} value - The value to get type for
344
+ * @returns {string} JSON Schema type
345
+ */
346
+ function getJsonSchemaType(value) {
347
+ if (value === null) return 'null';
348
+ if (Array.isArray(value)) return 'array';
349
+ if (typeof value === 'number') {
350
+ return Number.isInteger(value) ? 'integer' : 'number';
351
+ }
352
+ return typeof value;
353
+ }
354
+
355
+ /**
356
+ * Check if a JavaScript value matches a JSON Schema type
357
+ * @param {any} value - The value to check
358
+ * @param {string} schemaType - The JSON Schema type to check against
359
+ * @returns {boolean} True if value matches the schema type
360
+ */
361
+ function isValidJsonSchemaType(value, schemaType) {
362
+ if (schemaType === 'null') return value === null;
363
+ if (schemaType === 'array') return Array.isArray(value);
364
+ if (schemaType === 'object') return value !== null && typeof value === 'object' && !Array.isArray(value);
365
+ if (schemaType === 'boolean') return typeof value === 'boolean';
366
+ if (schemaType === 'string') return typeof value === 'string';
367
+ if (schemaType === 'number') return typeof value === 'number';
368
+ if (schemaType === 'integer') return typeof value === 'number' && Number.isInteger(value);
369
+
370
+ // For unknown types, fall back to JavaScript typeof
371
+ return typeof value === schemaType;
372
+ }
373
+
374
+ /**
375
+ * Enhanced tool argument validation against schema
376
+ * @param {object} args - Tool arguments to validate
377
+ * @param {object} schema - JSON schema for validation
378
+ * @returns {boolean} True if arguments are valid
379
+ * @throws {RouterError} If validation fails with details
380
+ */
381
+ export function validateToolArguments(args, schema) {
382
+ try {
383
+ // If no schema provided, assume valid
384
+ if (!schema) {
385
+ return true;
386
+ }
387
+
388
+ // Basic type checking
389
+ if (schema.type === 'object' && (typeof args !== 'object' || args === null)) {
390
+ throw new RouterError(
391
+ 'Arguments must be an object',
392
+ 'INVALID_ARGUMENT_TYPE',
393
+ { expected: 'object', received: typeof args }
394
+ );
395
+ }
396
+
397
+ // Check required properties
398
+ if (schema.required && Array.isArray(schema.required)) {
399
+ const missing = schema.required.filter(key => !(key in args));
400
+ if (missing.length > 0) {
401
+ throw new RouterError(
402
+ `Validation error: Missing required arguments: ${missing.join(', ')}`,
403
+ 'MISSING_REQUIRED_ARGS',
404
+ { missing, provided: Object.keys(args) }
405
+ );
406
+ }
407
+ }
408
+
409
+ // Validate individual properties
410
+ if (schema.properties) {
411
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
412
+ if (key in args) {
413
+ const value = args[key];
414
+
415
+ // Basic type validation using JSON Schema type semantics
416
+ if (propSchema.type && !isValidJsonSchemaType(value, propSchema.type)) {
417
+ const actualType = getJsonSchemaType(value);
418
+ throw new RouterError(
419
+ `Argument '${key}' must be of type ${propSchema.type}`,
420
+ 'INVALID_ARGUMENT_TYPE',
421
+ {
422
+ argument: key,
423
+ expected: propSchema.type,
424
+ received: actualType
425
+ }
426
+ );
427
+ }
428
+
429
+ // String length validation
430
+ if (propSchema.type === 'string') {
431
+ if (propSchema.minLength && value.length < propSchema.minLength) {
432
+ throw new RouterError(
433
+ `Argument '${key}' must be at least ${propSchema.minLength} characters`,
434
+ 'ARGUMENT_TOO_SHORT',
435
+ { argument: key, minLength: propSchema.minLength, actual: value.length }
436
+ );
437
+ }
438
+ if (propSchema.maxLength && value.length > propSchema.maxLength) {
439
+ throw new RouterError(
440
+ `Argument '${key}' must be at most ${propSchema.maxLength} characters`,
441
+ 'ARGUMENT_TOO_LONG',
442
+ { argument: key, maxLength: propSchema.maxLength, actual: value.length }
443
+ );
444
+ }
445
+ }
446
+ }
447
+ }
448
+ }
449
+
450
+ return true;
451
+
452
+ } catch (error) {
453
+ if (error instanceof RouterError) {
454
+ throw error;
455
+ }
456
+ throw new RouterError(
457
+ `Argument validation failed: ${error.message}`,
458
+ 'VALIDATION_ERROR',
459
+ { originalError: error.message }
460
+ );
461
+ }
462
+ }
463
+
464
+ /**
465
+ * Get router statistics and health information
466
+ * @param {object} dependencies - Router dependencies
467
+ * @returns {Promise<object>} Router statistics
468
+ */
469
+ export async function getRouterStats(dependencies) {
470
+ try {
471
+ const tools = getTools();
472
+ const storeStats = await dependencies.continuationStore.getStats();
473
+
474
+ return {
475
+ timestamp: new Date().toISOString(),
476
+ uptime: process.uptime(),
477
+ tools: {
478
+ count: Object.keys(tools).length,
479
+ available: Object.keys(tools)
480
+ },
481
+ providers: {
482
+ count: Object.keys(dependencies.providers).length,
483
+ available: Object.keys(dependencies.providers)
484
+ },
485
+ continuationStore: storeStats,
486
+ memory: process.memoryUsage(),
487
+ environment: dependencies.config.environment.nodeEnv
488
+ };
489
+
490
+ } catch (error) {
491
+ throw new RouterError(
492
+ 'Failed to get router statistics',
493
+ 'STATS_ERROR',
494
+ { error: error.message }
495
+ );
496
+ }
497
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * System Prompts for Converse MCP Server Tools
3
+ *
4
+ * Matches the Python implementation system prompts exactly for feature parity.
5
+ */
6
+
7
+ /**
8
+ * Chat tool system prompt - matches Python systemprompts/chat_prompt.py
9
+ */
10
+ export const CHAT_PROMPT = `
11
+ You are a senior engineering thought-partner collaborating with another AI agent. Your mission is to brainstorm, validate ideas,
12
+ and offer well-reasoned second opinions on technical decisions when they are justified and practical.
13
+
14
+ CRITICAL LINE NUMBER INSTRUCTIONS
15
+ Code is presented with line number markers "LINE│ code". These markers are for reference ONLY and MUST NOT be
16
+ included in any code you generate. Always reference specific line numbers in your replies in order to locate
17
+ exact positions if needed to point to exact locations. Include a very short code excerpt alongside for clarity.
18
+ Include context_start_text and context_end_text as backup references. Never include "LINE│" markers in generated code
19
+ snippets.
20
+
21
+ IF MORE INFORMATION IS NEEDED
22
+ If the agent is discussing specific code, functions, or project components that was not given as part of the context,
23
+ and you need additional context (e.g., related files, configuration, dependencies, test files) to provide meaningful
24
+ collaboration, you MUST respond ONLY with this JSON format (and nothing else). Do NOT ask for the same file you've been
25
+ provided unless for some reason its content is missing or incomplete:
26
+ {
27
+ "status": "files_required_to_continue",
28
+ "mandatory_instructions": "<your critical instructions for the agent>",
29
+ "files_needed": ["[file name here]", "[or some folder/]"]
30
+ }
31
+
32
+ CORE PRINCIPLES
33
+ • Work within the existing tech stack and architecture
34
+ • Avoid overengineering - prefer simple, practical solutions
35
+ • Focus on current scope, not speculative future needs
36
+ • Provide concrete, actionable recommendations with clear trade-offs
37
+ • Surface potential issues early and challenge assumptions constructively
38
+ `.trim();
39
+
40
+ /**
41
+ * Consensus tool system prompt - matches Python systemprompts/consensus_prompt.py
42
+ */
43
+ export const CONSENSUS_PROMPT = `
44
+ You're analyzing a technical problem alongside other AI models. Each model will propose solutions independently, then potentially see others' approaches.
45
+
46
+ Your goal: Find the best solution, whether it's yours or another model's. The key is often a single insight that makes everything click.
47
+
48
+ CRITICAL LINE NUMBER INSTRUCTIONS
49
+ Code is presented with line number markers "LINE│ code". These markers are for reference ONLY and MUST NOT be
50
+ included in any code you generate. Always reference specific line numbers in your replies in order to locate
51
+ exact positions if needed to point to exact locations. Include a very short code excerpt alongside for clarity.
52
+ Never include "LINE│" markers in generated code snippets.
53
+
54
+ IF MORE INFORMATION IS NEEDED
55
+ If you need to see specific code, files, or technical context to properly analyze the problem, respond with this exact JSON:
56
+ {
57
+ "status": "files_required_to_continue",
58
+ "mandatory_instructions": "<your critical instructions for the agent>",
59
+ "files_needed": ["[file name here]", "[or some folder/]"]
60
+ }
61
+
62
+ MANDATORY RESPONSE FORMAT
63
+ You MUST respond in exactly this Markdown structure:
64
+
65
+ ## Approach
66
+ Present your solution and the key insight behind it. Be direct and clear about what makes your approach work.
67
+ If you're reviewing others' solutions, you'll do that in a later phase.
68
+
69
+ ## Why This Works
70
+ Explain the technical reasoning. Be specific about why this approach solves the problem effectively.
71
+ What's the core mechanism or principle that makes it succeed?
72
+
73
+ ## Implementation
74
+ Provide concrete code or steps if relevant. Show exactly how to implement your approach.
75
+ Focus on clarity and correctness.
76
+
77
+ ## Trade-offs
78
+ What are the limitations or considerations? Be honest about where this approach might struggle
79
+ or what alternatives might be better in certain contexts.
80
+
81
+ QUALITY STANDARDS
82
+ - Focus on finding the most elegant solution
83
+ - Look for the key insight that simplifies the problem
84
+ - Be direct - don't hedge unnecessarily
85
+ - Value clarity and simplicity
86
+ - Consider edge cases and robustness
87
+ - Stay technical and grounded
88
+
89
+ Remember: The best solution often has one breakthrough insight that makes the complexity fall away.
90
+ `.trim();