reloaderoo
Version:
Hot-reload your MCP servers without restarting your AI coding assistant. Works excellently with VSCode MCP, well with Claude Code. A transparent development proxy for the Model Context Protocol that enables seamless server restarts during development.
272 lines • 12.5 kB
JavaScript
/**
* CapabilityAugmenter for modifying InitializeResult to add proxy capabilities
*
* This module handles the interception and modification of the initialize handshake
* between the MCP client and child server. It augments the child server's capabilities
* with proxy-specific functionality while preserving all original features.
*
* Key responsibilities:
* - Add restart_server tool to the child's tool capabilities
* - Append "-dev" suffix to server name and version for clear identification
* - Preserve all existing tools, resources, and prompts from the child
* - Enable tool list change notifications for restart events
* - Maintain protocol version compatibility
*/
import { RESTART_SERVER_TOOL } from './types.js';
import { logger } from './mcp-logger.js';
import { PROXY_TOOLS } from './constants.js';
/**
* CapabilityAugmenter handles the modification of InitializeResult responses
* from child MCP servers to add proxy-specific capabilities and naming.
*
* This class ensures that the proxy appears as a transparent wrapper that
* enhances the child server's capabilities rather than replacing them.
*/
export class CapabilityAugmenter {
logger = logger;
/**
* Main entry point for augmenting an InitializeResult from a child server.
* This method orchestrates all the individual augmentation steps.
*
* @param childResult - The original InitializeResult from the child server
* @returns Augmented InitializeResult with proxy capabilities added
* @throws Error if the childResult is malformed or missing required fields
*/
augmentInitializeResult(childResult) {
// Validate input first - outside try block to let validation errors propagate
this.validateInitializeResult(childResult);
try {
this.logger.debug('Augmenting InitializeResult from child server', {
originalServerName: childResult.serverInfo?.name,
originalVersion: childResult.serverInfo?.version,
originalCapabilities: Object.keys(childResult.capabilities || {})
});
// Extract and store child server information (for future use)
this.extractChildServerInfo(childResult);
// Create modified server info with -dev suffix
const modifiedServerInfo = this.modifyServerInfo(childResult.serverInfo);
// Augment capabilities with proxy tools
const augmentedCapabilities = this.addRestartTool(childResult.capabilities);
// Preserve child capabilities and add proxy enhancements
const finalCapabilities = this.preserveChildCapabilities(childResult.capabilities, augmentedCapabilities);
// Combine instructions if present
const combinedInstructions = this.combineInstructions(childResult.instructions);
const augmentedResult = {
protocolVersion: childResult.protocolVersion,
capabilities: finalCapabilities,
serverInfo: modifiedServerInfo,
...(combinedInstructions && { instructions: combinedInstructions }),
// Preserve any additional metadata from child
...('_meta' in childResult && { _meta: childResult._meta })
};
this.logger.info('Successfully augmented InitializeResult', {
newServerName: modifiedServerInfo.name,
newVersion: modifiedServerInfo.version,
addedTools: [PROXY_TOOLS.RESTART_SERVER],
totalCapabilities: Object.keys(finalCapabilities).length
});
return augmentedResult;
}
catch (error) {
this.logger.error('Failed to augment InitializeResult', {
error: error instanceof Error ? error.message : String(error),
childServerName: childResult.serverInfo?.name
});
throw error;
}
}
/**
* Adds the restart_server tool to the child server's capabilities.
* Ensures tools capability exists and includes the restart functionality.
*
* @param childCapabilities - Original capabilities from child server
* @returns Enhanced capabilities with restart_server tool
*/
addRestartTool(childCapabilities) {
this.logger.debug(`Adding ${PROXY_TOOLS.RESTART_SERVER} tool to capabilities`);
// Create enhanced capabilities with tools support
const enhancedCapabilities = {
...childCapabilities,
tools: {
// Include any other tools properties from child first
...childCapabilities.tools,
// Always enable listChanged for proxy capabilities (override child setting)
listChanged: true
}
};
this.logger.debug('Restart tool capability added', {
toolsListChanged: enhancedCapabilities.tools.listChanged,
existingToolsCapability: !!childCapabilities.tools
});
return enhancedCapabilities;
}
/**
* Modifies server information to append "-dev" suffix to name and version.
* This clearly identifies the server as running through the development proxy.
*
* @param originalServerInfo - Original Implementation from child server
* @returns Modified Implementation with -dev suffixes
*/
modifyServerInfo(originalServerInfo) {
const modifiedInfo = {
name: this.appendDevSuffix(originalServerInfo.name),
version: this.appendDevSuffix(originalServerInfo.version)
};
this.logger.debug('Modified server info with -dev suffix', {
originalName: originalServerInfo.name,
originalVersion: originalServerInfo.version,
newName: modifiedInfo.name,
newVersion: modifiedInfo.version
});
return modifiedInfo;
}
/**
* Preserves all child server capabilities while ensuring proxy capabilities are included.
* This method ensures no functionality is lost from the original server.
*
* @param originalCapabilities - Original capabilities from child
* @param proxyCapabilities - Enhanced capabilities with proxy additions
* @returns Final capabilities preserving all child features
*/
preserveChildCapabilities(originalCapabilities, proxyCapabilities) {
this.logger.debug('Preserving child capabilities', {
originalCapabilities: Object.keys(originalCapabilities),
hasTools: !!originalCapabilities.tools,
hasResources: !!originalCapabilities.resources,
hasPrompts: !!originalCapabilities.prompts
});
// Merge capabilities, ensuring proxy enhancements take precedence where needed
const preservedCapabilities = {
// Start with all original capabilities
...originalCapabilities,
// Override with proxy enhancements (primarily tools with restart_server)
...proxyCapabilities,
// Explicitly preserve specific child capabilities that shouldn't be overridden
...(originalCapabilities.resources && { resources: originalCapabilities.resources }),
...(originalCapabilities.prompts && { prompts: originalCapabilities.prompts }),
...(originalCapabilities.logging && { logging: originalCapabilities.logging }),
...(originalCapabilities.completions && { completions: originalCapabilities.completions }),
...(originalCapabilities.experimental && { experimental: originalCapabilities.experimental })
};
this.logger.debug('Child capabilities preserved successfully', {
finalCapabilities: Object.keys(preservedCapabilities),
toolsListChanged: preservedCapabilities.tools?.listChanged
});
return preservedCapabilities;
}
/**
* Extracts child server information for internal state management.
* This information is used by other proxy components for lifecycle management.
*
* @param initializeResult - The child's InitializeResult
* @returns Structured child server information
*/
extractChildServerInfo(initializeResult) {
const serverInfo = {
name: initializeResult.serverInfo.name,
version: initializeResult.serverInfo.version,
capabilities: initializeResult.capabilities,
protocolVersion: initializeResult.protocolVersion,
...(initializeResult.instructions && { instructions: initializeResult.instructions })
};
this.logger.debug('Extracted child server info', {
name: serverInfo.name,
version: serverInfo.version,
protocolVersion: serverInfo.protocolVersion,
hasInstructions: !!serverInfo.instructions
});
return serverInfo;
}
/**
* Gets the restart_server tool definition.
* Provides access to the tool definition for other components.
*
* @returns The restart_server Tool definition
*/
getRestartServerTool() {
return RESTART_SERVER_TOOL;
}
/**
* Validates that the InitializeResult contains required fields.
* Throws descriptive errors for missing or invalid data.
*
* @param result - InitializeResult to validate
* @throws Error if validation fails
*/
validateInitializeResult(result) {
if (!result) {
throw new Error('InitializeResult is null or undefined');
}
if (!result.serverInfo) {
throw new Error('InitializeResult missing required serverInfo field');
}
if (!result.capabilities) {
throw new Error('InitializeResult missing required capabilities field');
}
if (!result.protocolVersion) {
throw new Error('InitializeResult missing required protocolVersion field');
}
// Validate serverInfo has required fields
if (typeof result.serverInfo.name !== 'string') {
this.logger.warn('Child server has invalid name field, using default');
}
if (typeof result.serverInfo.version !== 'string') {
this.logger.warn('Child server has invalid version field, using default');
}
}
/**
* Appends "-dev" suffix to a string if not already present.
* Handles edge cases like empty strings and already-suffixed values.
*
* @param value - String to append suffix to
* @returns String with -dev suffix
*/
appendDevSuffix(value) {
if (!value || value.trim() === '') {
return 'unknown-dev';
}
// Avoid double-suffixing
if (value.endsWith('-dev')) {
return value;
}
return `${value}-dev`;
}
/**
* Combines child server instructions with proxy-specific guidance.
* Creates comprehensive instructions for the enhanced server.
*
* @param childInstructions - Original instructions from child server
* @returns Combined instructions including proxy information
*/
combineInstructions(childInstructions) {
const proxyInstructions = 'This server is running through mcpdev-proxy, which provides development capabilities. ' +
`Use the ${PROXY_TOOLS.RESTART_SERVER} tool to restart the underlying server with optional configuration updates.`;
if (!childInstructions) {
return proxyInstructions;
}
// Combine instructions with clear separation
return `${childInstructions}\n\n--- Development Proxy ---\n${proxyInstructions}`;
}
}
/**
* Convenience function to create and use a CapabilityAugmenter instance.
* Provides a simple interface for one-time augmentation operations.
*
* @param childResult - InitializeResult from child server
* @returns Augmented InitializeResult
*/
export function augmentCapabilities(childResult) {
const augmenter = new CapabilityAugmenter();
return augmenter.augmentInitializeResult(childResult);
}
/**
* Type guard to check if capabilities include proxy enhancements.
* Useful for validation and testing scenarios.
*
* @param capabilities - ServerCapabilities to check
* @returns True if capabilities appear to be proxy-enhanced
*/
export function hasProxyCapabilities(capabilities) {
return !!(capabilities.tools?.listChanged);
}
//# sourceMappingURL=capability-augmenter.js.map