halopsa-workflows-mcp
Version:
HaloPSA Workflows MCP Server
361 lines (313 loc) • 10 kB
JavaScript
/**
* MCP Protocol Compatibility Layer
*
* Ensures compatibility between different versions of MCP protocol implementations:
* - FastMCP (1.20.5+)
* - Azure MCP
* - Lucidity MCP
* - Browser-use MCP Server
*
* Version: 1.0.0
*/
import { FastMCP } from 'fastmcp';
import { z } from 'zod';
// Protocol versions supported
export const PROTOCOL_VERSIONS = {
DEFAULT: '1.0',
FASTMCP: '1.20.5',
LATEST: '2024-03-27'
};
/**
* Create an MCP server with enhanced compatibility features
* that works across different MCP implementations
*
* @param {Object} config - Server configuration
* @param {string} config.name - Server name
* @param {string} config.description - Server description
* @param {string} config.version - Server version
* @param {string[]} config.models - Supported models
* @returns {FastMCP} Configured FastMCP instance
*/
export function createCompatibleMcpServer(config) {
// Default configuration with safe fallbacks
const serverConfig = {
name: config.name || 'mcp-server',
version: config.version || '1.0.0',
description: config.description || 'MCP Server'
};
// Create server with standardized configuration
const mcp = new FastMCP({
name: serverConfig.name,
version: serverConfig.version
// Removed authenticate callback to avoid compatibility issues
});
// Add enhanced protocol capabilities
enhanceProtocolCapabilities(mcp, config);
// Add enhanced error handling
enhanceErrorHandling(mcp);
return mcp;
}
/**
* Enhance MCP server with protocol capabilities
* @param {FastMCP} mcp - FastMCP server instance
* @param {Object} config - Server configuration
*/
function enhanceProtocolCapabilities(mcp, config) {
// Store original protocol negotiation methods
const originalStart = mcp.start;
// Override start method to handle protocol negotiation
mcp.start = async function(options = { transportType: 'stdio' }) {
try {
// Add custom capabilities based on client needs
if (options.transportType === 'sse') {
// Configure for browser-based clients
options.sse = options.sse || {
endpoint: '/v1/mcp',
port: process.env.MCP_PORT || 3000
};
}
// Start the server with enhanced options
return await originalStart.call(this, options);
} catch (error) {
console.error(`MCP server start error: ${error.message}`);
// Attempt fallback to basic configuration
if (error.message.includes('transport')) {
console.warn('Falling back to stdio transport');
return await originalStart.call(this, { transportType: 'stdio' });
}
throw error;
}
};
}
/**
* Enhance MCP server with better error handling
* @param {FastMCP} mcp - FastMCP server instance
*/
function enhanceErrorHandling(mcp) {
const originalOnError = mcp.on;
// Override 'on' method to add enhanced error handling
mcp.on = function(event, handler) {
if (event === 'error') {
// Wrap the error handler to provide more detailed information
const enhancedHandler = (error) => {
const enhancedError = enhanceError(error);
handler(enhancedError);
};
return originalOnError.call(this, event, enhancedHandler);
}
// Handle session events with special care
if (event === 'connect') {
const enhancedHandler = (event) => {
try {
// Fix session handling for compatibility
handler(event);
// Send welcome message for debugging
if (event.session && event.session.requestSampling) {
try {
event.session.requestSampling({
role: 'tool',
content: [{
type: 'text',
text: 'Connected to HaloPSA Workflows MCP Server'
}]
}).catch(() => {
// Ignore errors from welcome message
});
} catch (error) {
// Ignore errors from welcome message
}
}
} catch (error) {
console.error(`Session connection error: ${error.message}`);
handler(event); // Fall back to original session
}
};
return originalOnError.call(this, event, enhancedHandler);
}
return originalOnError.call(this, event, handler);
};
}
/**
* Enhance error object with more details
* @param {Error} error - Original error
* @returns {Error} Enhanced error
*/
function enhanceError(error) {
if (error.name === 'McpError') {
// Already enhanced
return error;
}
// Create a new error with enhanced fields
const enhancedError = new Error(error.message);
enhancedError.name = 'McpError';
enhancedError.originalError = error;
enhancedError.code = error.code || 'UNKNOWN_ERROR';
enhancedError.data = error.data;
enhancedError.stack = error.stack;
return enhancedError;
}
/**
* Creates standardized tool parameters using Zod schema for MCP
* @param {Object} schema - JSON Schema for tool parameters
* @returns {Object} Standardized tool parameters as Zod schema
*/
export function createToolParameters(schema) {
try {
// If schema is already a Zod schema, return it
if (schema instanceof z.ZodType) {
return schema;
}
// Convert JSON schema to Zod schema for FastMCP
const zodSchema = convertToZodSchema(schema);
return zodSchema;
} catch (error) {
console.error(`Error creating tool parameters: ${error.message}`);
// Fallback to direct schema
return schema;
}
}
/**
* Convert JSON schema to Zod schema
* @param {Object} jsonSchema - JSON Schema object
* @returns {z.ZodType} Zod schema
*/
function convertToZodSchema(jsonSchema) {
if (!jsonSchema) return z.any();
switch (jsonSchema.type) {
case 'object':
const shape = {};
// Process properties
if (jsonSchema.properties) {
for (const [key, propSchema] of Object.entries(jsonSchema.properties)) {
let prop = convertToZodSchema(propSchema);
// Check if property is required
if (jsonSchema.required && jsonSchema.required.includes(key)) {
shape[key] = prop;
} else {
shape[key] = prop.optional();
}
}
}
return z.object(shape);
case 'array':
return z.array(jsonSchema.items ? convertToZodSchema(jsonSchema.items) : z.any());
case 'string':
let stringSchema = z.string();
if (jsonSchema.format === 'email') stringSchema = z.string().email();
if (jsonSchema.format === 'uri') stringSchema = z.string().url();
return stringSchema;
case 'number':
return z.number();
case 'integer':
return z.number().int();
case 'boolean':
return z.boolean();
case 'null':
return z.null();
default:
return z.any();
}
}
/**
* Wraps tool execution with standardized error handling
* @param {Function} handler - Tool handler function
* @returns {Function} Wrapped handler with error handling
*/
export function wrapToolHandler(handler) {
return async (params, context) => {
try {
// Normalize context for compatibility
const normalizedContext = normalizeContext(context);
// Execute the original handler
const result = await handler(params, normalizedContext);
// Normalize the result
return normalizeResult(result);
} catch (error) {
// Log the error
if (context.log && context.log.error) {
context.log.error(`Tool execution error: ${error.message}`);
} else {
console.error(`Tool execution error: ${error.message}`);
}
// Return standardized error response
return {
type: 'text',
text: JSON.stringify({
error: {
message: error.message,
code: error.code || 'EXECUTION_ERROR',
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
}
})
};
}
};
}
/**
* Normalize context object for compatibility
* @param {Object} context - Context object
* @returns {Object} Normalized context
*/
function normalizeContext(context) {
// Create a copy of the context to avoid mutation
const normalizedContext = { ...context };
// Ensure log methods exist
if (!normalizedContext.log) {
normalizedContext.log = {
debug: (message) => console.debug(`[DEBUG] ${message}`),
info: (message) => console.info(`[INFO] ${message}`),
warn: (message) => console.warn(`[WARN] ${message}`),
error: (message) => console.error(`[ERROR] ${message}`)
};
}
// Ensure reportProgress exists
if (!normalizedContext.reportProgress) {
normalizedContext.reportProgress = async () => {};
}
return normalizedContext;
}
/**
* Normalize result for compatibility
* @param {any} result - Result from handler
* @returns {Object} Normalized result
*/
function normalizeResult(result) {
// Handle string results
if (typeof result === 'string') {
return {
type: 'text',
text: result
};
}
// Handle plain objects that are not content objects
if (typeof result === 'object' &&
result !== null &&
!result.type &&
!result.content) {
return {
type: 'text',
text: JSON.stringify(result, null, 2)
};
}
// Handle content objects
if (typeof result === 'object' &&
result !== null &&
result.type &&
(result.type === 'text' || result.type === 'image')) {
return result;
}
// Handle content arrays
if (typeof result === 'object' &&
result !== null &&
result.content &&
Array.isArray(result.content)) {
return result;
}
// Default fallback
return {
type: 'text',
text: typeof result === 'object' ?
JSON.stringify(result, null, 2) :
String(result)
};
}