Skip to main content
Glama

roll_dice

Generate dice rolls for tabletop RPGs using standard notation like 2d6+4. Supports single rolls, batch processing, and advantage/disadvantage mechanics for d20 rolls.

Instructions

Roll dice using standard notation (e.g., "2d6+4", "4d6kh3"). Supports single rolls or batch rolling multiple expressions at once. Supports advantage/disadvantage for d20 rolls. Provide either "expression" for single roll or "batch" for multiple rolls.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
expressionNoDice expression for single roll (e.g., "2d6+4", "1d20", "4d6kh3")
batchNoArray of roll requests for batch rolling (alternative to expression)
reasonNoOptional reason for the roll(s)
advantageNoRoll with advantage (2d20, keep highest). Only works with single d20 rolls.
disadvantageNoRoll with disadvantage (2d20, keep lowest). Only works with single d20 rolls.

Implementation Reference

  • Main handler for roll_dice tool. Handles single and batch dice rolls, advantage/disadvantage logic for d20, validation, and delegates rolling to parseDice and formatting to formatDiceResult. Returns formatted markdown output.
    handler: async (args) => { // Check if this is a batch operation const argsObj = args as Record<string, unknown>; if ('batch' in argsObj && argsObj.batch) { const { batch, reason } = args as { batch?: Array<{ expression: string; label?: string; advantage?: boolean; disadvantage?: boolean }>; reason?: string; }; if (!batch || batch.length === 0) { return error('Missing required parameter: batch (must be non-empty array)'); } if (batch.length > 20) { return error('Too many rolls (maximum 20 per batch)'); } try { const results: Array<{ label?: string; expression: string; total: number; rolls: number[]; kept: number[] }> = []; for (const roll of batch) { if (!roll.expression) { return error('Each roll must have an expression'); } if (roll.advantage && roll.disadvantage) { return error(`Roll "${roll.label || roll.expression}": Cannot have both advantage and disadvantage`); } let finalExpression = roll.expression; // Auto-convert d20 rolls to advantage/disadvantage if ((roll.advantage || roll.disadvantage) && roll.expression.match(/^1d20([+-]\d+)?$/i)) { const modifier = roll.expression.match(/([+-]\d+)$/)?.[1] || ''; finalExpression = roll.advantage ? `2d20kh1${modifier}` : `2d20kl1${modifier}`; } else if (roll.advantage || roll.disadvantage) { return error(`Roll "${roll.label || roll.expression}": Advantage/disadvantage only works with single d20 rolls`); } const result = parseDice(finalExpression); results.push({ label: roll.label, expression: finalExpression, total: result.total, rolls: result.rolls, kept: result.kept, }); } // Format batch output const content: string[] = []; if (reason) { content.push(reason.toUpperCase()); content.push(''); } content.push(`ROLLING ${results.length} DICE ${results.length === 1 ? 'EXPRESSION' : 'EXPRESSIONS'}`); content.push(''); content.push('─'.repeat(40)); content.push(''); for (let i = 0; i < results.length; i++) { const r = results[i]; const label = r.label || `Roll ${i + 1}`; content.push(`${label}:`); content.push(` Expression: ${r.expression}`); if (r.rolls.length !== r.kept.length) { content.push(` Rolled: [${r.rolls.join(', ')}]`); content.push(` Kept: [${r.kept.join(', ')}]`); } else if (r.rolls.length > 1) { content.push(` Rolled: [${r.rolls.join(', ')}]`); } content.push(` Result: ${r.total}`); content.push(''); } content.push('─'.repeat(40)); content.push(''); content.push(`TOTAL ACROSS ALL ROLLS: ${results.reduce((sum, r) => sum + r.total, 0)}`); return success(createBox('BATCH ROLL', content, undefined, 'HEAVY')); } catch (err) { const message = err instanceof Error ? err.message : String(err); return error(message); } } // Single roll mode const { expression, reason, advantage, disadvantage } = args as { expression?: string; reason?: string; advantage?: boolean; disadvantage?: boolean; }; if (!expression) { return error('Missing required parameter: expression'); } if (advantage && disadvantage) { return error('Cannot have both advantage and disadvantage'); } try { let finalExpression = expression; // Auto-convert d20 rolls to advantage/disadvantage if ((advantage || disadvantage) && expression.match(/^1d20([+-]\d+)?$/i)) { const modifier = expression.match(/([+-]\d+)$/)?.[1] || ''; finalExpression = advantage ? `2d20kh1${modifier}` : `2d20kl1${modifier}`; } else if (advantage || disadvantage) { return error('Advantage/disadvantage only works with single d20 rolls (e.g., "1d20" or "1d20+5")'); } const result = parseDice(finalExpression); return success(formatDiceResult(result, reason)); } catch (err) { const message = err instanceof Error ? err.message : String(err); return error(message); } },
  • Input schema definition for roll_dice tool, supporting single expression or batch array of rolls with optional labels, advantage/disadvantage, and reason.
    type: 'object', properties: { expression: { type: 'string', description: 'Dice expression for single roll (e.g., "2d6+4", "1d20", "4d6kh3")', }, batch: { type: 'array', description: 'Array of roll requests for batch rolling (alternative to expression)', items: { type: 'object', properties: { expression: { type: 'string', description: 'Dice expression (e.g., "2d6+4", "1d20", "4d6kh3")', }, label: { type: 'string', description: 'Label for this roll (e.g., "Attack 1", "Damage", "Goblin 1")', }, advantage: { type: 'boolean', description: 'Roll with advantage (2d20, keep highest)', }, disadvantage: { type: 'boolean', description: 'Roll with disadvantage (2d20, keep lowest)', }, }, required: ['expression'], }, minItems: 1, maxItems: 20, }, reason: { type: 'string', description: 'Optional reason for the roll(s)', }, advantage: { type: 'boolean', description: 'Roll with advantage (2d20, keep highest). Only works with single d20 rolls.', }, disadvantage: { type: 'boolean', description: 'Roll with disadvantage (2d20, keep lowest). Only works with single d20 rolls.', }, },
  • Registration of the roll_dice tool in the static toolRegistry export.
    roll_dice: { name: 'roll_dice', description: 'Roll dice using standard notation (e.g., "2d6+4", "4d6kh3"). Supports single rolls or batch rolling multiple expressions at once. Supports advantage/disadvantage for d20 rolls. Provide either "expression" for single roll or "batch" for multiple rolls.', inputSchema: { type: 'object', properties: { expression: { type: 'string', description: 'Dice expression for single roll (e.g., "2d6+4", "1d20", "4d6kh3")', }, batch: { type: 'array', description: 'Array of roll requests for batch rolling (alternative to expression)', items: { type: 'object', properties: { expression: { type: 'string', description: 'Dice expression (e.g., "2d6+4", "1d20", "4d6kh3")', }, label: { type: 'string', description: 'Label for this roll (e.g., "Attack 1", "Damage", "Goblin 1")', }, advantage: { type: 'boolean', description: 'Roll with advantage (2d20, keep highest)', }, disadvantage: { type: 'boolean', description: 'Roll with disadvantage (2d20, keep lowest)', }, }, required: ['expression'], }, minItems: 1, maxItems: 20, }, reason: { type: 'string', description: 'Optional reason for the roll(s)', }, advantage: { type: 'boolean', description: 'Roll with advantage (2d20, keep highest). Only works with single d20 rolls.', }, disadvantage: { type: 'boolean', description: 'Roll with disadvantage (2d20, keep lowest). Only works with single d20 rolls.', }, }, }, handler: async (args) => { // Check if this is a batch operation const argsObj = args as Record<string, unknown>; if ('batch' in argsObj && argsObj.batch) { const { batch, reason } = args as { batch?: Array<{ expression: string; label?: string; advantage?: boolean; disadvantage?: boolean }>; reason?: string; }; if (!batch || batch.length === 0) { return error('Missing required parameter: batch (must be non-empty array)'); } if (batch.length > 20) { return error('Too many rolls (maximum 20 per batch)'); } try { const results: Array<{ label?: string; expression: string; total: number; rolls: number[]; kept: number[] }> = []; for (const roll of batch) { if (!roll.expression) { return error('Each roll must have an expression'); } if (roll.advantage && roll.disadvantage) { return error(`Roll "${roll.label || roll.expression}": Cannot have both advantage and disadvantage`); } let finalExpression = roll.expression; // Auto-convert d20 rolls to advantage/disadvantage if ((roll.advantage || roll.disadvantage) && roll.expression.match(/^1d20([+-]\d+)?$/i)) { const modifier = roll.expression.match(/([+-]\d+)$/)?.[1] || ''; finalExpression = roll.advantage ? `2d20kh1${modifier}` : `2d20kl1${modifier}`; } else if (roll.advantage || roll.disadvantage) { return error(`Roll "${roll.label || roll.expression}": Advantage/disadvantage only works with single d20 rolls`); } const result = parseDice(finalExpression); results.push({ label: roll.label, expression: finalExpression, total: result.total, rolls: result.rolls, kept: result.kept, }); } // Format batch output const content: string[] = []; if (reason) { content.push(reason.toUpperCase()); content.push(''); } content.push(`ROLLING ${results.length} DICE ${results.length === 1 ? 'EXPRESSION' : 'EXPRESSIONS'}`); content.push(''); content.push('─'.repeat(40)); content.push(''); for (let i = 0; i < results.length; i++) { const r = results[i]; const label = r.label || `Roll ${i + 1}`; content.push(`${label}:`); content.push(` Expression: ${r.expression}`); if (r.rolls.length !== r.kept.length) { content.push(` Rolled: [${r.rolls.join(', ')}]`); content.push(` Kept: [${r.kept.join(', ')}]`); } else if (r.rolls.length > 1) { content.push(` Rolled: [${r.rolls.join(', ')}]`); } content.push(` Result: ${r.total}`); content.push(''); } content.push('─'.repeat(40)); content.push(''); content.push(`TOTAL ACROSS ALL ROLLS: ${results.reduce((sum, r) => sum + r.total, 0)}`); return success(createBox('BATCH ROLL', content, undefined, 'HEAVY')); } catch (err) { const message = err instanceof Error ? err.message : String(err); return error(message); } } // Single roll mode const { expression, reason, advantage, disadvantage } = args as { expression?: string; reason?: string; advantage?: boolean; disadvantage?: boolean; }; if (!expression) { return error('Missing required parameter: expression'); } if (advantage && disadvantage) { return error('Cannot have both advantage and disadvantage'); } try { let finalExpression = expression; // Auto-convert d20 rolls to advantage/disadvantage if ((advantage || disadvantage) && expression.match(/^1d20([+-]\d+)?$/i)) { const modifier = expression.match(/([+-]\d+)$/)?.[1] || ''; finalExpression = advantage ? `2d20kh1${modifier}` : `2d20kl1${modifier}`; } else if (advantage || disadvantage) { return error('Advantage/disadvantage only works with single d20 rolls (e.g., "1d20" or "1d20+5")'); } const result = parseDice(finalExpression); return success(formatDiceResult(result, reason)); } catch (err) { const message = err instanceof Error ? err.message : String(err); return error(message); } }, },
  • Core helper function that parses dice expression (NdX modifiers), rolls random dice, applies keep/drop highest/lowest logic, adds modifier, and computes total.
    export function parseDice(expression: string): DiceResult { const cleaned = expression.replace(/\s/g, '').toLowerCase(); const match = cleaned.match(DICE_REGEX); if (!match) { throw new Error(`Invalid dice expression: "${expression}"`); } const numDice = parseInt(match[1]); const dieSize = parseInt(match[2]); const keepDrop = match[3] || ''; const modifierStr = match[4] || ''; if (numDice < 1 || numDice > 100) { throw new Error(`Invalid number of dice: ${numDice}`); } if (dieSize < 2 || dieSize > 1000) { throw new Error(`Invalid die size: ${dieSize}`); } // Roll dice const rolls: number[] = []; for (let i = 0; i < numDice; i++) { rolls.push(Math.floor(Math.random() * dieSize) + 1); } // Apply keep/drop modifier let kept = [...rolls]; if (keepDrop) { const type = keepDrop.slice(0, 2); const count = parseInt(keepDrop.slice(2)); if (count > rolls.length) { throw new Error(`Cannot keep/drop ${count} from ${rolls.length} dice`); } const sorted = [...rolls].sort((a, b) => b - a); // Descending switch (type) { case 'kh': // Keep highest kept = sorted.slice(0, count); break; case 'kl': // Keep lowest kept = sorted.slice(-count); break; case 'dh': // Drop highest kept = sorted.slice(count); break; case 'dl': // Drop lowest kept = sorted.slice(0, -count); break; } } // Parse modifier const modifier = modifierStr ? parseInt(modifierStr) : 0; // Calculate total const total = kept.reduce((sum, n) => sum + n, 0) + modifier; return { expression, rolls, kept, modifier, total, }; }
  • Helper function to format DiceResult into a beautiful markdown ASCII art box, with special displays for d20 crits, dice faces, calculations, and totals.
    export function formatDiceResult(result: DiceResult, reason?: string): string { const content: string[] = []; const box = BOX.LIGHT; // Title line content.push(`DICE ROLL: ${result.expression}`); if (reason) { content.push(`(${reason})`); } content.push(''); // For d20 critical hits/fails, show special display if (result.expression.toLowerCase().includes('d20') && result.rolls.length === 1) { const roll = result.rolls[0]; if (roll === 1 || roll === 20) { const special = D20_SPECIAL[roll]; special.forEach(line => content.push(line)); content.push(''); content.push(`TOTAL: ${result.total}`); return createBox('DICE ROLL', content, undefined, 'HEAVY'); } } // Show dice faces (for reasonable number of dice) if (result.rolls.length <= 6 && result.rolls.every(r => r <= 6)) { const diceFaces = renderDiceHorizontal(result.rolls); diceFaces.forEach(line => content.push(line)); content.push(''); // Show which were kept if different if (result.rolls.length !== result.kept.length) { const keptIndices = result.rolls.map((r, i) => result.kept.includes(r) && !result.kept.slice(0, result.kept.indexOf(r)).includes(r) ? i : -1 ).filter(i => i >= 0); content.push(`KEPT: [${result.kept.join(', ')}]`); content.push(`DROPPED: [${result.rolls.filter(r => !result.kept.includes(r)).join(', ')}]`); content.push(''); } } else { // For many dice or large dice, show numbers if (result.rolls.length !== result.kept.length) { content.push(`ROLLED: [${result.rolls.join(', ')}]`); content.push(`KEPT: [${result.kept.join(', ')}]`); } else { content.push(`ROLLED: [${result.rolls.join(', ')}]`); } content.push(''); } // Show calculation const diceSum = result.kept.reduce((sum, n) => sum + n, 0); if (result.modifier !== 0) { const sign = result.modifier > 0 ? '+' : ''; content.push(`CALCULATION: ${diceSum} ${sign}${result.modifier} = ${result.total}`); } else { content.push(`TOTAL: ${result.total}`); } // Add bottom divider content.push(''); content.push('─'.repeat(40)); // Will be adjusted by auto-sizing content.push(`FINAL RESULT: ${result.total}`); return createBox('DICE ROLL', content, undefined, 'HEAVY'); }

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/Mnehmos/ChatRPG'

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