Skip to main content
Glama
DollhouseMCP

DollhouseMCP

Official

import_persona

Add a custom AI persona to the DollhouseMCP server by importing from a file or JSON string, enabling dynamic behavior switching for compatible assistants.

Instructions

Import a persona from a file path or JSON string

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
sourceYesFile path to a .md or .json file, or a JSON string of the persona
overwriteNoOverwrite if persona already exists (default: false)

Implementation Reference

  • MCP tool definition for 'import_persona' including handler function that delegates to server.importPersona
      {
        tool: {
          name: "import_persona",
          description: "Import a persona from a file path or JSON string",
          inputSchema: {
            type: "object",
            properties: {
              source: {
                type: "string",
                description: "File path to a .md or .json file, or a JSON string of the persona",
              },
              overwrite: {
                type: "boolean",
                description: "Overwrite if persona already exists (default: false)",
              },
            },
            required: ["source"],
          },
        },
        handler: (args: any) => server.importPersona(args.source, args.overwrite)
      }
    ];
  • Interface definition for the server.importPersona method called by the tool handler
    importPersona(source: string, overwrite?: boolean): Promise<any>;
  • Registers the persona tools (including import_persona) with the MCP tool registry
    this.toolRegistry.registerMany(getPersonaExportImportTools(instance));
  • Main importPersona method implementing parsing, validation, sanitization, conflict detection, and file saving for imported personas
    async importPersona(source: string, existingPersonas: Map<string, Persona>, overwrite = false): Promise<ImportResult> {
      try {
        // Determine source type
        let personaData: ExportedPersona | null = null;
    
        // Check if it's a file path
        if (source.startsWith('/') || source.startsWith('./') || source.endsWith('.md') || source.endsWith('.json')) {
          personaData = await this.importFromFile(source);
        } 
        // Check if it's base64 encoded
        else if (this.isBase64(source)) {
          personaData = await this.importFromBase64(source);
        }
        // Try parsing as JSON directly
        else {
          try {
            const parsed = JSON.parse(source);
            if (this.isExportBundle(parsed)) {
              return this.importBundle(parsed, existingPersonas, overwrite);
            } else if (this.isExportedPersona(parsed)) {
              personaData = parsed;
            }
          } catch {
            // Not JSON, might be raw markdown
            return this.importFromMarkdown(source, existingPersonas, overwrite);
          }
        }
    
        if (!personaData) {
          return {
            success: false,
            message: "Could not parse import source. Please provide a file path, JSON string, or base64 encoded data."
          };
        }
    
        // Validate and create persona
        return await this.createPersonaFromExport(personaData, existingPersonas, overwrite);
    
      } catch (error) {
        logger.error('Import error', error);
        return {
          success: false,
          message: `Import failed: ${error instanceof Error ? error.message : String(error)}`
        };
      }
    }
  • PersonaImporter class providing comprehensive import functionality with security features used likely by server.importPersona
    export class PersonaImporter {
      constructor(
        private personasDir: string,
        private currentUser: string | null
      ) {}
    
      /**
       * Import a persona from various sources
       */
      async importPersona(source: string, existingPersonas: Map<string, Persona>, overwrite = false): Promise<ImportResult> {
        try {
          // Determine source type
          let personaData: ExportedPersona | null = null;
    
          // Check if it's a file path
          if (source.startsWith('/') || source.startsWith('./') || source.endsWith('.md') || source.endsWith('.json')) {
            personaData = await this.importFromFile(source);
          } 
          // Check if it's base64 encoded
          else if (this.isBase64(source)) {
            personaData = await this.importFromBase64(source);
          }
          // Try parsing as JSON directly
          else {
            try {
              const parsed = JSON.parse(source);
              if (this.isExportBundle(parsed)) {
                return this.importBundle(parsed, existingPersonas, overwrite);
              } else if (this.isExportedPersona(parsed)) {
                personaData = parsed;
              }
            } catch {
              // Not JSON, might be raw markdown
              return this.importFromMarkdown(source, existingPersonas, overwrite);
            }
          }
    
          if (!personaData) {
            return {
              success: false,
              message: "Could not parse import source. Please provide a file path, JSON string, or base64 encoded data."
            };
          }
    
          // Validate and create persona
          return await this.createPersonaFromExport(personaData, existingPersonas, overwrite);
    
        } catch (error) {
          logger.error('Import error', error);
          return {
            success: false,
            message: `Import failed: ${error instanceof Error ? error.message : String(error)}`
          };
        }
      }
    
      /**
       * Import from file path
       */
      private async importFromFile(filePath: string): Promise<ExportedPersona | null> {
        try {
          // Validate path
          const validatedPath = validatePath(filePath);
          const content = await fs.readFile(validatedPath, 'utf-8');
    
          if (filePath.endsWith('.json')) {
            const parsed = JSON.parse(content);
            if (this.isExportedPersona(parsed)) {
              return parsed;
            } else if (this.isExportBundle(parsed)) {
              throw new Error("This is a bundle file. It will be imported as multiple personas.");
            }
          } else if (filePath.endsWith('.md')) {
            // Parse markdown file
            const { data, content: mdContent } = matter(content);
            const filename = path.basename(filePath);
            return {
              metadata: data as PersonaMetadata,
              content: mdContent,
              filename: filename,
              exportedAt: new Date().toISOString()
            };
          }
        } catch (error) {
          logger.error('File import error', error);
          throw error;
        }
        return null;
      }
    
      /**
       * Import from base64 string
       */
      private async importFromBase64(base64: string): Promise<ExportedPersona | null> {
        try {
          const json = Buffer.from(base64, 'base64').toString('utf-8');
          const parsed = JSON.parse(json);
          
          if (this.isExportedPersona(parsed)) {
            return parsed;
          } else if (this.isExportBundle(parsed)) {
            throw new Error("This is a bundle. It will be imported as multiple personas.");
          }
        } catch (error) {
          logger.error('Base64 import error', error);
        }
        return null;
      }
    
      /**
       * Import from raw markdown content
       */
      private async importFromMarkdown(content: string, existingPersonas: Map<string, Persona>, overwrite: boolean): Promise<ImportResult> {
        try {
          // Validate content size
          validateContentSize(content, 100 * 1024); // 100KB limit
    
          // Try to parse as markdown with frontmatter
          const { data, content: mdContent } = matter(content);
          
          if (!data.name || !data.description) {
            return {
              success: false,
              message: "Invalid persona format. Must include name and description in YAML frontmatter."
            };
          }
    
          const metadata = data as PersonaMetadata;
          const filename = `${metadata.name.toLowerCase().replaceAll(/\s+/g, '-')}.md`;
    
          const exportedPersona: ExportedPersona = {
            metadata,
            content: mdContent,
            filename,
            exportedAt: new Date().toISOString(),
            exportedBy: this.currentUser || undefined
          };
    
          return await this.createPersonaFromExport(exportedPersona, existingPersonas, overwrite);
        } catch (error) {
          return {
            success: false,
            message: `Failed to parse markdown: ${error instanceof Error ? error.message : String(error)}`
          };
        }
      }
    
      /**
       * Import a bundle of personas
       */
      private async importBundle(bundle: ExportBundle, existingPersonas: Map<string, Persona>, overwrite: boolean): Promise<ImportResult> {
        const results = {
          success: true,
          imported: [] as string[],
          failed: [] as string[],
          conflicts: [] as string[]
        };
    
        for (const personaData of bundle.personas) {
          const result = await this.createPersonaFromExport(personaData, existingPersonas, overwrite);
          
          if (result.success) {
            results.imported.push(personaData.metadata.name);
          } else {
            results.failed.push(`${personaData.metadata.name}: ${result.message}`);
            if (result.conflicts) {
              results.conflicts.push(...result.conflicts);
            }
          }
        }
    
        return {
          success: results.failed.length === 0,
          message: this.formatBundleImportResult(results),
          conflicts: results.conflicts.length > 0 ? results.conflicts : undefined
        };
      }
    
      /**
       * Create persona from exported data
       */
      private async createPersonaFromExport(
        exportData: ExportedPersona, 
        existingPersonas: Map<string, Persona>, 
        overwrite: boolean
      ): Promise<ImportResult> {
        try {
          // Validate metadata
          const metadata = await this.validateAndEnrichMetadata(exportData.metadata);
          
          // Validate and normalize Unicode content first
          const unicodeResult = UnicodeValidator.normalize(exportData.content);
          if (unicodeResult.severity === 'critical') {
            throw new Error(`Critical Unicode security threat detected: ${unicodeResult.detectedIssues?.join(', ')}`);
          }
          const unicodeNormalizedContent = unicodeResult.normalizedContent;
    
          // Then validate content for other security threats
          const validationResult = ContentValidator.validateAndSanitize(unicodeNormalizedContent);
          if (!validationResult.isValid && validationResult.severity === 'critical') {
            throw new Error(`Critical security threat detected: ${validationResult.detectedPatterns?.join(', ')}`);
          }
          const sanitizedContent = validationResult.sanitizedContent || unicodeNormalizedContent;
    
          // Generate safe filename
          let filename = validateFilename(exportData.filename || `${metadata.name.toLowerCase().replaceAll(/\s+/g, '-')}.md`);
          
          // Check for conflicts
          const conflicts = this.findConflicts(metadata.name, filename, existingPersonas);
          if (conflicts.length > 0 && !overwrite) {
            return {
              success: false,
              message: `Persona already exists: ${conflicts.join(', ')}. Use overwrite=true to replace.`,
              conflicts
            };
          }
    
          // Create the persona file
          const personaPath = path.join(this.personasDir, filename);
          const fileContent = matter.stringify(sanitizedContent, metadata);
          
          // Use file locking to prevent race conditions
          await FileLockManager.withLock(`persona:${metadata.name}`, async () => {
            await FileLockManager.atomicWriteFile(personaPath, fileContent);
          });
    
          // Create persona object
          const persona: Persona = {
            metadata,
            content: sanitizedContent,
            filename,
            unique_id: metadata.unique_id!
          };
    
          return {
            success: true,
            message: `Successfully imported "${metadata.name}"`,
            persona,
            filename
          };
    
        } catch (error) {
          logger.error('Create persona error', error);
          return {
            success: false,
            message: `Failed to create persona: ${error instanceof Error ? error.message : String(error)}`
          };
        }
      }
    
      /**
       * Validate and enrich metadata
       */
      private async validateAndEnrichMetadata(metadata: any): Promise<PersonaMetadata> {
        // Ensure required fields
        if (!metadata.name || !metadata.description) {
          throw new Error("Missing required fields: name and description");
        }
    
        // Validate and normalize Unicode in metadata fields
        const nameResult = UnicodeValidator.normalize(metadata.name);
        const descResult = UnicodeValidator.normalize(metadata.description);
        
        if (nameResult.severity === 'critical') {
          throw new Error(`Critical Unicode security threat in persona name: ${nameResult.detectedIssues?.join(', ')}`);
        }
        if (descResult.severity === 'critical') {
          throw new Error(`Critical Unicode security threat in persona description: ${descResult.detectedIssues?.join(', ')}`);
        }
    
        // Use normalized values
        metadata.name = nameResult.normalizedContent;
        metadata.description = descResult.normalizedContent;
    
        // Generate unique_id if missing
        if (!metadata.unique_id) {
          metadata.unique_id = generateUniqueId(metadata.name, this.currentUser || 'imported');
        }
    
        // Set defaults
        metadata.version = metadata.version || '1.0';
        metadata.author = metadata.author || this.currentUser || 'imported';
        metadata.category = metadata.category || 'custom';
        metadata.created_date = metadata.created_date || new Date().toISOString();
    
        // Validate with YAML parser for security using normalized metadata
        const validated = await SecureYamlParser.parse(matter.stringify('', metadata));
    
        return validated.data as PersonaMetadata;
      }
    
      /**
       * Find conflicts with existing personas
       */
      private findConflicts(name: string, filename: string, existingPersonas: Map<string, Persona>): string[] {
        const conflicts: string[] = [];
    
        for (const [key, persona] of existingPersonas) {
          if (persona.metadata.name === name || persona.filename === filename) {
            conflicts.push(key);
          }
        }
    
        return conflicts;
      }
    
      /**
       * Check if string is base64
       */
      private isBase64(str: string): boolean {
        // Check length is multiple of 4
        if (str.length % 4 !== 0) return false;
        
        // SECURITY FIX: Ensure base64 string is not empty
        // Previously: /^[A-Za-z0-9+/]*={0,2}$/ allowed empty strings
        // Now: Require at least one character before optional padding
        return /^[A-Za-z0-9+/]+={0,2}$/.test(str);
      }
    
      /**
       * Type guard for ExportedPersona
       */
      private isExportedPersona(obj: any): obj is ExportedPersona {
        return obj && 
          typeof obj.metadata === 'object' &&
          typeof obj.content === 'string' &&
          typeof obj.filename === 'string';
      }
    
      /**
       * Type guard for ExportBundle
       */
      private isExportBundle(obj: any): obj is ExportBundle {
        return obj &&
          typeof obj.version === 'string' &&
          Array.isArray(obj.personas) &&
          typeof obj.personaCount === 'number';
      }
    
      /**
       * Format bundle import results
       */
      private formatBundleImportResult(results: any): string {
        let message = `Bundle Import Summary:\n`;
        message += `✅ Successfully imported: ${results.imported.length} personas\n`;
        
        if (results.imported.length > 0) {
          message += `Imported:\n${results.imported.map((n: string) => `  - ${n}`).join('\n')}\n`;
        }
    
        if (results.failed.length > 0) {
          message += `\n❌ Failed: ${results.failed.length} personas\n`;
          message += `Errors:\n${results.failed.map((e: string) => `  - ${e}`).join('\n')}`;
        }
    
        return message;
      }
    }

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/DollhouseMCP/mcp-server'

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