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;
      }
    }
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It states the import action but doesn't describe what happens during import (e.g., validation, error handling, side effects), whether it requires specific permissions, or what the expected outcome looks like. The mention of 'overwrite' parameter hints at mutation behavior, but this isn't elaborated in the description itself.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence that communicates the core functionality without unnecessary words. It's appropriately sized for a tool with two parameters and no complex behavioral nuances to explain. Every word earns its place in conveying the essential operation.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a mutation tool with no annotations and no output schema, the description is inadequate. It doesn't explain what constitutes a valid persona format, what validation occurs during import, what happens on success/failure, or what the tool returns. The 100% schema coverage helps with parameters, but the overall behavioral context is missing for a tool that presumably modifies system state.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already fully documents both parameters ('source' and 'overwrite'). The description adds no additional meaning beyond what's in the schema - it mentions 'file path or JSON string' which is covered by the schema's description of 'source', and doesn't elaborate on parameter interactions or constraints. Baseline 3 is appropriate when schema does all the work.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the action ('Import') and resource ('a persona'), specifying the source types ('from a file path or JSON string'). It distinguishes from siblings like 'create_element' by focusing on importing rather than creating from scratch. However, it doesn't explicitly differentiate from 'install_collection_content' which might involve similar data loading operations.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No guidance is provided on when to use this tool versus alternatives like 'create_element' or 'install_collection_content'. The description lacks context about prerequisites, typical use cases, or scenarios where importing is preferred over other creation methods. It mentions the source format but not when this operation is appropriate.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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