UNPKG

@dollhousemcp/mcp-server

Version:

DollhouseMCP - A Model Context Protocol (MCP) server that enables dynamic AI persona management from markdown files, allowing Claude and other compatible AI assistants to activate and switch between different behavioral personas.

1,107 lines 438 kB
#!/usr/bin/env node // Defensive error handling for npx/CLI execution process.on('uncaughtException', (error) => { console.error('[DollhouseMCP] Server startup failed'); process.exit(1); }); process.on('unhandledRejection', (reason, promise) => { console.error('[DollhouseMCP] Server startup failed'); process.exit(1); }); import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; import * as fs from "fs/promises"; import * as path from "path"; import { loadIndicatorConfig, formatIndicator, validateCustomFormat } from './config/indicator-config.js'; import { SecureYamlParser } from './security/secureYamlParser.js'; import { SecurityError } from './errors/SecurityError.js'; import { SecureErrorHandler } from './security/errorHandler.js'; import { APICache, CollectionCache } from './cache/index.js'; import { validateFilename, sanitizeInput, validateContentSize, validateUsername, MCPInputValidator } from './security/InputValidator.js'; import { SECURITY_LIMITS, VALIDATION_PATTERNS } from './security/constants.js'; import { ContentValidator } from './security/contentValidator.js'; import { PathValidator } from './security/pathValidator.js'; import { FileLockManager } from './security/fileLockManager.js'; import { generateAnonymousId, generateUniqueId, slugify } from './utils/filesystem.js'; import { GitHubClient, CollectionBrowser, CollectionSearch, PersonaDetails, PersonaSubmitter, ElementInstaller } from './collection/index.js'; import { UpdateManager } from './update/index.js'; import { ServerSetup } from './server/index.js'; import { GitHubAuthManager } from './auth/GitHubAuthManager.js'; import { logger } from './utils/logger.js'; import { PersonaExporter, PersonaImporter, PersonaSharer } from './persona/export-import/index.js'; import { isDefaultPersona } from './constants/defaultPersonas.js'; import { PortfolioManager, ElementType } from './portfolio/PortfolioManager.js'; import { MigrationManager } from './portfolio/MigrationManager.js'; import { SkillManager } from './elements/skills/index.js'; import { TemplateManager } from './elements/templates/TemplateManager.js'; import { AgentManager } from './elements/agents/AgentManager.js'; // Detect execution environment const EXECUTION_ENV = { isNpx: process.env.npm_execpath?.includes('npx') || false, isCli: process.argv[1]?.endsWith('/dollhousemcp') || false, isDirect: !process.env.npm_execpath, cwd: process.cwd(), scriptPath: process.argv[1], }; // Only log execution environment in debug mode if (process.env.DOLLHOUSE_DEBUG) { console.error('[DollhouseMCP] Debug mode enabled'); } export class DollhouseMCPServer { server; personasDir; personas = new Map(); activePersona = null; currentUser = null; apiCache = new APICache(); collectionCache = new CollectionCache(); rateLimitTracker = new Map(); indicatorConfig; githubClient; githubAuthManager; collectionBrowser; collectionSearch; personaDetails; elementInstaller; personaSubmitter; updateManager; serverSetup; personaExporter; personaImporter; personaSharer; portfolioManager; migrationManager; skillManager; templateManager; agentManager; constructor() { this.server = new Server({ name: "dollhousemcp", version: "1.0.0", }, { capabilities: { tools: {}, }, }); // Initialize portfolio system this.portfolioManager = PortfolioManager.getInstance(); this.migrationManager = new MigrationManager(this.portfolioManager); // CRITICAL FIX: Don't access directories until after migration runs // Previously: this.personasDir was set here, creating directories before migration could fix them // Now: We delay directory access until initializePortfolio() completes this.personasDir = ''; // Temporary - will be set after migration // Initialize element managers this.skillManager = new SkillManager(); this.templateManager = new TemplateManager(); this.agentManager = new AgentManager(this.portfolioManager.getBaseDir()); // Log resolved path for debugging logger.info(`Personas directory resolved to: ${this.personasDir}`); // PathValidator will be initialized after migration completes // Load user identity from environment variables this.currentUser = process.env.DOLLHOUSE_USER || null; // Load indicator configuration this.indicatorConfig = loadIndicatorConfig(); // Initialize persona manager // Initialize collection modules this.githubClient = new GitHubClient(this.apiCache, this.rateLimitTracker); this.githubAuthManager = new GitHubAuthManager(this.apiCache); this.collectionBrowser = new CollectionBrowser(this.githubClient, this.collectionCache); this.collectionSearch = new CollectionSearch(this.githubClient, this.collectionCache); this.personaDetails = new PersonaDetails(this.githubClient); this.elementInstaller = new ElementInstaller(this.githubClient); this.personaSubmitter = new PersonaSubmitter(); // Update manager will be initialized after migration completes to avoid jsdom crash // Initialize export/import/share modules this.personaExporter = new PersonaExporter(this.currentUser); // PersonaImporter will be initialized after migration completes this.personaSharer = new PersonaSharer(this.githubClient, this.currentUser); // Initialize server setup this.serverSetup = new ServerSetup(); this.serverSetup.setupServer(this.server, this); // Initialize portfolio and perform migration if needed this.initializePortfolio().then(() => { // NOW safe to access directories after migration this.personasDir = this.portfolioManager.getElementDir(ElementType.PERSONA); // Log resolved path for debugging logger.info(`Personas directory resolved to: ${this.personasDir}`); // Initialize PathValidator with the personas directory PathValidator.initialize(this.personasDir); // Initialize update manager with safe directory // Use the parent of personas directory to avoid production check const safeDir = path.dirname(this.personasDir); try { this.updateManager = new UpdateManager(safeDir); } catch (error) { console.error('[DollhouseMCP] Failed to initialize UpdateManager:', error); logger.error(`Failed to initialize UpdateManager: ${error}`); // Continue without update functionality } // Initialize import module that depends on personasDir this.personaImporter = new PersonaImporter(this.personasDir, this.currentUser); this.loadPersonas(); }).catch(error => { // Don't use CRITICAL in the error message as it triggers Docker test failures console.error('[DollhouseMCP] Failed to initialize portfolio:', error); logger.error(`Failed to initialize portfolio: ${error}`); }); } async initializePortfolio() { // Check if migration is needed const needsMigration = await this.migrationManager.needsMigration(); if (needsMigration) { logger.info('Legacy personas detected. Starting migration...'); const result = await this.migrationManager.migrate({ backup: true }); if (result.success) { logger.info(`Successfully migrated ${result.migratedCount} personas`); if (result.backedUp && result.backupPath) { logger.info(`Backup created at: ${result.backupPath}`); } } else { logger.error('Migration completed with errors:'); result.errors.forEach(err => logger.error(` - ${err}`)); } } // Ensure portfolio structure exists const portfolioExists = await this.portfolioManager.exists(); if (!portfolioExists) { logger.info('Creating portfolio directory structure...'); await this.portfolioManager.initialize(); } // Initialize collection cache for anonymous access await this.initializeCollectionCache(); } /** * Initialize collection cache with seed data for anonymous browsing */ async initializeCollectionCache() { try { const isCacheValid = await this.collectionCache.isCacheValid(); if (!isCacheValid) { logger.info('Initializing collection cache with seed data...'); const { CollectionSeeder } = await import('./collection/CollectionSeeder.js'); const seedData = CollectionSeeder.getSeedData(); await this.collectionCache.saveCache(seedData); logger.info(`Collection cache initialized with ${seedData.length} items`); } else { const stats = await this.collectionCache.getCacheStats(); logger.debug(`Collection cache already valid with ${stats.itemCount} items`); } } catch (error) { logger.error(`Failed to initialize collection cache: ${error}`); // Don't throw - cache failures shouldn't prevent server startup } } // Tool handler methods - now public for access from tool modules getPersonaIndicator() { if (!this.activePersona) { return ""; } const persona = this.personas.get(this.activePersona); if (!persona) { return ""; } return formatIndicator(this.indicatorConfig, { name: persona.metadata.name, version: persona.metadata.version, author: persona.metadata.author, category: persona.metadata.category }); } /** * Normalize element type to handle both singular (new) and plural (legacy) forms * This provides backward compatibility during the transition to v1.4.0 */ normalizeElementType(type) { // Map plural forms to singular ElementType values const pluralToSingularMap = { 'personas': ElementType.PERSONA, 'skills': ElementType.SKILL, 'templates': ElementType.TEMPLATE, 'agents': ElementType.AGENT, 'memories': ElementType.MEMORY, 'ensembles': ElementType.ENSEMBLE }; // If it's already a valid ElementType value, return as-is if (Object.values(ElementType).includes(type)) { return type; } // If it's a plural form, convert to singular if (pluralToSingularMap[type]) { // Log deprecation warning logger.warn(`Using plural element type '${type}' is deprecated. Please use singular form '${pluralToSingularMap[type]}' instead.`); return pluralToSingularMap[type]; } // Unknown type - return as-is and let validation handle it return type; } /** * Sanitize metadata object to prevent prototype pollution * Removes any dangerous properties that could affect Object.prototype */ sanitizeMetadata(metadata) { if (!metadata || typeof metadata !== 'object') { return {}; } const dangerousProperties = ['__proto__', 'constructor', 'prototype']; const sanitized = {}; for (const [key, value] of Object.entries(metadata)) { if (!dangerousProperties.includes(key)) { // Recursively sanitize nested objects if (value && typeof value === 'object' && !Array.isArray(value)) { sanitized[key] = this.sanitizeMetadata(value); } else { sanitized[key] = value; } } } return sanitized; } async loadPersonas() { // Validate the personas directory path if (!path.isAbsolute(this.personasDir)) { logger.warn(`Personas directory path is not absolute: ${this.personasDir}`); } try { await fs.access(this.personasDir); } catch (error) { // Create personas directory if it doesn't exist try { await fs.mkdir(this.personasDir, { recursive: true }); logger.info(`Created personas directory at: ${this.personasDir}`); // Continue to try loading (directory will be empty) } catch (mkdirError) { logger.error(`Failed to create personas directory at ${this.personasDir}: ${mkdirError.message}`); // Don't throw - empty portfolio is valid this.personas.clear(); return; } } try { const files = await fs.readdir(this.personasDir); const markdownFiles = files.filter(file => file.endsWith('.md')); this.personas.clear(); if (markdownFiles.length === 0) { logger.info('[DollhouseMCP] No personas found in portfolio. Use browse_collection to install some!'); } for (const file of markdownFiles) { try { const filePath = path.join(this.personasDir, file); const fileContent = await PathValidator.safeReadFile(filePath); // Use secure YAML parser let parsed; try { parsed = SecureYamlParser.safeMatter(fileContent); } catch (error) { if (error instanceof SecurityError) { logger.warn(`Security threat detected in persona ${file}: ${error.message}`); continue; } throw error; } const metadata = parsed.data; const content = parsed.content; if (!metadata.name) { metadata.name = path.basename(file, '.md'); } // Generate unique ID if not present let uniqueId = metadata.unique_id; if (!uniqueId) { const authorForId = metadata.author || this.getCurrentUserForAttribution(); uniqueId = generateUniqueId(metadata.name, authorForId); logger.debug(`Generated unique ID for ${metadata.name}: ${uniqueId}`); } // Set default values for new metadata fields if (!metadata.category) metadata.category = 'general'; if (!metadata.age_rating) metadata.age_rating = 'all'; if (!metadata.content_flags) metadata.content_flags = []; if (metadata.ai_generated === undefined) metadata.ai_generated = false; if (!metadata.generation_method) metadata.generation_method = 'human'; if (!metadata.price) metadata.price = 'free'; if (!metadata.license) metadata.license = 'CC-BY-SA-4.0'; const persona = { metadata, content, filename: file, unique_id: uniqueId, }; this.personas.set(file, persona); logger.debug(`Loaded persona: ${metadata.name} (${uniqueId}`); } catch (error) { logger.error(`Error loading persona ${file}: ${error}`); } } } catch (error) { // Handle ENOENT gracefully - directory might not exist yet if (error.code === 'ENOENT') { logger.info('[DollhouseMCP] Personas directory does not exist yet - portfolio is empty'); this.personas.clear(); return; } logger.error(`Error reading personas directory: ${error}`); this.personas.clear(); } } async listPersonas() { const personaList = Array.from(this.personas.values()).map(persona => ({ filename: persona.filename, unique_id: persona.unique_id, name: persona.metadata.name, description: persona.metadata.description, triggers: persona.metadata.triggers || [], version: persona.metadata.version || "1.0", author: persona.metadata.author || "Unknown", category: persona.metadata.category || 'general', age_rating: persona.metadata.age_rating || 'all', price: persona.metadata.price || 'free', ai_generated: persona.metadata.ai_generated || false, active: this.activePersona === persona.filename, })); if (personaList.length === 0) { return { content: [ { type: "text", text: `${this.getPersonaIndicator()}You don't have any personas installed yet. Would you like to browse the DollhouseMCP collection on GitHub to see what's available? I can show you personas for creative writing, technical analysis, and more. Just say "yes" or use 'browse_collection'.`, }, ], }; } return { content: [ { type: "text", text: `${this.getPersonaIndicator()}Available Personas (${personaList.length}):\n\n` + personaList.map(p => `${p.active ? '🔹 ' : '▫️ '}**${p.name}** (${p.unique_id})\n` + ` ${p.description}\n` + ` 📁 ${p.category} | 🎭 ${p.author} | 🔖 ${p.price} | ${p.ai_generated ? '🤖 AI' : '👤 Human'}\n` + ` Age: ${p.age_rating} | Version: ${p.version}\n` + ` Triggers: ${p.triggers.join(', ') || 'None'}\n`).join('\n'), }, ], }; } async activatePersona(personaIdentifier) { // Enhanced input validation for persona identifier const validatedIdentifier = MCPInputValidator.validatePersonaIdentifier(personaIdentifier); // Try to find persona by filename first, then by name let persona = this.personas.get(validatedIdentifier); if (!persona) { // Search by name persona = Array.from(this.personas.values()).find(p => p.metadata.name.toLowerCase() === validatedIdentifier.toLowerCase()); } if (!persona) { throw new McpError(ErrorCode.InvalidParams, `Persona not found: ${personaIdentifier}`); } this.activePersona = persona.filename; return { content: [ { type: "text", text: `${this.getPersonaIndicator()}Persona Activated: **${persona.metadata.name}**\n\n` + `${persona.metadata.description}\n\n` + `**Instructions:**\n${persona.content}`, }, ], }; } async getActivePersona() { if (!this.activePersona) { return { content: [ { type: "text", text: `${this.getPersonaIndicator()}No persona is currently active.`, }, ], }; } const persona = this.personas.get(this.activePersona); if (!persona) { this.activePersona = null; return { content: [ { type: "text", text: `${this.getPersonaIndicator()}Active persona not found. Deactivated.`, }, ], }; } return { content: [ { type: "text", text: `${this.getPersonaIndicator()}Active Persona: **${persona.metadata.name}**\n\n` + `${persona.metadata.description}\n\n` + `File: ${persona.filename}\n` + `Version: ${persona.metadata.version || '1.0'}\n` + `Author: ${persona.metadata.author || 'Unknown'}`, }, ], }; } async deactivatePersona() { const wasActive = this.activePersona !== null; const indicator = this.getPersonaIndicator(); this.activePersona = null; return { content: [ { type: "text", text: wasActive ? `${indicator}✅ Persona deactivated. Back to default mode.` : "No persona was active.", }, ], }; } async getPersonaDetails(personaIdentifier) { // Try to find persona by filename first, then by name let persona = this.personas.get(personaIdentifier); if (!persona) { // Search by name persona = Array.from(this.personas.values()).find(p => p.metadata.name.toLowerCase() === personaIdentifier.toLowerCase()); } if (!persona) { throw new McpError(ErrorCode.InvalidParams, `Persona not found: ${personaIdentifier}`); } return { content: [ { type: "text", text: `${this.getPersonaIndicator()}📋 **${persona.metadata.name}** Details\n\n` + `**Description:** ${persona.metadata.description}\n` + `**File:** ${persona.filename}\n` + `**Version:** ${persona.metadata.version || '1.0'}\n` + `**Author:** ${persona.metadata.author || 'Unknown'}\n` + `**Triggers:** ${persona.metadata.triggers?.join(', ') || 'None'}\n\n` + `**Full Content:**\n\`\`\`\n${persona.content}\n\`\`\``, }, ], }; } async reloadPersonas() { await this.loadPersonas(); return { content: [ { type: "text", text: `${this.getPersonaIndicator()}🔄 Reloaded ${this.personas.size} personas from ${this.personasDir}`, }, ], }; } // ===== Element Methods (Generic for all element types) ===== async listElements(type) { try { // Normalize the type to handle both plural and singular forms const normalizedType = this.normalizeElementType(type); switch (normalizedType) { case ElementType.PERSONA: return this.listPersonas(); case ElementType.SKILL: { const skills = await this.skillManager.list(); if (skills.length === 0) { return { content: [{ type: "text", text: "No skills are currently installed. The DollhouseMCP collection has skills for code review, data analysis, creative writing and more. Would you like me to show you what's available? Just say \"yes\" or I can help you create a custom skill." }] }; } const skillList = skills.map(skill => { const complexity = skill.metadata.complexity || 'beginner'; const domains = skill.metadata.domains?.join(', ') || 'general'; return `🛠️ ${skill.metadata.name} - ${skill.metadata.description}\n Complexity: ${complexity} | Domains: ${domains}`; }).join('\n\n'); return { content: [{ type: "text", text: `📚 Available Skills:\n\n${skillList}` }] }; } case ElementType.TEMPLATE: { const templates = await this.templateManager.list(); if (templates.length === 0) { return { content: [{ type: "text", text: "You haven't installed any templates yet. Would you like to see available templates for emails, reports, and documentation? I can show you examples from the collection or help you create your own. What would you prefer?" }] }; } const templateList = templates.map(template => { const variables = template.metadata.variables?.map(v => v.name).join(', ') || 'none'; return `📄 ${template.metadata.name} - ${template.metadata.description}\n Variables: ${variables}`; }).join('\n\n'); return { content: [{ type: "text", text: `📝 Available Templates:\n\n${templateList}` }] }; } case ElementType.AGENT: { const agents = await this.agentManager.list(); if (agents.length === 0) { return { content: [{ type: "text", text: "No agents installed yet. Agents are autonomous helpers that can work on tasks independently. The DollhouseMCP collection includes task managers, research assistants, and more. Would you like to browse available agents or learn how to create your own?" }] }; } const agentList = agents.map(agent => { const specializations = agent.metadata.specializations?.join(', ') || 'general'; const status = agent.getStatus(); return `🤖 ${agent.metadata.name} - ${agent.metadata.description}\n Status: ${status} | Specializations: ${specializations}`; }).join('\n\n'); return { content: [{ type: "text", text: `🤖 Available Agents:\n\n${agentList}` }] }; } default: return { content: [{ type: "text", text: `❌ Unknown element type '${type}'. Available types: ${Object.values(ElementType).join(', ')} (or legacy plural forms: personas, skills, templates, agents)` }] }; } } catch (error) { logger.error(`Failed to list ${type} elements:`, error); return { content: [{ type: "text", text: `❌ Failed to list ${type}: ${error instanceof Error ? error.message : 'Unknown error'}` }] }; } } async activateElement(name, type) { try { // Normalize the type to handle both plural and singular forms const normalizedType = this.normalizeElementType(type); switch (normalizedType) { case ElementType.PERSONA: return this.activatePersona(name); case ElementType.SKILL: { const skill = await this.skillManager.find(s => s.metadata.name === name); if (!skill) { return { content: [{ type: "text", text: `❌ Skill '${name}' not found` }] }; } // Activate the skill await skill.activate?.(); return { content: [{ type: "text", text: `✅ Skill '${name}' activated\n\n${skill.instructions}` }] }; } case ElementType.TEMPLATE: { const template = await this.templateManager.find(t => t.metadata.name === name); if (!template) { return { content: [{ type: "text", text: `❌ Template '${name}' not found` }] }; } const variables = template.metadata.variables?.map(v => v.name).join(', ') || 'none'; return { content: [{ type: "text", text: `✅ Template '${name}' ready to use\nVariables: ${variables}\n\nUse 'render_template' to generate content with this template.` }] }; } case ElementType.AGENT: { const agent = await this.agentManager.find(a => a.metadata.name === name); if (!agent) { return { content: [{ type: "text", text: `❌ Agent '${name}' not found` }] }; } // Activate the agent await agent.activate(); return { content: [{ type: "text", text: `✅ Agent '${name}' activated and ready\nSpecializations: ${agent.metadata.specializations?.join(', ') || 'general'}\n\nUse 'execute_agent' to give this agent a goal.` }] }; } default: return { content: [{ type: "text", text: `❌ Unknown element type '${type}'` }] }; } } catch (error) { logger.error(`Failed to activate ${type} '${name}':`, error); return { content: [{ type: "text", text: `❌ Failed to activate ${type} '${name}': ${error instanceof Error ? error.message : 'Unknown error'}` }] }; } } async getActiveElements(type) { try { // Normalize the type to handle both plural and singular forms const normalizedType = this.normalizeElementType(type); switch (normalizedType) { case ElementType.PERSONA: return this.getActivePersona(); case ElementType.SKILL: { const skills = await this.skillManager.list(); const activeSkills = skills.filter(s => s.getStatus() === 'active'); if (activeSkills.length === 0) { return { content: [{ type: "text", text: "📋 No active skills" }] }; } const skillList = activeSkills.map(s => `🛠️ ${s.metadata.name}`).join(', '); return { content: [{ type: "text", text: `Active skills: ${skillList}` }] }; } case ElementType.TEMPLATE: { return { content: [{ type: "text", text: "📝 Templates are stateless and activated on-demand when rendering" }] }; } case ElementType.AGENT: { const agents = await this.agentManager.list(); const activeAgents = agents.filter(a => a.getStatus() === 'active'); if (activeAgents.length === 0) { return { content: [{ type: "text", text: "🤖 No active agents" }] }; } const agentList = activeAgents.map(a => { const goals = a.state?.goals?.length || 0; return `🤖 ${a.metadata.name} (${goals} active goals)`; }).join('\n'); return { content: [{ type: "text", text: `Active agents:\n${agentList}` }] }; } default: return { content: [{ type: "text", text: `❌ Unknown element type '${type}'` }] }; } } catch (error) { logger.error(`Failed to get active ${type}:`, error); return { content: [{ type: "text", text: `❌ Failed to get active ${type}: ${error instanceof Error ? error.message : 'Unknown error'}` }] }; } } async deactivateElement(name, type) { try { // Normalize the type to handle both plural and singular forms const normalizedType = this.normalizeElementType(type); switch (normalizedType) { case ElementType.PERSONA: return this.deactivatePersona(); case ElementType.SKILL: { const skill = await this.skillManager.find(s => s.metadata.name === name); if (!skill) { return { content: [{ type: "text", text: `❌ Skill '${name}' not found` }] }; } await skill.deactivate?.(); return { content: [{ type: "text", text: `✅ Skill '${name}' deactivated` }] }; } case ElementType.TEMPLATE: { return { content: [{ type: "text", text: "📝 Templates are stateless - nothing to deactivate" }] }; } case ElementType.AGENT: { const agent = await this.agentManager.find(a => a.metadata.name === name); if (!agent) { return { content: [{ type: "text", text: `❌ Agent '${name}' not found` }] }; } await agent.deactivate(); return { content: [{ type: "text", text: `✅ Agent '${name}' deactivated` }] }; } default: return { content: [{ type: "text", text: `❌ Unknown element type '${type}'` }] }; } } catch (error) { logger.error(`Failed to deactivate ${type} '${name}':`, error); return { content: [{ type: "text", text: `❌ Failed to deactivate ${type} '${name}': ${error instanceof Error ? error.message : 'Unknown error'}` }] }; } } async getElementDetails(name, type) { try { // Normalize the type to handle both plural and singular forms const normalizedType = this.normalizeElementType(type); switch (normalizedType) { case ElementType.PERSONA: return this.getPersonaDetails(name); case ElementType.SKILL: { const skill = await this.skillManager.find(s => s.metadata.name === name); if (!skill) { return { content: [{ type: "text", text: `❌ Skill '${name}' not found` }] }; } const details = [ `🛠️ **${skill.metadata.name}**`, `${skill.metadata.description}`, ``, `**Complexity**: ${skill.metadata.complexity || 'beginner'}`, `**Domains**: ${skill.metadata.domains?.join(', ') || 'general'}`, `**Languages**: ${skill.metadata.languages?.join(', ') || 'any'}`, `**Prerequisites**: ${skill.metadata.prerequisites?.join(', ') || 'none'}`, ``, `**Instructions**:`, skill.instructions ]; if (skill.metadata.parameters && skill.metadata.parameters.length > 0) { details.push('', '**Parameters**:'); skill.metadata.parameters.forEach(p => { details.push(`- ${p.name} (${p.type}): ${p.description}`); }); } return { content: [{ type: "text", text: details.join('\n') }] }; } case ElementType.TEMPLATE: { const template = await this.templateManager.find(t => t.metadata.name === name); if (!template) { return { content: [{ type: "text", text: `❌ Template '${name}' not found` }] }; } const details = [ `📄 **${template.metadata.name}**`, `${template.metadata.description}`, ``, `**Output Format**: ${template.metadata.output_format || 'text'}`, `**Template Content**:`, '```', template.content, '```' ]; if (template.metadata.variables && template.metadata.variables.length > 0) { details.push('', '**Variables**:'); template.metadata.variables.forEach(v => { details.push(`- ${v.name} (${v.type}): ${v.description}`); }); } return { content: [{ type: "text", text: details.join('\n') }] }; } case ElementType.AGENT: { const agent = await this.agentManager.find(a => a.metadata.name === name); if (!agent) { return { content: [{ type: "text", text: `❌ Agent '${name}' not found` }] }; } const details = [ `🤖 **${agent.metadata.name}**`, `${agent.metadata.description}`, ``, `**Status**: ${agent.getStatus()}`, `**Specializations**: ${agent.metadata.specializations?.join(', ') || 'general'}`, `**Decision Framework**: ${agent.metadata.decisionFramework || 'rule-based'}`, `**Risk Tolerance**: ${agent.metadata.riskTolerance || 'low'}`, ``, `**Instructions**:`, agent.instructions || 'No instructions available' ]; const agentState = agent.state; if (agentState?.goals && agentState.goals.length > 0) { details.push('', '**Current Goals**:'); agentState.goals.forEach((g) => { details.push(`- ${g.description} (${g.status})`); }); } return { content: [{ type: "text", text: details.join('\n') }] }; } default: return { content: [{ type: "text", text: `❌ Unknown element type '${type}'` }] }; } } catch (error) { logger.error(`Failed to get ${type} details for '${name}':`, error); return { content: [{ type: "text", text: `❌ Failed to get ${type} details: ${error instanceof Error ? error.message : 'Unknown error'}` }] }; } } async reloadElements(type) { try { // Normalize the type to handle both plural and singular forms const normalizedType = this.normalizeElementType(type); switch (normalizedType) { case ElementType.PERSONA: return this.reloadPersonas(); case ElementType.SKILL: { this.skillManager.clearCache(); const skills = await this.skillManager.list(); return { content: [{ type: "text", text: `🔄 Reloaded ${skills.length} skills from portfolio` }] }; } case ElementType.TEMPLATE: { // Template manager doesn't have clearCache, just list const templates = await this.templateManager.list(); return { content: [{ type: "text", text: `🔄 Reloaded ${templates.length} templates from portfolio` }] }; } case ElementType.AGENT: { // Agent manager doesn't have clearCache, just list const agents = await this.agentManager.list(); return { content: [{ type: "text", text: `🔄 Reloaded ${agents.length} agents from portfolio` }] }; } default: return { content: [{ type: "text", text: `❌ Unknown element type '${type}'` }] }; } } catch (error) { logger.error(`Failed to reload ${type}:`, error); return { content: [{ type: "text", text: `❌ Failed to reload ${type}: ${error instanceof Error ? error.message : 'Unknown error'}` }] }; } } // Element-specific methods async renderTemplate(name, variables) { try { const template = await this.templateManager.find(t => t.metadata.name === name); if (!template) { return { content: [{ type: "text", text: `❌ Template '${name}' not found` }] }; } // Simple template rendering - replace variables in content let rendered = template.content; for (const [key, value] of Object.entries(variables)) { const regex = new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, 'g'); rendered = rendered.replace(regex, String(value)); } return { content: [{ type: "text", text: `📄 Rendered template '${name}':\n\n${rendered}` }] }; } catch (error) { logger.error(`Failed to render template '${name}':`, error); return { content: [{ type: "text", text: `❌ Failed to render template: ${error instanceof Error ? error.message : 'Unknown error'}` }] }; } } async executeAgent(name, goal) { try { const agent = await this.agentManager.find(a => a.metadata.name === name); if (!agent) { return { content: [{ type: "text", text: `❌ Agent '${name}' not found` }] }; } // Simple agent execution simulation const result = { summary: `Agent '${name}' is now working on: ${goal}`, status: 'in-progress', actionsTaken: 1 }; return { content: [{ type: "text", text: `🤖 Agent '${name}' execution result:\n\n${result.summary}\n\nStatus: ${result.status}\nActions taken: ${result.actionsTaken || 0}` }] }; } catch (error) { logger.error(`Failed to execute agent '${name}':`, error); return { content: [{ type: "text", text: `❌ Failed to execute agent: ${error instanceof Error ? error.message : 'Unknown error'}` }] }; } } async createElement(args) { try { const { name, type, description, content, metadata } = args; // Validate element type if (!Object.values(ElementType).includes(type)) { return { content: [{ type: "text", text: `❌ Invalid element type '${type}'. Valid types: ${Object.values(ElementType).join(', ')} (or legacy plural forms: personas, skills, templates, agents)` }] }; } // Validate inputs const validatedName = validateFilename(name); const validatedDescription = sanitizeInput(description, SECURITY_LIMITS.MAX_METADATA_FIELD_LENGTH); // SECURITY FIX: Sanitize metadata to prevent prototype pollution const sanitizedMetadata = this.sanitizeMetadata(metadata || {}); // Create element based on type switch (type) { case ElementType.PERSONA: // Use existing persona creation logic return this.create