UNPKG

solidworks-mcp-server

Version:

Clean Architecture SolidWorks MCP Server - Production-ready with SOLID principles

527 lines 21.3 kB
/** * SolidWorks COM Adapter Implementation * Clean architecture implementation with proper abstractions */ import winax from 'winax'; import { ResultUtil, ModelType, ConnectionError, ModelNotFoundError, InvalidOperationError, COMError, } from './core-abstractions'; import { SwDocumentType, SwOpenDocOptions, SwSaveAsOptions, SwEndCondition, SwConstants, ConversionFactors, DefaultConfiguration, } from './solidworks-constants'; /** * COM Object wrapper for safe disposal */ class COMObject { object; logger; constructor(object, logger) { this.object = object; this.logger = logger; } get value() { return this.object; } dispose() { try { if (this.object) { // Release COM reference this.object = null; } } catch (error) { this.logger?.warn('Failed to dispose COM object', { error }); } } } /** * Connection pool for managing SolidWorks connections */ class ConnectionPool { connection = null; maxRetries; retryDelay; logger; constructor(config) { this.maxRetries = config.retryAttempts ?? DefaultConfiguration.Connection.RetryAttempts; this.retryDelay = config.retryDelay ?? DefaultConfiguration.Connection.RetryDelay; this.logger = config.logger; } async getConnection() { if (this.connection) { return ResultUtil.ok(this.connection); } return this.createConnection(); } async createConnection() { let lastError = null; for (let attempt = 1; attempt <= this.maxRetries; attempt++) { try { this.logger?.debug(`Attempting to connect to SolidWorks (attempt ${attempt}/${this.maxRetries})`); // Try to create or get existing SolidWorks instance const swApp = await this.tryConnect(); if (swApp) { swApp.Visible = true; this.connection = new COMObject(swApp, this.logger); this.logger?.info('Successfully connected to SolidWorks'); return ResultUtil.ok(this.connection); } } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); this.logger?.warn(`Connection attempt ${attempt} failed`, { error: lastError }); if (attempt < this.maxRetries) { await this.delay(this.retryDelay * attempt); // Exponential backoff } } } return ResultUtil.fail(new ConnectionError(`Failed to connect to SolidWorks after ${this.maxRetries} attempts`, { lastError })); } async tryConnect() { // Try different connection methods const connectionMethods = [ () => new winax.Object('SldWorks.Application'), () => winax.Object('SldWorks.Application'), () => new winax.Object('SldWorks.Application.24'), // Version specific () => winax.GetObject('', 'SldWorks.Application'), ]; for (const method of connectionMethods) { try { const app = method(); if (app) return app; } catch { // Continue to next method } } throw new Error('All connection methods failed'); } delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } disconnect() { if (this.connection) { this.connection.dispose(); this.connection = null; this.logger?.info('Disconnected from SolidWorks'); } } isConnected() { return this.connection !== null; } } /** * Model mapper to convert COM objects to domain models */ class ModelMapper { static toDomainModel(comModel, id) { const type = comModel.GetType(); const typeMap = { [SwDocumentType.Part]: ModelType.Part, [SwDocumentType.Assembly]: ModelType.Assembly, [SwDocumentType.Drawing]: ModelType.Drawing, }; return { id: id || this.generateId(), path: comModel.GetPathName ? comModel.GetPathName() : '', name: comModel.GetTitle ? comModel.GetTitle() : 'Untitled', type: typeMap[type] || ModelType.Part, isActive: true, isDirty: comModel.GetSaveFlag ? comModel.GetSaveFlag() : false, metadata: { documentType: type, version: comModel.GetVersion ? comModel.GetVersion() : null, }, }; } static toFeature(comFeature) { return { id: this.generateId(), name: comFeature.Name || 'Unknown', type: comFeature.GetTypeName2 ? comFeature.GetTypeName2() : 'Unknown', suppressed: comFeature.IsSuppressed ? comFeature.IsSuppressed() : false, parameters: { definitionType: comFeature.GetDefinitionType ? comFeature.GetDefinitionType() : null, }, }; } static toDimension(name, value, feature) { return { name, value: value * ConversionFactors.MetersToMillimeters, // Convert to mm feature, }; } static generateId() { return `sw_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } } /** * Main SolidWorks Adapter implementation */ export class SolidWorksAdapter { pool; logger; currentModel = null; constructor(config = {}) { this.pool = new ConnectionPool(config); this.logger = config.logger; } // ============================================ // CONNECTION MANAGEMENT // ============================================ async connect() { const connectionResult = await this.pool.getConnection(); if (ResultUtil.isFailure(connectionResult)) { return ResultUtil.fail(connectionResult.error); } return ResultUtil.ok(undefined); } async disconnect() { try { if (this.currentModel) { this.currentModel.dispose(); this.currentModel = null; } this.pool.disconnect(); return ResultUtil.ok(undefined); } catch (error) { return ResultUtil.fail(new COMError('Failed to disconnect', { error })); } } isConnected() { return this.pool.isConnected(); } // ============================================ // MODEL OPERATIONS // ============================================ async openModel(path) { const connectionResult = await this.pool.getConnection(); if (ResultUtil.isFailure(connectionResult)) { return ResultUtil.fail(connectionResult.error); } try { const swApp = connectionResult.data.value; const docType = SwConstants.getDocumentTypeFromExtension(path); if (docType === SwDocumentType.None) { return ResultUtil.fail(new InvalidOperationError(`Unsupported file type: ${path}`)); } // Open the document with proper error handling const errors = { value: 0 }; const warnings = { value: 0 }; const model = await this.safeComCall(() => swApp.OpenDoc6(path, docType, SwOpenDocOptions.Silent, '', errors, warnings)); if (!model) { return ResultUtil.fail(new ModelNotFoundError(`Failed to open model: ${path}`, { errors: errors.value, warnings: warnings.value, })); } this.currentModel = new COMObject(model, this.logger); const domainModel = ModelMapper.toDomainModel(model); this.logger?.info('Model opened successfully', { path, type: domainModel.type }); return ResultUtil.ok(domainModel); } catch (error) { return ResultUtil.fail(new COMError(`Failed to open model: ${path}`, { error })); } } async closeModel(save) { if (!this.currentModel) { return ResultUtil.ok(undefined); } try { const model = this.currentModel.value; if (save) { const saveResult = await this.saveModel(); if (ResultUtil.isFailure(saveResult)) { this.logger?.warn('Failed to save model before closing', { error: saveResult.error }); } } const title = await this.safeComCall(() => model.GetTitle()); const connectionResult = await this.pool.getConnection(); if (ResultUtil.isSuccess(connectionResult)) { const swApp = connectionResult.data.value; await this.safeComCall(() => swApp.CloseDoc(title)); } this.currentModel.dispose(); this.currentModel = null; this.logger?.info('Model closed', { title, saved: save }); return ResultUtil.ok(undefined); } catch (error) { return ResultUtil.fail(new COMError('Failed to close model', { error })); } } async createPart() { const connectionResult = await this.pool.getConnection(); if (ResultUtil.isFailure(connectionResult)) { return ResultUtil.fail(connectionResult.error); } try { const swApp = connectionResult.data.value; // Try to create a new part const model = await this.safeComCall(() => swApp.NewPart()); if (!model) { // Try with template const template = DefaultConfiguration.Templates.DefaultPartTemplate; const modelWithTemplate = await this.safeComCall(() => swApp.NewDocument(template, 0, 0, 0)); if (!modelWithTemplate) { return ResultUtil.fail(new COMError('Failed to create new part')); } this.currentModel = new COMObject(modelWithTemplate, this.logger); return ResultUtil.ok(ModelMapper.toDomainModel(modelWithTemplate)); } this.currentModel = new COMObject(model, this.logger); const domainModel = ModelMapper.toDomainModel(model); this.logger?.info('Part created successfully'); return ResultUtil.ok(domainModel); } catch (error) { return ResultUtil.fail(new COMError('Failed to create part', { error })); } } getCurrentModel() { if (!this.currentModel) { return ResultUtil.ok(null); } try { const domainModel = ModelMapper.toDomainModel(this.currentModel.value); return ResultUtil.ok(domainModel); } catch (error) { return ResultUtil.fail(new COMError('Failed to get current model', { error })); } } async saveModel(path) { if (!this.currentModel) { return ResultUtil.fail(new InvalidOperationError('No model open')); } try { const model = this.currentModel.value; if (path) { // Save as const success = await this.safeComCall(() => model.SaveAs3(path, SwSaveAsOptions.Silent, 0)); if (!success) { return ResultUtil.fail(new COMError('Failed to save model to path', { path })); } } else { // Save const success = await this.safeComCall(() => model.Save3(SwSaveAsOptions.Silent, 0, 0)); if (!success) { return ResultUtil.fail(new COMError('Failed to save model')); } } this.logger?.info('Model saved', { path }); return ResultUtil.ok(undefined); } catch (error) { return ResultUtil.fail(new COMError('Failed to save model', { error })); } } // ============================================ // FEATURE OPERATIONS // ============================================ async createFeature(params) { if (!this.currentModel) { return ResultUtil.fail(new InvalidOperationError('No model open')); } try { const model = this.currentModel.value; const featureMgr = model.FeatureManager; if (!featureMgr) { return ResultUtil.fail(new COMError('Cannot access FeatureManager')); } // Example: Create extrusion const { depth = 25, draft = 0, reverse = false } = params; const feature = await this.safeComCall(() => featureMgr.FeatureExtrusion3(true, // SingleEndedFeature reverse, // ReverseDirection false, // UseDirection2 SwEndCondition.Blind, SwEndCondition.Blind, depth * ConversionFactors.MillimetersToMeters, 0.01, false, false, false, draft * ConversionFactors.DegreesToRadians, 0, false, false, false, false, false, true, false)); if (!feature) { return ResultUtil.fail(new COMError('Failed to create feature')); } const domainFeature = ModelMapper.toFeature(feature); this.logger?.info('Feature created', { type: domainFeature.type }); return ResultUtil.ok(domainFeature); } catch (error) { return ResultUtil.fail(new COMError('Failed to create feature', { error })); } } async getFeatures() { if (!this.currentModel) { return ResultUtil.fail(new InvalidOperationError('No model open')); } try { const model = this.currentModel.value; const features = []; let feature = await this.safeComCall(() => model.FirstFeature()); while (feature) { features.push(ModelMapper.toFeature(feature)); feature = await this.safeComCall(() => feature.GetNextFeature()); } this.logger?.debug('Retrieved features', { count: features.length }); return ResultUtil.ok(features); } catch (error) { return ResultUtil.fail(new COMError('Failed to get features', { error })); } } async suppressFeature(name) { if (!this.currentModel) { return ResultUtil.fail(new InvalidOperationError('No model open')); } try { const model = this.currentModel.value; const feature = await this.safeComCall(() => model.FeatureByName(name)); if (!feature) { return ResultUtil.fail(new InvalidOperationError(`Feature not found: ${name}`)); } const success = await this.safeComCall(() => feature.SetSuppression2(0, 2, null) // 0 = suppressed ); if (!success) { return ResultUtil.fail(new COMError(`Failed to suppress feature: ${name}`)); } // Rebuild after suppression await this.safeComCall(() => model.EditRebuild3()); this.logger?.info('Feature suppressed', { name }); return ResultUtil.ok(undefined); } catch (error) { return ResultUtil.fail(new COMError('Failed to suppress feature', { error })); } } // ============================================ // DIMENSION OPERATIONS // ============================================ async getDimension(name) { if (!this.currentModel) { return ResultUtil.fail(new InvalidOperationError('No model open')); } try { const model = this.currentModel.value; const dimension = await this.safeComCall(() => model.Parameter(name)); if (!dimension) { return ResultUtil.fail(new InvalidOperationError(`Dimension not found: ${name}`)); } const value = await this.safeComCall(() => dimension.SystemValue); const feature = name.includes('@') ? name.split('@')[1] : 'Unknown'; const domainDimension = ModelMapper.toDimension(name, value, feature); return ResultUtil.ok(domainDimension); } catch (error) { return ResultUtil.fail(new COMError('Failed to get dimension', { error })); } } async setDimension(name, value) { if (!this.currentModel) { return ResultUtil.fail(new InvalidOperationError('No model open')); } try { const model = this.currentModel.value; const dimension = await this.safeComCall(() => model.Parameter(name)); if (!dimension) { return ResultUtil.fail(new InvalidOperationError(`Dimension not found: ${name}`)); } // Convert mm to meters for SolidWorks API await this.safeComCall(() => { dimension.SystemValue = value * ConversionFactors.MillimetersToMeters; }); // Rebuild after dimension change await this.safeComCall(() => model.ForceRebuild3(false)); this.logger?.info('Dimension set', { name, value }); return ResultUtil.ok(undefined); } catch (error) { return ResultUtil.fail(new COMError('Failed to set dimension', { error })); } } async listDimensions() { if (!this.currentModel) { return ResultUtil.fail(new InvalidOperationError('No model open')); } try { const model = this.currentModel.value; const dimensions = []; const processedNames = new Set(); let feature = await this.safeComCall(() => model.FirstFeature()); while (feature) { try { const featName = await this.safeComCall(() => feature.Name); const dispDim = await this.safeComCall(() => feature.GetFirstDisplayDimension()); let currentDim = dispDim; while (currentDim) { const dim = await this.safeComCall(() => currentDim.GetDimension2(0)); if (dim) { const fullName = await this.safeComCall(() => dim.FullName || dim.Name); if (fullName && !processedNames.has(fullName)) { processedNames.add(fullName); const value = await this.safeComCall(() => dim.SystemValue); dimensions.push(ModelMapper.toDimension(fullName, value, featName)); } } currentDim = await this.safeComCall(() => feature.GetNextDisplayDimension(currentDim)); } } catch { // Continue with next feature } feature = await this.safeComCall(() => feature.GetNextFeature()); } this.logger?.debug('Retrieved dimensions', { count: dimensions.length }); return ResultUtil.ok(dimensions); } catch (error) { return ResultUtil.fail(new COMError('Failed to list dimensions', { error })); } } // ============================================ // EXPORT OPERATIONS // ============================================ async exportModel(path, format) { if (!this.currentModel) { return ResultUtil.fail(new InvalidOperationError('No model open')); } try { const model = this.currentModel.value; // Ensure model is saved first const currentPath = await this.safeComCall(() => model.GetPathName()); if (!currentPath || currentPath === '') { const saveResult = await this.saveModel(path.replace(/\.[^.]+$/, '.SLDPRT')); if (ResultUtil.isFailure(saveResult)) { return saveResult; } } // Export based on format const success = await this.safeComCall(() => model.SaveAs3(path, 0, 1)); if (!success) { return ResultUtil.fail(new COMError(`Failed to export to ${format}`)); } this.logger?.info('Model exported', { path, format }); return ResultUtil.ok(undefined); } catch (error) { return ResultUtil.fail(new COMError('Failed to export model', { error })); } } // ============================================ // HELPER METHODS // ============================================ /** * Safe COM call with error handling */ async safeComCall(fn) { try { return fn(); } catch (error) { this.logger?.error('COM call failed', error); throw error; } } } //# sourceMappingURL=solidworks-adapter.js.map