create_character
Generate D&D 5e characters with customizable stats, classes, races, equipment, and abilities for your RPG campaigns.
Instructions
Create a new D&D 5e character with stats, class, race, and equipment
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| name | Yes | ||
| race | No | Human | |
| class | No | Fighter | |
| level | No | ||
| background | No | ||
| characterType | No | pc | |
| stats | No | ||
| hp | No | ||
| maxHp | No | ||
| ac | No | ||
| speed | No | ||
| resistances | No | ||
| immunities | No | ||
| vulnerabilities | No | ||
| conditionImmunities | No | ||
| spellcastingAbility | No | ||
| knownSpells | No | ||
| preparedSpells | No | ||
| cantrips | No | ||
| skillProficiencies | No | ||
| saveProficiencies | No | ||
| equipment | No |
Implementation Reference
- src/modules/characters.ts:304-376 (handler)Core handler function that creates a new D&D 5e character: generates ID, calculates derived stats (HP, proficiency), builds Character object, persists to JSON file in app data dir, formats and returns markdown character sheet.export function createCharacter(input: CreateCharacterInput): { success: boolean; character: Character; markdown: string; } { // Generate unique ID const id = randomUUID(); // Get stats with defaults const stats = input.stats || { str: 10, dex: 10, con: 10, int: 10, wis: 10, cha: 10, }; // Calculate derived stats const conModifier = calculateModifier(stats.con); const proficiencyBonus = calculateProficiencyBonus(input.level); // Calculate HP (use provided values or auto-calculate) const maxHp = input.maxHp || calculateMaxHP(input.class, input.level, conModifier); const hp = input.hp !== undefined ? input.hp : maxHp; // Build character object const character: Character = { id, name: input.name, race: input.race, class: input.class, level: input.level, background: input.background, characterType: input.characterType, stats, hp, maxHp, ac: input.ac, speed: input.speed, proficiencyBonus, resistances: input.resistances, immunities: input.immunities, vulnerabilities: input.vulnerabilities, conditionImmunities: input.conditionImmunities, spellcastingAbility: input.spellcastingAbility, knownSpells: input.knownSpells, preparedSpells: input.preparedSpells, cantrips: input.cantrips, skillProficiencies: input.skillProficiencies, saveProficiencies: input.saveProficiencies, equipment: input.equipment, createdAt: new Date().toISOString(), }; // Save to file const dataDir = path.join(DATA_ROOT, 'characters'); if (!fs.existsSync(dataDir)) { fs.mkdirSync(dataDir, { recursive: true }); } const filePath = path.join(dataDir, `${id}.json`); fs.writeFileSync(filePath, JSON.stringify(character, null, 2), 'utf-8'); // Format markdown output const markdown = formatCharacterSheet(character); return { success: true, character, markdown, }; }
- src/modules/characters.ts:48-78 (schema)Zod input schema defining all parameters for character creation: name, race, class, level, stats, HP, AC, equipment, spells, proficiencies, etc. with sensible defaults and validation.export const createCharacterSchema = z.object({ name: z.string().min(1, 'Name is required'), race: z.string().default('Human'), class: z.string().default('Fighter'), level: z.number().min(1, 'Level must be at least 1').max(20, 'Level cannot exceed 20').default(1), background: z.string().optional(), characterType: z.enum(['pc', 'npc', 'enemy', 'neutral']).default('pc'), stats: z.object({ str: z.number().min(1, 'Ability score must be at least 1').max(30, 'Ability score cannot exceed 30').default(10), dex: z.number().min(1, 'Ability score must be at least 1').max(30, 'Ability score cannot exceed 30').default(10), con: z.number().min(1, 'Ability score must be at least 1').max(30, 'Ability score cannot exceed 30').default(10), int: z.number().min(1, 'Ability score must be at least 1').max(30, 'Ability score cannot exceed 30').default(10), wis: z.number().min(1, 'Ability score must be at least 1').max(30, 'Ability score cannot exceed 30').default(10), cha: z.number().min(1, 'Ability score must be at least 1').max(30, 'Ability score cannot exceed 30').default(10), }).optional(), hp: z.number().optional(), maxHp: z.number().optional(), ac: z.number().default(10), speed: z.number().default(30), resistances: z.array(DamageTypeSchema).optional(), immunities: z.array(DamageTypeSchema).optional(), vulnerabilities: z.array(DamageTypeSchema).optional(), conditionImmunities: z.array(ConditionSchema).optional(), spellcastingAbility: AbilitySchema.optional(), knownSpells: z.array(z.string()).optional(), preparedSpells: z.array(z.string()).optional(), cantrips: z.array(z.string()).optional(), skillProficiencies: z.array(SkillSchema).optional(), saveProficiencies: z.array(AbilitySchema).optional(), equipment: z.array(z.string()).optional(), });
- src/registry.ts:312-329 (registration)Tool registration in central registry: defines name, description, converts Zod schema to JSON schema for MCP, provides async handler that validates input, calls createCharacter, and returns formatted success response or error.create_character: { name: 'create_character', description: 'Create a new D&D 5e character with stats, class, race, and equipment', inputSchema: toJsonSchema(createCharacterSchema), handler: async (args) => { try { const validated = createCharacterSchema.parse(args); const result = createCharacter(validated); return success(result.markdown); } catch (err) { if (err instanceof z.ZodError) { const messages = err.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', '); return error(`Validation failed: ${messages}`); } const message = err instanceof Error ? err.message : String(err); return error(message); } },
- src/modules/characters.ts:382-643 (helper)Helper function that generates rich ASCII art markdown character sheet including HP bar, ability scores table, combat stats, equipment, spells, conditions, etc. Called by createCharacter.function formatCharacterSheet(character: Character): string { const content: string[] = []; const box = BOX.LIGHT; const WIDTH = 68; // Calculate effective stats with conditions const effectiveStats = calculateEffectiveStats(character.id, { maxHp: character.maxHp, hp: character.hp, speed: character.speed, ac: character.ac, }); // Character Header const typeLabel = character.characterType.toUpperCase(); content.push(centerText(`${character.name}`, WIDTH)); content.push(centerText(`${typeLabel} - ${character.race} ${character.class} (Level ${character.level})`, WIDTH)); if (character.background) { content.push(centerText(`Background: ${character.background}`, WIDTH)); } content.push(''); content.push(box.H.repeat(WIDTH)); content.push(''); // Combat Stats Section content.push(padText('COMBAT STATS', WIDTH, 'center')); content.push(''); // HP Bar - use effective maxHp if modified by conditions const displayMaxHp = effectiveStats.maxHp.effective; const displayHp = Math.min(character.hp, displayMaxHp); // Clamp current HP to effective max const hpBar = createStatusBar(displayHp, displayMaxHp, 40, 'HP'); content.push(centerText(hpBar, WIDTH)); // Show HP modification if conditions affect it if (effectiveStats.maxHp.modified) { const hpNote = `Base: ${character.hp}/${character.maxHp} → Effective: ${displayHp}/${displayMaxHp}`; content.push(centerText(hpNote, WIDTH)); } content.push(''); // Core combat stats in table content.push(createTableRow(['AC', 'Speed', 'Initiative', 'Prof Bonus'], [10, 12, 12, 14], 'LIGHT')); // Show effective values (with condition modifications if any) const displayAC = effectiveStats.ac ? effectiveStats.ac.effective : character.ac; const displaySpeed = effectiveStats.speed.effective; content.push(createTableRow([ displayAC.toString() + (effectiveStats.ac?.modified ? '*' : ''), `${displaySpeed} ft` + (effectiveStats.speed.modified ? '*' : ''), `+${calculateModifier(character.stats.dex)}`, `+${character.proficiencyBonus}` ], [10, 12, 12, 14], 'LIGHT')); // Show condition notes if stats are modified if (effectiveStats.speed.modified && effectiveStats.speed.base !== displaySpeed) { content.push(padText(` * Speed: ${effectiveStats.speed.base} ft (base)`, WIDTH, 'left')); } if (effectiveStats.ac?.modified && effectiveStats.ac.base !== displayAC) { content.push(padText(` * AC: ${effectiveStats.ac.base} (base)`, WIDTH, 'left')); } content.push(''); content.push(box.H.repeat(WIDTH)); content.push(''); // Ability Scores Section content.push(padText('ABILITY SCORES', WIDTH, 'center')); content.push(''); // Ability scores in two rows of three const abilities = [ { key: 'str', name: 'STR' }, { key: 'dex', name: 'DEX' }, { key: 'con', name: 'CON' }, { key: 'int', name: 'INT' }, { key: 'wis', name: 'WIS' }, { key: 'cha', name: 'CHA' }, ]; // Header content.push(createTableRow( abilities.slice(0, 3).map(a => a.name), [10, 10, 10], 'LIGHT' )); // Scores content.push(createTableRow( abilities.slice(0, 3).map(a => { const score = character.stats[a.key as keyof typeof character.stats]; return ` ${score}`; }), [10, 10, 10], 'LIGHT' )); // Modifiers content.push(createTableRow( abilities.slice(0, 3).map(a => { const score = character.stats[a.key as keyof typeof character.stats]; const mod = calculateModifier(score); return ` ${mod >= 0 ? '+' : ''}${mod}`; }), [10, 10, 10], 'LIGHT' )); content.push(''); // Second row content.push(createTableRow( abilities.slice(3, 6).map(a => a.name), [10, 10, 10], 'LIGHT' )); content.push(createTableRow( abilities.slice(3, 6).map(a => { const score = character.stats[a.key as keyof typeof character.stats]; return ` ${score}`; }), [10, 10, 10], 'LIGHT' )); content.push(createTableRow( abilities.slice(3, 6).map(a => { const score = character.stats[a.key as keyof typeof character.stats]; const mod = calculateModifier(score); return ` ${mod >= 0 ? '+' : ''}${mod}`; }), [10, 10, 10], 'LIGHT' )); // Proficiencies if (character.skillProficiencies && character.skillProficiencies.length > 0) { content.push(''); content.push(box.H.repeat(WIDTH)); content.push(''); content.push(padText('SKILL PROFICIENCIES', WIDTH, 'center')); content.push(''); const skills = character.skillProficiencies.join(', '); content.push(padText(skills, WIDTH, 'left')); } if (character.saveProficiencies && character.saveProficiencies.length > 0) { content.push(''); content.push(padText('SAVING THROWS: ' + character.saveProficiencies.map(s => s.toUpperCase()).join(', '), WIDTH, 'left')); } // Resistances/Immunities/Vulnerabilities const defenses: string[] = []; if (character.resistances && character.resistances.length > 0) { defenses.push(`Resistances: ${character.resistances.join(', ')}`); } if (character.immunities && character.immunities.length > 0) { defenses.push(`Immunities: ${character.immunities.join(', ')}`); } if (character.vulnerabilities && character.vulnerabilities.length > 0) { defenses.push(`Vulnerabilities: ${character.vulnerabilities.join(', ')}`); } if (character.conditionImmunities && character.conditionImmunities.length > 0) { defenses.push(`Condition Immunities: ${character.conditionImmunities.join(', ')}`); } if (defenses.length > 0) { content.push(''); content.push(box.H.repeat(WIDTH)); content.push(''); content.push(padText('DEFENSES', WIDTH, 'center')); content.push(''); defenses.forEach(d => content.push(padText(d, WIDTH, 'left'))); } // Spellcasting if (character.spellcastingAbility) { content.push(''); content.push(box.H.repeat(WIDTH)); content.push(''); content.push(padText('SPELLCASTING', WIDTH, 'center')); content.push(''); content.push(padText(`Ability: ${character.spellcastingAbility.toUpperCase()}`, WIDTH, 'left')); if (character.cantrips && character.cantrips.length > 0) { content.push(padText(`Cantrips: ${character.cantrips.join(', ')}`, WIDTH, 'left')); } if (character.knownSpells && character.knownSpells.length > 0) { content.push(padText(`Known: ${character.knownSpells.join(', ')}`, WIDTH, 'left')); } if (character.preparedSpells && character.preparedSpells.length > 0) { content.push(padText(`Prepared: ${character.preparedSpells.join(', ')}`, WIDTH, 'left')); } } // Equipment if (character.equipment && character.equipment.length > 0) { content.push(''); content.push(box.H.repeat(WIDTH)); content.push(''); content.push(padText('EQUIPMENT', WIDTH, 'center')); content.push(''); character.equipment.forEach(item => { content.push(padText(`• ${item}`, WIDTH, 'left')); }); } // Active Conditions Section (if any) const activeConditions = getActiveConditions(character.id); if (activeConditions.length > 0 || effectiveStats.conditionEffects.length > 0) { content.push(''); content.push(box.H.repeat(WIDTH)); content.push(''); content.push(padText('ACTIVE CONDITIONS', WIDTH, 'center')); content.push(''); // List each condition for (const cond of activeConditions) { const condName = typeof cond.condition === 'string' ? cond.condition.charAt(0).toUpperCase() + cond.condition.slice(1) : cond.condition; let condLabel = cond.condition === 'exhaustion' && cond.exhaustionLevel ? `${condName} (Level ${cond.exhaustionLevel})` : condName; content.push(padText(`• ${condLabel}`, WIDTH, 'left')); // Show source if available if (cond.source) { content.push(padText(` Source: ${cond.source}`, WIDTH, 'left')); } // Show duration if (cond.roundsRemaining !== undefined) { content.push(padText(` Duration: ${cond.roundsRemaining} round${cond.roundsRemaining !== 1 ? 's' : ''}`, WIDTH, 'left')); } else if (cond.duration && typeof cond.duration === 'string') { content.push(padText(` Duration: ${cond.duration.replace(/_/g, ' ')}`, WIDTH, 'left')); } content.push(''); } // Show mechanical effects summary if (effectiveStats.conditionEffects.length > 0) { content.push(padText('MECHANICAL EFFECTS:', WIDTH, 'left')); for (const effect of effectiveStats.conditionEffects) { content.push(padText(` ${effect}`, WIDTH, 'left')); } } } // Footer content.push(''); content.push(box.H.repeat(WIDTH)); content.push(centerText(`Character ID: ${character.id}`, WIDTH)); content.push(centerText(`Created: ${new Date(character.createdAt).toLocaleString()}`, WIDTH)); return createBox('CHARACTER SHEET', content, undefined, 'HEAVY'); }