Skip to main content
Glama
index.ts23.3 kB
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { writeFile } from 'fs-extra'; import { randomUUID } from 'node:crypto'; // Keep for Node.js compatibility in local version import { MsonModelSchema, CreateMsonModelInputSchema } from './schemas.js'; import { setupTools } from './tools.js'; import { msonToSystemRuntimeBundle } from './transformers/system-runtime.js'; import type { MsonModel, ValidationWarning } from './types.js'; import type { z } from 'zod'; import { validateSystemRuntimeBundle } from './validators/system-runtime.js'; // ============================================================================ // MAIN SERVER CLASS // ============================================================================ class SystemDesignerMCPServer { private readonly server: McpServer; constructor() { this.server = new McpServer({ name: 'system-designer-mcp', version: '1.0.0', }); // Register all MCP tools setupTools(this.server, { handleCreateMsonModel: this.handleCreateMsonModel.bind(this), handleValidateMsonModel: this.handleValidateMsonModel.bind(this), handleGenerateUmlDiagram: this.handleGenerateUmlDiagram.bind(this), handleExportToSystemDesigner: this.handleExportToSystemDesigner.bind(this), handleCreateSystemRuntimeBundle: this.handleCreateSystemRuntimeBundle.bind(this), handleValidateSystemRuntimeBundle: this.handleValidateSystemRuntimeBundle.bind(this), }); } // Tool handler methods private async handleCreateMsonModel(args: unknown): Promise<any> { const validationResult = CreateMsonModelInputSchema.safeParse(args); if (!validationResult.success) { const errorMessage = this.formatValidationError(validationResult.error, 'create_mson_model'); return { content: [{ type: 'text', text: errorMessage }], isError: true, }; } const model = this.ensureUniqueIds(validationResult.data); const warnings = this.validateBusinessLogic(model); // Check for critical relationship issues that should fail the operation const relationshipErrors = this.validateRelationships(model); if (relationshipErrors.length > 0) { const errorMessage = `❌ Model creation failed due to relationship errors: ${relationshipErrors.map((error) => ` • ${error}`).join('\n')} 💡 **How to fix:** • Ensure all relationships reference valid entity IDs • Check for typos in entity IDs referenced in relationships • Make sure entities are defined before referencing them in relationships 📖 **Valid relationship example:** { "relationships": [ { "from": "team_entity_id", "to": "player_entity_id", "type": "association" } ] }`; return { content: [{ type: 'text', text: errorMessage }], isError: true, }; } const successMessage = `MSON Model Created Successfully: Name: ${model.name} Type: ${model.type} Description: ${model.description || 'No description'} Entities: ${model.entities.length} Relationships: ${model.relationships.length} Model ID: ${model.id} ${warnings.length > 0 ? `⚠️ Warnings:\n${warnings.map((w) => `- ${w.message}`).join('\n')}` : 'No warnings detected.'} You can now use this model with other tools.`; return { content: [ { type: 'text', text: successMessage }, { type: 'json', json: model }, ], }; } private async handleValidateMsonModel(args: unknown): Promise<any> { // Handle both wrapped format { model: ... } and direct model input let modelToValidate: unknown; if (typeof args === 'object' && args !== null && 'model' in args) { modelToValidate = (args as { model: unknown }).model; } else { modelToValidate = args; } const validationResult = MsonModelSchema.safeParse(modelToValidate); if (!validationResult.success) { const errorMessage = this.formatValidationError( validationResult.error, 'validate_mson_model' ); return { content: [{ type: 'text', text: errorMessage }], isError: true, }; } const validatedModel = validationResult.data; const warnings = this.validateBusinessLogic(validatedModel); // Check for critical relationship issues that should fail validation const relationshipErrors = this.validateRelationships(validatedModel); if (relationshipErrors.length > 0) { const errorMessage = `❌ Model validation failed due to relationship errors: ${relationshipErrors.map((error) => ` • ${error}`).join('\n')} 💡 **How to fix:** • Ensure all relationships reference valid entity IDs • Check for typos in entity IDs referenced in relationships • Make sure entities are defined before referencing them in relationships`; return { content: [{ type: 'text', text: errorMessage }], isError: true, }; } const successMessage = `✅ Model Validation Successful! Model: ${validatedModel.name} Type: ${validatedModel.type} Entities: ${validatedModel.entities.length} Relationships: ${validatedModel.relationships.length} ${warnings.length > 0 ? `⚠️ Warnings (${warnings.length}):\n${warnings.map((w) => `- ${w.message}`).join('\n')}` : 'No warnings detected.'} Model is ready for UML generation and export.`; return { content: [{ type: 'text', text: successMessage }] }; } private async handleGenerateUmlDiagram(args: unknown): Promise<any> { const { model, format = 'plantuml' } = args as { model: unknown; format?: 'plantuml' | 'mermaid'; }; const validationResult = MsonModelSchema.safeParse(model); if (!validationResult.success) { return { content: [ { type: 'text', text: `❌ Cannot generate UML: Invalid model format\nError: ${validationResult.error}`, }, ], isError: true, }; } // Validate format if (format !== 'plantuml' && format !== 'mermaid') { return { content: [ { type: 'text', text: `❌ Unsupported format: ${format}`, }, ], isError: true, }; } const validatedModel = validationResult.data; const umlOutput = format === 'plantuml' ? this.generatePlantUML(validatedModel) : this.generateMermaid(validatedModel); const successMessage = `🎨 UML Diagram Generated (${format.toUpperCase()}): Model: ${validatedModel.name} Format: ${format} Copy the markup below into your preferred UML tool: --- ${umlOutput} ---`; return { content: [{ type: 'text', text: successMessage }] }; } private async handleExportToSystemDesigner(args: unknown): Promise<any> { const { model, filePath } = args as { model: unknown; filePath?: string }; const validationResult = MsonModelSchema.safeParse(model); if (!validationResult.success) { return { content: [ { type: 'text', text: `❌ Cannot export: Invalid model format\nError: ${validationResult.error}`, }, ], isError: true, }; } const validatedModel = validationResult.data; const systemDesignerFormat = { version: '1.0', type: 'system_designer_model', metadata: { name: validatedModel.name, modelType: validatedModel.type, description: validatedModel.description || '', createdAt: new Date().toISOString(), exportedBy: 'system-designer-mcp', }, model: validatedModel, }; const fileName = filePath || `${this.sanitizeFilename(validatedModel.name)}_system_designer.json`; try { await writeFile(fileName, JSON.stringify(systemDesignerFormat, null, 2)); const successMessage = `✅ Successfully exported to System Designer format! File: ${fileName} Model: ${validatedModel.name} Type: ${validatedModel.type} Entities: ${validatedModel.entities.length} Relationships: ${validatedModel.relationships.length} You can now import this file into System Designer or other compatible UML tools. File saved at: ${fileName}`; return { content: [{ type: 'text', text: successMessage }] }; } catch (error) { return { content: [ { type: 'text', text: `❌ Export failed: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } private async handleCreateSystemRuntimeBundle(args: unknown): Promise<any> { const { model, version } = args as { model: unknown; version?: string }; // Validate MSON model const validationResult = MsonModelSchema.safeParse(model); if (!validationResult.success) { return { content: [ { type: 'text', text: `❌ Invalid MSON model: ${validationResult.error.message}`, }, ], isError: true, }; } try { // Transform to System Runtime bundle const bundle = msonToSystemRuntimeBundle(validationResult.data, version); // Validate the generated bundle const bundleValidation = validateSystemRuntimeBundle(bundle); if (!bundleValidation.isValid) { const errors = bundleValidation.warnings.filter((w) => w.severity === 'error'); return { content: [ { type: 'text', text: `❌ Bundle validation failed:\n${errors.map((e) => ` - ${e.message}`).join('\n')}`, }, ], isError: true, }; } const warnings = bundleValidation.warnings.filter((w) => w.severity === 'warning'); const warningText = warnings.length > 0 ? `\n\n⚠️ Warnings:\n${warnings.map((w) => ` - ${w.message}`).join('\n')}` : ''; const successMessage = `✅ System Runtime Bundle Created Successfully: Name: ${bundle.name} Version: ${bundle.version} Description: ${bundle.description} Bundle Structure: - Schemas: ${Object.keys(bundle.schemas).length} - Models: ${Object.keys(bundle.models).length} - Types: ${Object.keys(bundle.types).length} - Behaviors: ${Object.keys(bundle.behaviors).length} - Component Types: ${Object.keys(bundle.components).length}${warningText}`; return { content: [ { type: 'text', text: successMessage }, { type: 'text', text: '\n\nSystem Runtime Bundle (JSON):' }, { type: 'text', text: JSON.stringify(bundle, null, 2) }, ], }; } catch (error) { return { content: [ { type: 'text', text: `❌ Bundle creation failed: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } private async handleValidateSystemRuntimeBundle(args: unknown): Promise<any> { const { bundle } = args as { bundle: unknown }; // Handle both object and string formats let bundleToValidate: unknown; if (typeof bundle === 'string') { try { bundleToValidate = JSON.parse(bundle); } catch (parseError) { return { content: [ { type: 'text', text: `❌ Invalid JSON bundle: "${bundle}" is not valid JSON. Error: ${parseError instanceof Error ? parseError.message : 'Unknown error'}`, }, ], isError: true, }; } } else { bundleToValidate = bundle; } try { const validation = validateSystemRuntimeBundle(bundleToValidate); const errors = validation.warnings.filter((w) => w.severity === 'error'); const warnings = validation.warnings.filter((w) => w.severity === 'warning'); if (!validation.isValid) { const errorMessage = `❌ Bundle Validation Failed: Errors (${errors.length}): ${errors.map((e) => ` - ${e.message}`).join('\n')} ${warnings.length > 0 ? `Warnings (${warnings.length}):\n${warnings.map((w) => ` - ${w.message}`).join('\n')}` : ''}`; return { content: [{ type: 'text', text: errorMessage }], isError: true, }; } const successMessage = `✅ System Runtime Bundle is Valid! Bundle: ${validation.bundle?.name || 'Unknown'} Version: ${validation.bundle?.version || 'Unknown'} ${warnings.length > 0 ? `⚠️ Warnings (${warnings.length}):\n${warnings.map((w) => ` - ${w.message}`).join('\n')}` : 'No warnings detected.'}`; return { content: [{ type: 'text', text: successMessage }], }; } catch (error) { return { content: [ { type: 'text', text: `❌ Validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } private ensureUniqueIds(model: z.infer<typeof CreateMsonModelInputSchema>): MsonModel { // Create ID mapping for entities that need new IDs generated const entityIdMap = new Map<string, string>(); const processedEntities = model.entities.map((entity) => { const newId = entity.id || `entity_${randomUUID()}`; if (entity.id) { entityIdMap.set(entity.id, newId); } return { id: newId, name: entity.name, type: entity.type, description: entity.description, attributes: entity.attributes || [], methods: entity.methods || [], stereotype: entity.stereotype, namespace: entity.namespace, }; }); // Process relationships and update entity references if needed const processedRelationships = model.relationships.map((relationship) => ({ id: relationship.id || `rel_${randomUUID()}`, from: entityIdMap.get(relationship.from) || relationship.from, to: entityIdMap.get(relationship.to) || relationship.to, type: relationship.type, multiplicity: relationship.multiplicity, name: relationship.name, description: relationship.description, })); return { id: model.id || `model_${Date.now()}`, name: model.name, type: model.type, description: model.description, entities: processedEntities, relationships: processedRelationships, }; } private validateBusinessLogic(model: MsonModel): ValidationWarning[] { const warnings: ValidationWarning[] = []; // Check for entity name duplicates (warning level) const entityNames = new Set<string>(); for (const entity of model.entities) { if (entityNames.has(entity.name)) { warnings.push({ message: `Duplicate entity name detected: ${entity.name}`, severity: 'warning', }); } entityNames.add(entity.name); } return warnings; } private validateRelationships(model: MsonModel): string[] { const errors: string[] = []; const entityIds = new Set(model.entities.map((e) => e.id)); for (const relationship of model.relationships) { if (!entityIds.has(relationship.from)) { errors.push( `Relationship '${relationship.name || relationship.id}' references unknown entity '${relationship.from}' in 'from' field` ); } if (!entityIds.has(relationship.to)) { errors.push( `Relationship '${relationship.name || relationship.id}' references unknown entity '${relationship.to}' in 'to' field` ); } } return errors; } private visibilityToPlantUML(visibility: string): string { switch (visibility) { case 'private': return '-'; case 'protected': return '#'; default: return '+'; } } private visibilityToMermaid(visibility: string): string { switch (visibility) { case 'private': return '-'; case 'protected': return '#'; default: return '+'; } } private relationshipToPlantUML(type: string): string { switch (type) { case 'inheritance': return '<|--'; case 'implementation': return '<|..'; case 'association': return '-->'; case 'dependency': return '..>'; case 'aggregation': return 'o-->'; case 'composition': return '*-->'; default: return '-->'; } } private relationshipToMermaid(type: string): string { return this.relationshipToPlantUML(type); } private generatePlantUML(model: MsonModel): string { let uml = '@startuml\n'; uml += `title ${model.name}\n`; if (model.description) { uml += `note top of ${model.entities[0]?.name || 'FirstEntity'}\n`; uml += `${model.description}\n`; uml += 'end note\n'; } for (const entity of model.entities) { const entityType = entity.type === 'interface' ? 'interface' : entity.type === 'enum' ? 'enum' : 'class'; uml += `${entityType} ${entity.name} {\n`; for (const attr of entity.attributes) { const visibility = this.visibilityToPlantUML(attr.visibility); const staticStr = attr.isStatic ? '{static} ' : ''; const readOnlyStr = attr.isReadOnly ? '{readOnly} ' : ''; uml += ` ${visibility}${staticStr}${readOnlyStr}${attr.name}: ${attr.type}\n`; } for (const method of entity.methods) { const visibility = this.visibilityToPlantUML(method.visibility); const staticStr = method.isStatic ? '{static} ' : ''; const abstractStr = method.isAbstract ? '{abstract} ' : ''; const params = method.parameters.map((p) => `${p.name}: ${p.type}`).join(', '); uml += ` ${visibility}${staticStr}${abstractStr}${method.name}(${params}): ${method.returnType}\n`; } uml += '}\n'; } for (const rel of model.relationships) { const relStr = this.relationshipToPlantUML(rel.type); const fromMultiplicity = rel.multiplicity?.from ? `"${rel.multiplicity.from}"` : ''; const toMultiplicity = rel.multiplicity?.to ? `"${rel.multiplicity.to}"` : ''; const relName = rel.name ? `: ${rel.name}` : ''; const fromEntity = model.entities.find((e) => e.id === rel.from)?.name; const toEntity = model.entities.find((e) => e.id === rel.to)?.name; if (fromEntity && toEntity) { uml += `${fromEntity} ${fromMultiplicity} ${relStr} ${toMultiplicity} ${toEntity} ${relName}\n`; } } uml += '@enduml'; return uml; } private generateMermaid(model: MsonModel): string { let mermaid = 'classDiagram\n'; mermaid += `title ${model.name}\n`; for (const entity of model.entities) { const classType = entity.type === 'interface' ? 'interface' : entity.type === 'enum' ? 'enum' : 'class'; mermaid += `${classType} ${entity.name} {\n`; for (const attr of entity.attributes) { const visibility = this.visibilityToMermaid(attr.visibility); const staticStr = attr.isStatic ? '*' : ''; const readOnlyStr = attr.isReadOnly ? '?' : ''; mermaid += ` ${visibility}${staticStr}${readOnlyStr}${attr.name}${attr.type ? `: ${attr.type}` : ''}\n`; } for (const method of entity.methods) { const visibility = this.visibilityToMermaid(method.visibility); const staticStr = method.isStatic ? '*' : ''; const abstractStr = method.isAbstract ? '*' : ''; const params = method.parameters .map((p) => `${p.name}${p.type ? `: ${p.type}` : ''}`) .join(', '); mermaid += ` ${visibility}${staticStr}${abstractStr}${method.name}(${params})${method.returnType ? `: ${method.returnType}` : ''}\n`; } mermaid += '}\n'; } for (const rel of model.relationships) { const relStr = this.relationshipToMermaid(rel.type); const fromName = model.entities.find((e) => e.id === rel.from)?.name; const toName = model.entities.find((e) => e.id === rel.to)?.name; const relName = rel.name ? `: ${rel.name}` : ''; if (fromName && toName) { mermaid += `${fromName} ${relStr} ${toName} ${relName}\n`; } } return mermaid; } private sanitizeFilename(name: string): string { return name.replace(/[^a-zA-Z0-9]/g, '_'); } private formatValidationError(error: any, toolName: string): string { const errorDetails = error.issues ?.map((issue: any) => { const path = issue.path?.join('.') || 'root'; const expected = issue.expected || 'expected value'; const received = issue.received || 'received value'; return ` • ${path}: ${issue.message} (expected ${expected}, received ${received})`; }) .join('\n') || ` • Unknown validation error: ${error.message}`; return `❌ ${toolName} validation failed: ${errorDetails} 💡 **Tips for fixing this issue:** ${this.getValidationTips(toolName, error.issues || [])} 📖 **Expected format:** ${this.getExpectedFormatTip(toolName)}`; } private getValidationTips(toolName: string, issues: any[]): string { const tips = new Set<string>(); issues.forEach((issue) => { const path = issue.path?.join('.') || ''; if (path.includes('name') && issue.code === 'invalid_type') { tips.add('• Ensure all required fields like "name" are provided as strings'); } if (path.includes('entities') && issue.code === 'invalid_type') { tips.add('• The "entities" field should be an array (even if empty)'); } if (path.includes('id') && issue.code === 'invalid_type') { tips.add('• Entity and relationship IDs should be strings (leave empty to auto-generate)'); } if (path.includes('type') && issue.code === 'invalid_enum_value') { tips.add('• Check that entity and relationship types match the allowed values'); } }); if (tips.size === 0) { tips.add('• Check that all required fields are present and have correct types'); tips.add('• Refer to the expected format example below'); } return Array.from(tips).join('\n'); } private getExpectedFormatTip(toolName: string): string { switch (toolName) { case 'create_mson_model': return `{ name: "MyModel", type: "class", description: "Optional description", entities: [ { name: "EntityName", type: "class", properties: [ { name: "fieldName", type: "string" } ] } ], relationships: [] }`; case 'validate_mson_model': return `{ id: "model_id", name: "MyModel", type: "class", entities: [...], relationships: [...] }`; default: return 'Refer to the tool documentation for the correct input format'; } } async run(): Promise<void> { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('System Designer MCP Server running on stdio'); } } async function main() { const mcpServer = new SystemDesignerMCPServer(); try { await mcpServer.run(); } catch (error) { console.error('Failed to start MCP Server:', error); process.exit(1); } } export { SystemDesignerMCPServer }; main();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/chevyfsa/system-designer-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server