Skip to main content
Glama

Dice Roller MCP Server

by GUEPARD98
server.py24.1 kB
#!/usr/bin/env python3 """ Dice Roller MCP Server A simple and clean MCP server for rolling dice, flipping coins, and other dice mechanics. """ import asyncio import json import random import re import os import logging import sys import traceback from typing import Any, Dict, List, Optional, Tuple from datetime import datetime from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent, ErrorData # Configure logging logging.basicConfig( level=logging.DEBUG, # Changed to DEBUG for more detailed logging format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger("dice-roller") # Configuration MAX_HISTORY = 100 HISTORY_FILE = "dice_history.json" class DiceRoller: def __init__(self, max_history: int = MAX_HISTORY, history_file: str = HISTORY_FILE): self.max_history = max_history self.history_file = history_file self.roll_history: List[Dict[str, Any]] = [] self.load_history() def load_history(self) -> None: """Load roll history from file if it exists""" if os.path.exists(self.history_file): try: with open(self.history_file, 'r') as f: self.roll_history = json.load(f) logger.info(f"Loaded {len(self.roll_history)} history entries") except Exception as e: logger.error(f"Failed to load history: {e}") self.roll_history = [] def save_history(self) -> None: """Save roll history to file""" try: with open(self.history_file, 'w') as f: json.dump(self.roll_history, f) except Exception as e: logger.error(f"Failed to save history: {e}") def add_to_history(self, roll_type: str, result: Any) -> None: """Add a roll to the history and persist it""" entry = { "timestamp": datetime.now().isoformat(), "type": roll_type, "result": result } self.roll_history.append(entry) # Keep only the last MAX_HISTORY entries if len(self.roll_history) > self.max_history: self.roll_history = self.roll_history[-self.max_history:] self.save_history() def parse_dice_notation(self, notation: str) -> Tuple[int, int, int]: """ Parse dice notation like "2d6+3" or "1d20-2" Returns: (num_dice, die_size, modifier) """ if not notation or not isinstance(notation, str): raise ValueError("Dice notation must be a non-empty string") # Remove spaces and convert to lowercase notation = notation.lower().replace(" ", "") # Pattern for dice notation: XdY+Z or XdY-Z pattern = r'^(\d*)d(\d+)([+-]?\d*)?$' match = re.match(pattern, notation) if not match: raise ValueError(f"Invalid dice notation: {notation}") num_dice = int(match.group(1)) if match.group(1) else 1 die_size = int(match.group(2)) modifier = int(match.group(3)) if match.group(3) else 0 # Validate values if num_dice < 1 or num_dice > 100: raise ValueError("Number of dice must be between 1 and 100") if die_size < 2 or die_size > 1000: raise ValueError("Die size must be between 2 and 1000") if abs(modifier) > 1000: raise ValueError("Modifier must be between -1000 and 1000") return num_dice, die_size, modifier def roll_dice(self, num_dice: int, die_size: int, modifier: int = 0) -> Dict[str, Any]: """Roll dice and return detailed results""" rolls = [random.randint(1, die_size) for _ in range(num_dice)] total = sum(rolls) + modifier return { "rolls": rolls, "modifier": modifier, "total": total, "notation": f"{num_dice}d{die_size}{'+' + str(modifier) if modifier > 0 else str(modifier) if modifier < 0 else ''}" } def roll_with_advantage(self, die_size: int = 20, modifier: int = 0) -> Dict[str, Any]: """Roll with advantage (roll twice, take higher)""" roll1 = random.randint(1, die_size) roll2 = random.randint(1, die_size) result = max(roll1, roll2) return { "rolls": [roll1, roll2], "result": result, "modifier": modifier, "total": result + modifier, "type": "advantage", "kept": "highest" } def roll_with_disadvantage(self, die_size: int = 20, modifier: int = 0) -> Dict[str, Any]: """Roll with disadvantage (roll twice, take lower)""" roll1 = random.randint(1, die_size) roll2 = random.randint(1, die_size) result = min(roll1, roll2) return { "rolls": [roll1, roll2], "result": result, "modifier": modifier, "total": result + modifier, "type": "disadvantage", "kept": "lowest" } def roll_exploding_dice(self, num_dice: int, die_size: int, modifier: int = 0) -> Dict[str, Any]: """Roll exploding dice (reroll on max value)""" all_rolls = [] final_rolls = [] for _ in range(num_dice): die_rolls = [] roll = random.randint(1, die_size) die_rolls.append(roll) # Keep rolling while we get max value while roll == die_size: roll = random.randint(1, die_size) die_rolls.append(roll) all_rolls.append(die_rolls) final_rolls.append(sum(die_rolls)) total = sum(final_rolls) + modifier return { "all_rolls": all_rolls, "final_rolls": final_rolls, "modifier": modifier, "total": total, "exploded_count": sum(len(rolls) - 1 for rolls in all_rolls), "notation": f"{num_dice}d{die_size}!{'+' + str(modifier) if modifier > 0 else str(modifier) if modifier < 0 else ''}" } def roll_percentile(self) -> Dict[str, Any]: """Roll a percentile die (d100)""" tens = random.randint(0, 9) * 10 ones = random.randint(0, 9) result = tens + ones return { "tens": tens, "ones": ones, "result": result, "notation": "d100" } def roll_fudge_dice(self, num_dice: int = 4) -> Dict[str, Any]: """Roll Fudge dice (dF)""" results = [random.choice([-1, 0, 1]) for _ in range(num_dice)] total = sum(results) return { "rolls": results, "total": total, "notation": f"{num_dice}dF" } def format_roll_result(self, result: Dict[str, Any]) -> str: """Format a roll result consistently""" rolls_str = ", ".join(str(r) for r in result["rolls"]) response = f"🎲 Rolling {result['notation']}:\n" response += f"Rolls: [{rolls_str}]" if result.get("modifier", 0) != 0: modifier = result["modifier"] response += f" {'+' if modifier > 0 else ''}{modifier}" response += f"\n**Total: {result['total']}**" return response # Initialize the server and dice roller app = Server("dice-roller") dice_roller = DiceRoller() @app.list_tools() async def list_tools() -> List[Tool]: """List all available dice rolling tools""" logger.debug("Listing tools") return [ Tool( name="flip_coin", description="Flip a coin. Returns heads or tails with optional number of flips.", inputSchema={ "type": "object", "properties": { "num_flips": { "type": "integer", "description": "Number of coins to flip (default: 1, max: 100)", "minimum": 1, "maximum": 100, "default": 1 } } } ), Tool( name="roll_dice", description="Roll dice using standard notation (e.g., '2d6+3', '1d20', '3d8-2')", inputSchema={ "type": "object", "properties": { "notation": { "type": "string", "description": "Dice notation (e.g., '2d6+3', '1d20', '3d8-2')", "pattern": r"^\d*d\d+[+-]?\d*$" } }, "required": ["notation"] } ), Tool( name="roll_standard", description="Roll standard gaming dice (d4, d6, d8, d10, d12, d20, d100)", inputSchema={ "type": "object", "properties": { "die_type": { "type": "string", "description": "Type of die to roll", "enum": ["d4", "d6", "d8", "d10", "d12", "d20", "d100"] }, "num_dice": { "type": "integer", "description": "Number of dice to roll (default: 1)", "minimum": 1, "maximum": 100, "default": 1 }, "modifier": { "type": "integer", "description": "Modifier to add to the total (default: 0)", "minimum": -1000, "maximum": 1000, "default": 0 } }, "required": ["die_type"] } ), Tool( name="roll_advantage", description="Roll with advantage (roll twice, keep higher) - D&D 5e mechanic", inputSchema={ "type": "object", "properties": { "die_size": { "type": "integer", "description": "Size of the die (default: 20)", "minimum": 2, "maximum": 1000, "default": 20 }, "modifier": { "type": "integer", "description": "Modifier to add to the result (default: 0)", "minimum": -1000, "maximum": 1000, "default": 0 } } } ), Tool( name="roll_disadvantage", description="Roll with disadvantage (roll twice, keep lower) - D&D 5e mechanic", inputSchema={ "type": "object", "properties": { "die_size": { "type": "integer", "description": "Size of the die (default: 20)", "minimum": 2, "maximum": 1000, "default": 20 }, "modifier": { "type": "integer", "description": "Modifier to add to the result (default: 0)", "minimum": -1000, "maximum": 1000, "default": 0 } } } ), Tool( name="roll_exploding", description="Roll exploding dice (reroll on max value and add to total)", inputSchema={ "type": "object", "properties": { "num_dice": { "type": "integer", "description": "Number of dice to roll", "minimum": 1, "maximum": 100, "default": 1 }, "die_size": { "type": "integer", "description": "Size of the die", "minimum": 2, "maximum": 1000 }, "modifier": { "type": "integer", "description": "Modifier to add to the total (default: 0)", "minimum": -1000, "maximum": 1000, "default": 0 } }, "required": ["die_size"] } ), Tool( name="roll_percentile", description="Roll a percentile die (d100)", inputSchema={ "type": "object", "properties": {} } ), Tool( name="roll_fudge", description="Roll Fudge dice (dF) - returns -1, 0, or 1 for each die", inputSchema={ "type": "object", "properties": { "num_dice": { "type": "integer", "description": "Number of Fudge dice to roll (default: 4)", "minimum": 1, "maximum": 100, "default": 4 } } } ), Tool( name="get_history", description="Get the history of recent dice rolls", inputSchema={ "type": "object", "properties": { "limit": { "type": "integer", "description": "Number of recent rolls to retrieve (default: 10)", "minimum": 1, "maximum": 100, "default": 10 } } } ), Tool( name="clear_history", description="Clear the dice roll history", inputSchema={ "type": "object", "properties": {} } ) ] @app.call_tool() async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]: """Handle tool calls""" logger.debug(f"Calling tool: {name} with arguments: {arguments}") try: if name == "flip_coin": num_flips = arguments.get("num_flips", 1) if num_flips == 1: result = "Heads" if random.random() < 0.5 else "Tails" dice_roller.add_to_history("coin_flip", result) return [TextContent(type="text", text=f"🪙 Coin flip: **{result}**")] else: results = ["Heads" if random.random() < 0.5 else "Tails" for _ in range(num_flips)] heads_count = results.count("Heads") tails_count = results.count("Tails") dice_roller.add_to_history("coin_flip_multiple", {"results": results, "count": num_flips}) return [TextContent( type="text", text=f"🪙 Flipped {num_flips} coins:\n" f"Results: {', '.join(results)}\n" f"Summary: {heads_count} Heads, {tails_count} Tails" )] elif name == "roll_dice": notation = arguments["notation"] num_dice, die_size, modifier = dice_roller.parse_dice_notation(notation) result = dice_roller.roll_dice(num_dice, die_size, modifier) dice_roller.add_to_history("dice_roll", result) rolls_str = ", ".join(str(r) for r in result["rolls"]) response = f"🎲 Rolling {result['notation']}:\n" response += f"Rolls: [{rolls_str}]" if modifier != 0: response += f" {'+' if modifier > 0 else ''}{modifier}" response += f"\n**Total: {result['total']}**" return [TextContent(type="text", text=response)] elif name == "roll_standard": die_type = arguments["die_type"] num_dice = arguments.get("num_dice", 1) modifier = arguments.get("modifier", 0) die_size = int(die_type[1:]) # Remove 'd' prefix result = dice_roller.roll_dice(num_dice, die_size, modifier) dice_roller.add_to_history("standard_roll", result) rolls_str = ", ".join(str(r) for r in result["rolls"]) response = f"🎲 Rolling {result['notation']}:\n" response += f"Rolls: [{rolls_str}]" if modifier != 0: response += f" {'+' if modifier > 0 else ''}{modifier}" response += f"\n**Total: {result['total']}**" return [TextContent(type="text", text=response)] elif name == "roll_advantage": die_size = arguments.get("die_size", 20) modifier = arguments.get("modifier", 0) result = dice_roller.roll_with_advantage(die_size, modifier) dice_roller.add_to_history("advantage_roll", result) response = f"🎯 Rolling d{die_size} with ADVANTAGE:\n" response += f"Rolls: {result['rolls'][0]} and {result['rolls'][1]}\n" response += f"Keeping highest: {result['result']}" if modifier != 0: response += f" {'+' if modifier > 0 else ''}{modifier}" response += f"\n**Total: {result['total']}**" return [TextContent(type="text", text=response)] elif name == "roll_disadvantage": die_size = arguments.get("die_size", 20) modifier = arguments.get("modifier", 0) result = dice_roller.roll_with_disadvantage(die_size, modifier) dice_roller.add_to_history("disadvantage_roll", result) response = f"🎯 Rolling d{die_size} with DISADVANTAGE:\n" response += f"Rolls: {result['rolls'][0]} and {result['rolls'][1]}\n" response += f"Keeping lowest: {result['result']}" if modifier != 0: response += f" {'+' if modifier > 0 else ''}{modifier}" response += f"\n**Total: {result['total']}**" return [TextContent(type="text", text=response)] elif name == "roll_exploding": num_dice = arguments.get("num_dice", 1) die_size = arguments["die_size"] modifier = arguments.get("modifier", 0) result = dice_roller.roll_exploding_dice(num_dice, die_size, modifier) dice_roller.add_to_history("exploding_roll", result) response = f"💥 Rolling {result['notation']} (Exploding!):\n" for i, rolls in enumerate(result['all_rolls'], 1): if len(rolls) > 1: response += f"Die {i}: {' → '.join(str(r) for r in rolls)} (exploded!)\n" else: response += f"Die {i}: {rolls[0]}\n" response += f"Die totals: [{', '.join(str(r) for r in result['final_rolls'])}]" if modifier != 0: response += f" {'+' if modifier > 0 else ''}{modifier}" response += f"\n**Total: {result['total']}**" if result['exploded_count'] > 0: response += f"\n🎆 Dice exploded {result['exploded_count']} time(s)!" return [TextContent(type="text", text=response)] elif name == "roll_percentile": result = dice_roller.roll_percentile() dice_roller.add_to_history("percentile_roll", result) response = f"🎲 Rolling percentile die (d100):\n" response += f"Tens: {result['tens']}, Ones: {result['ones']}\n" response += f"**Result: {result['result']}**" return [TextContent(type="text", text=response)] elif name == "roll_fudge": num_dice = arguments.get("num_dice", 4) result = dice_roller.roll_fudge_dice(num_dice) dice_roller.add_to_history("fudge_roll", result) rolls_str = ", ".join(["+" if r == 1 else "-" if r == -1 else "0" for r in result["rolls"]]) response = f"🎲 Rolling {result['notation']}:\n" response += f"Rolls: [{rolls_str}]\n" response += f"**Total: {result['total']}**" return [TextContent(type="text", text=response)] elif name == "get_history": limit = arguments.get("limit", 10) if not dice_roller.roll_history: return [TextContent(type="text", text="📜 No roll history available.")] recent_rolls = dice_roller.roll_history[-limit:] response = f"📜 Last {min(limit, len(recent_rolls))} rolls:\n\n" for i, entry in enumerate(reversed(recent_rolls), 1): timestamp = entry['timestamp'].split('T')[1].split('.')[0] # Get time only roll_type = entry['type'].replace('_', ' ').title() result = entry['result'] if isinstance(result, dict): if 'total' in result: response += f"{i}. [{timestamp}] {roll_type}: **{result['total']}**\n" elif 'results' in result: response += f"{i}. [{timestamp}] {roll_type}: {result['results']}\n" else: response += f"{i}. [{timestamp}] {roll_type}: {result}\n" return [TextContent(type="text", text=response)] elif name == "clear_history": dice_roller.roll_history = [] dice_roller.save_history() dice_roller.add_to_history("history_cleared", "History was cleared") return [TextContent(type="text", text="🗑️ Roll history has been cleared.")] else: raise ValueError(f"Unknown tool: {name}") except ValueError as e: error_msg = f"❌ Validation Error: {str(e)}" logger.error(f"Validation error in tool {name}: {e}") return [TextContent(type="text", text=error_msg)] except KeyError as e: error_msg = f"❌ Missing Parameter: {str(e)}" logger.error(f"Missing parameter in tool {name}: {e}") return [TextContent(type="text", text=error_msg)] except Exception as e: error_msg = f"❌ Unexpected Error: {str(e)}" logger.error(f"Unexpected error in tool {name}: {e}") return [TextContent(type="text", text=error_msg)] async def main(): """Main entry point for the server""" logger.info("Starting Dice Roller MCP Server") try: # Run the server using stdio transport async with stdio_server() as (read_stream, write_stream): logger.debug("Server streams created") server_task = asyncio.create_task(app.run( read_stream, write_stream, app.create_initialization_options() )) logger.debug("Server task created") # Keep the main coroutine alive while True: await asyncio.sleep(3600) except Exception as e: logger.error(f"Fatal error: {e}") logger.error(traceback.format_exc()) raise if __name__ == "__main__": try: asyncio.run(main()) except KeyboardInterrupt: logger.info("Received keyboard interrupt") sys.exit(0) except Exception as e: logger.error(f"Fatal error in main: {e}") logger.error(traceback.format_exc()) sys.exit(1)

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/GUEPARD98/Servidor_MCP'

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