Skip to main content
Glama

D&D MCP Server

main.pyโ€ข34.2 kB
""" D&D MCP Server A comprehensive D&D campaign management server built with modern FastMCP framework. """ import logging import random import re import os from pathlib import Path from typing import Annotated, Literal from dotenv import load_dotenv from fastmcp import FastMCP from pydantic import Field from .storage import DnDStorage from .models import ( Character, NPC, Location, Quest, SessionNote, AdventureEvent, EventType, AbilityScore, CharacterClass, Race, Item ) logger = logging.getLogger("gamemaster-mcp") logging.basicConfig( level=logging.DEBUG, ) if not load_dotenv(): logger.warning("โŒ .env file invalid or not found! Please see README.md for instructions. Using project root instead.") data_path = Path(os.getenv("GAMEMASTER_STORAGE_DIR", "")).resolve() logger.debug(f"๐Ÿ“‚ Data path: {data_path}") # Initialize storage and FastMCP server storage = DnDStorage(data_dir=data_path) logger.debug("โœ… Storage layer initialized") mcp = FastMCP( name="gamemaster-mcp" ) logger.debug("โœ… Server initialized, registering tools") # ---------------------------------------------------------------------- # Tools # ---------------------------------------------------------------------- # Campaign management tools @mcp.tool def create_campaign( name: Annotated[str, Field(description="Campaign name")], description: Annotated[str, Field(description="Brief decription of the campaign, or a tagline")], dm_name: Annotated[str | None, Field(description="Dungeon Master name")] = None, setting: Annotated[str | Path | None, Field(description=""" Campaign setting - a full description of the setting of the campaign in markdown format, or the path to a `.txt` or `.md` file containing the same. """)] = None, ) -> str: """Create a new D&D campaign.""" campaign = storage.create_campaign( name=name, description=description, dm_name=dm_name, setting=setting ) return f"๐ŸŒŸ Created campaign: '{campaign.name} and set as active ๐ŸŒŸ'" @mcp.tool def get_campaign_info() -> str: """Get information about the current campaign.""" campaign = storage.get_current_campaign() if not campaign: return "No active campaign." info = { "name": campaign.name, "description": campaign.description, "dm_name": campaign.dm_name, "setting": campaign.get_setting(), "character_count": len(campaign.characters), "npc_count": len(campaign.npcs), "location_count": len(campaign.locations), "quest_count": len(campaign.quests), "session_count": len(campaign.sessions), "current_session": campaign.game_state.current_session, "current_location": campaign.game_state.current_location, "party_level": campaign.game_state.party_level, "in_combat": campaign.game_state.in_combat } return f"**Campaign: {campaign.name}**\n\n" + \ "\n".join([f"**{k.replace('_', ' ').title()}:** {v}" for k, v in info.items()]) @mcp.tool def list_campaigns() -> str: """List all available campaigns.""" campaigns = storage.list_campaigns() if not campaigns: return f"โŒ No campaigns found in {storage.data_dir}!" current = storage.get_current_campaign() current_name = current.name if current else None campaign_list = [] for campaign in campaigns: marker = " (current)" if campaign == current_name else "" campaign_list.append(f"โ€ข {campaign}{marker}") return "**Available Campaigns:**\n" + "\n".join(campaign_list) @mcp.tool def load_campaign( name: Annotated[str, Field(description="Campaign name to load")] ) -> str: """Load a specific campaign.""" campaign = storage.load_campaign(name) return f"๐Ÿ“– Loaded campaign: '{campaign.name}'. Campaign is now active!" # Character Management Tools @mcp.tool def create_character( name: Annotated[str, Field(description="Character name")], character_class: Annotated[str, Field(description="Character class")], class_level: Annotated[int, Field(description="Class level", ge=1, le=20)], race: Annotated[str, Field(description="Character race")], player_name: Annotated[str | None, Field(description="The name of the player in control of this character")] = None, description: Annotated[str | None, Field(description="A brief description of the character's appearance and demeanor.")] = None, bio: Annotated[str | None, Field(description="The character's backstory, personality, and motivations.")] = None, background: Annotated[str | None, Field(description="Character background")] = None, alignment: Annotated[str | None, Field(description="Character alignment")] = None, strength: Annotated[int, Field(description="Strength score", ge=1, le=30)] = 10, dexterity: Annotated[int, Field(description="Dexterity score", ge=1, le=30)] = 10, constitution: Annotated[int, Field(description="Constitution score", ge=1, le=30)] = 10, intelligence: Annotated[int, Field(description="Intelligence score", ge=1, le=30)] = 10, wisdom: Annotated[int, Field(description="Wisdom score", ge=1, le=30)] = 10, charisma: Annotated[int, Field(description="Charisma score", ge=1, le=30)] = 10, ) -> str: """Create a new player character.""" # Build ability scores abilities = { "strength": AbilityScore(score=strength), "dexterity": AbilityScore(score=dexterity), "constitution": AbilityScore(score=constitution), "intelligence": AbilityScore(score=intelligence), "wisdom": AbilityScore(score=wisdom), "charisma": AbilityScore(score=charisma), } character = Character( name=name, player_name=player_name, character_class=CharacterClass(name=character_class, level=class_level), race=Race(name=race), background=background, alignment=alignment, abilities=abilities, description=description, bio=bio, ) storage.add_character(character) return f"Created character '{character.name}' (Level {character.character_class.level} {character.race.name} {character.character_class.name})" @mcp.tool def get_character( name_or_id: Annotated[str, Field(description="Character name or ID")] ) -> str: """Get detailed character information.""" character = storage.get_character(name_or_id) if not character: return f"โŒ Character '{name_or_id}' not found." char_info = f"""**{character.name}** (`{character.id}`) Level {character.character_class.level} {character.race.name} {character.character_class.name} **Player:** {character.player_name or 'N/A'} **Background:** {character.background or 'N/A'} **Alignment:** {character.alignment or 'N/A'} **Description:** {character.description or 'No description provided.'} **Bio:** {character.bio or 'No bio provided.'} **Ability Scores:** โ€ข STR: {character.abilities['strength'].score} ({character.abilities['strength'].mod:+d}) โ€ข DEX: {character.abilities['dexterity'].score} ({character.abilities['dexterity'].mod:+d}) โ€ข CON: {character.abilities['constitution'].score} ({character.abilities['constitution'].mod:+d}) โ€ข INT: {character.abilities['intelligence'].score} ({character.abilities['intelligence'].mod:+d}) โ€ข WIS: {character.abilities['wisdom'].score} ({character.abilities['wisdom'].mod:+d}) โ€ข CHA: {character.abilities['charisma'].score} ({character.abilities['charisma'].mod:+d}) **Combat Stats:** โ€ข AC: {character.armor_class} โ€ข HP: {character.hit_points_current}/{character.hit_points_max} โ€ข Temp HP: {character.temporary_hit_points} **Inventory:** {len(character.inventory)} items """ return char_info @mcp.tool def update_character( name_or_id: Annotated[str, Field(description="The name or ID of the character to update.")], name: Annotated[str | None, Field(description="New character name. If you change this, you must use the character's ID to identify them.")] = None, player_name: Annotated[str | None, Field(description="The name of the player in control of this character")] = None, description: Annotated[str | None, Field(description="A brief description of the character's appearance and demeanor.")] = None, bio: Annotated[str | None, Field(description="The character's backstory, personality, and motivations.")] = None, background: Annotated[str | None, Field(description="Character background")] = None, alignment: Annotated[str | None, Field(description="Character alignment")] = None, hit_points_current: Annotated[int | None, Field(description="Current hit points", ge=0)] = None, hit_points_max: Annotated[int | None, Field(description="Maximum hit points", ge=1)] = None, temporary_hit_points: Annotated[int | None, Field(description="Temporary hit points", ge=0)] = None, armor_class: Annotated[int | None, Field(description="Armor class")] = None, inspiration: Annotated[bool | None, Field(description="Inspiration status")] = None, notes: Annotated[str | None, Field(description="Additional notes about the character")] = None, strength: Annotated[int | None, Field(description="Strength score", ge=1, le=30)] = None, dexterity: Annotated[int | None, Field(description="Dexterity score", ge=1, le=30)] = None, constitution: Annotated[int | None, Field(description="Constitution score", ge=1, le=30)] = None, intelligence: Annotated[int | None, Field(description="Intelligence score", ge=1, le=30)] = None, wisdom: Annotated[int | None, Field(description="Wisdom score", ge=1, le=30)] = None, charisma: Annotated[int | None, Field(description="Charisma score", ge=1, le=30)] = None, ) -> str: """Update a character's properties.""" character = storage.get_character(name_or_id) if not character: return f"โŒ Character '{name_or_id}' not found." updates = {k: v for k, v in locals().items() if v is not None and k not in ["name_or_id", "character"]} updated_fields = [f"{key.replace('_', ' ')}: {value}" for key, value in updates.items()] if not updates: return f"No updates provided for {character.name}." storage.update_character(str(character.id), **updates) return f"Updated {character.name}'s properties: {'; '.join(updated_fields)}." @mcp.tool def bulk_update_characters( names_or_ids: Annotated[list[str], Field(description="List of character names or IDs to update.")], hp_change: Annotated[int | None, Field(description="Amount to change current HP by (positive or negative).")] = None, temp_hp_change: Annotated[int | None, Field(description="Amount to change temporary HP by (positive or negative).")] = None, strength_change: Annotated[int | None, Field(description="Amount to change strength by.")] = None, dexterity_change: Annotated[int | None, Field(description="Amount to change dexterity by.")] = None, constitution_change: Annotated[int | None, Field(description="Amount to change constitution by.")] = None, intelligence_change: Annotated[int | None, Field(description="Amount to change intelligence by.")] = None, wisdom_change: Annotated[int | None, Field(description="Amount to change wisdom by.")] = None, charisma_change: Annotated[int | None, Field(description="Amount to change charisma by.")] = None, ) -> str: """Update properties for multiple characters at once by a given amount.""" updates_log = [] not_found_log = [] changes = { "hp_change": hp_change, "temp_hp_change": temp_hp_change, "strength_change": strength_change, "dexterity_change": dexterity_change, "constitution_change": constitution_change, "intelligence_change": intelligence_change, "wisdom_change": wisdom_change, "charisma_change": charisma_change, } # Filter out None changes active_changes = {k: v for k, v in changes.items() if v is not None} if not active_changes: return "No changes specified." for name_or_id in names_or_ids: character = storage.get_character(name_or_id) if not character: not_found_log.append(name_or_id) continue char_updates = {} char_log = [f"{character.name}:"] if hp_change is not None: new_hp = character.hit_points_current + hp_change # Clamp HP between 0 and max HP new_hp = max(0, min(new_hp, character.hit_points_max)) char_updates['hit_points_current'] = new_hp char_log.append(f"HP -> {new_hp}") if temp_hp_change is not None: new_temp_hp = character.temporary_hit_points + temp_hp_change # Temp HP cannot be negative new_temp_hp = max(0, new_temp_hp) char_updates['temporary_hit_points'] = new_temp_hp char_log.append(f"Temp HP -> {new_temp_hp}") abilities_updated = False ability_changes = { "strength": strength_change, "dexterity": dexterity_change, "constitution": constitution_change, "intelligence": intelligence_change, "wisdom": wisdom_change, "charisma": charisma_change } for ability, change in ability_changes.items(): if change is not None: new_score = character.abilities[ability].score + change new_score = max(1, min(new_score, 30)) # Clamp score character.abilities[ability].score = new_score abilities_updated = True char_log.append(f"{ability.capitalize()} -> {new_score}") if abilities_updated: char_updates['abilities'] = character.abilities if char_updates: storage.update_character(str(character.id), **char_updates) updates_log.append(" ".join(char_log)) response_parts = [] if updates_log: response_parts.append("Characters updated:\n" + "\n".join(updates_log)) if not_found_log: response_parts.append(f"Characters not found: {', '.join(not_found_log)}") return "\n".join(response_parts) if response_parts else "No characters found to update." @mcp.tool def add_item_to_character( character_name_or_id: Annotated[str, Field(description="Name or ID of the character to receive the item.")], item_name: Annotated[str, Field(description="Item name")], description: Annotated[str | None, Field(description="Item description")] = None, quantity: Annotated[int, Field(description="Quantity", ge=1)] = 1, item_type: Annotated[Literal["weapon", "armor", "consumable", "misc"], Field(description="Item type")] = "misc", weight: Annotated[float | None, Field(description="Item weight", ge=0)] = None, value: Annotated[str | None, Field(description="Item value (e.g., '50 gp')")] = None, ) -> str: """Add an item to a character's inventory.""" character = storage.get_character(character_name_or_id) if not character: return f"โŒ Character '{character_name_or_id}' not found!" item = Item( name=item_name, description=description, quantity=quantity, item_type=item_type, weight=weight, value=value ) character.inventory.append(item) storage.update_character(str(character.id), inventory=character.inventory) return f"Added {item.quantity}x {item.name} to {character.name}'s inventory" @mcp.tool def list_characters() -> str: """List all characters in the current campaign.""" characters = storage.list_characters() if not characters: return "No characters in the current campaign." char_list = [] for char_name in characters: char = storage.get_character(char_name) if char: char_list.append(f"โ€ข {char.name} (Level {char.character_class.level} {char.race.name} {char.character_class.name})") return "**Characters:**\n" + "\n".join(char_list) # NPC Management Tools @mcp.tool def create_npc( name: Annotated[str, Field(description="NPC name")], description: Annotated[str | None, Field(description="A brief, public description of the NPC.")] = None, bio: Annotated[str | None, Field(description="A detailed, private bio for the NPC, including secrets.")] = None, race: Annotated[str | None, Field(description="NPC race")] = None, occupation: Annotated[str | None, Field(description="NPC occupation")] = None, location: Annotated[str | None, Field(description="Current location")] = None, attitude: Annotated[Literal["friendly", "neutral", "hostile", "unknown"] | None, Field(description="Attitude towards party")] = None, notes: Annotated[str, Field(description="Additional notes")] = "", ) -> str: """Create a new NPC.""" npc = NPC( name=name, description=description, bio=bio, race=race, occupation=occupation, location=location, attitude=attitude, notes=notes ) storage.add_npc(npc) return f"Created NPC '{npc.name}'" @mcp.tool def get_npc( name: Annotated[str, Field(description="NPC name")] ) -> str: """Get NPC information.""" npc = storage.get_npc(name) if not npc: return f"NPC '{name}' not found." npc_info = f"""**{npc.name}** (`{npc.id}`) **Race:** {npc.race or 'Unknown'} **Occupation:** {npc.occupation or 'Unknown'} **Location:** {npc.location or 'Unknown'} **Attitude:** {npc.attitude or 'Neutral'} **Description:** {npc.description or 'No description available.'} **Bio:** {npc.bio or 'No bio available.'} **Notes:** {npc.notes or 'No additional notes.'} """ return npc_info @mcp.tool def list_npcs() -> str: """List all NPCs in the current campaign.""" npcs = storage.list_npcs() if not npcs: return "No NPCs in the current campaign." npc_list = [] for npc_name in npcs: npc = storage.get_npc(npc_name) if npc: location = f" ({npc.location})" if npc.location else "" npc_list.append(f"โ€ข {npc.name}{location}") return "**NPCs:**\n" + "\n".join(npc_list) # Location Management Tools @mcp.tool def create_location( name: Annotated[str, Field(description="Location name")], location_type: Annotated[str, Field(description="Type of location (city, town, village, dungeon, etc.)")], description: Annotated[str, Field(description="Location description")], population: Annotated[int | None, Field(description="Population (if applicable)", ge=0)] = None, government: Annotated[str | None, Field(description="Government type")] = None, notable_features: Annotated[list[str] | None, Field(description="Notable features")] = None, notes: Annotated[str, Field(description="Additional notes")] = "", ) -> str: """Create a new location.""" location = Location( name=name, location_type=location_type, description=description, population=population, government=government, notable_features=notable_features or [], notes=notes ) storage.add_location(location) return f"Created location '{location.name}' ({location.location_type})" @mcp.tool def get_location( name: Annotated[str, Field(description="Location name")] ) -> str: """Get location information.""" location = storage.get_location(name) if not location: return f"Location '{name}' not found." loc_info = f"""**{location.name}** ({location.location_type}) **Description:** {location.description} **Population:** {location.population or 'Unknown'} **Government:** {location.government or 'Unknown'} **Notable Features:** {chr(10).join(['โ€ข ' + feature for feature in location.notable_features]) if location.notable_features else 'None listed'} **Notes:** {location.notes or 'No additional notes.'} """ return loc_info @mcp.tool def list_locations() -> str: """List all locations in the current campaign.""" locations = storage.list_locations() if not locations: return "No locations in the current campaign." loc_list = [] for loc_name in locations: loc = storage.get_location(loc_name) if loc: loc_list.append(f"โ€ข {loc.name} ({loc.location_type})") return "**Locations:**\n" + "\n".join(loc_list) # Quest Management Tools @mcp.tool def create_quest( title: Annotated[str, Field(description="Quest title")], description: Annotated[str, Field(description="Quest description")], giver: Annotated[str | None, Field(description="Quest giver (NPC name)")] = None, objectives: Annotated[list[str] | None, Field(description="Quest objectives")] = None, reward: Annotated[str | None, Field(description="Quest reward")] = None, notes: Annotated[str, Field(description="Additional notes")] = "", ) -> str: """Create a new quest.""" quest = Quest( title=title, description=description, giver=giver, objectives=objectives or [], reward=reward, notes=notes ) storage.add_quest(quest) return f"Created quest '{quest.title}'" @mcp.tool def update_quest( title: Annotated[str, Field(description="Quest title")], status: Annotated[Literal["active", "completed", "failed", "on_hold"] | None, Field(description="New quest status")] = None, completed_objective: Annotated[str | None, Field(description="Objective to mark as completed")] = None, ) -> str: """Update quest status or complete objectives.""" quest = storage.get_quest(title) if not quest: return f"Quest '{title}' not found." if status: storage.update_quest_status(title, status) if completed_objective: if completed_objective in quest.objectives and completed_objective not in quest.completed_objectives: quest.completed_objectives.append(completed_objective) storage._save_campaign() # Direct save since we modified the object return f"Updated quest '{title}'" @mcp.tool def list_quests( status: Annotated[Literal["active", "completed", "failed", "on_hold"] | None, Field(description="Filter by status")] = None, ) -> str: """List quests, optionally filtered by status.""" quests = storage.list_quests(status) if not quests: filter_text = f" with status '{status}'" if status else "" return f"No quests found{filter_text}." quest_list = [] for quest_title in quests: quest = storage.get_quest(quest_title) if quest: status_text = f" [{quest.status}]" quest_list.append(f"โ€ข {quest.title}{status_text}") return "**Quests:**\n" + "\n".join(quest_list) # Game State Management Tools @mcp.tool def update_game_state( current_location: Annotated[str | None, Field(description="Current party location")] = None, current_session: Annotated[int | None, Field(description="Current session number", ge=1)] = None, current_date_in_game: Annotated[str | None, Field(description="Current in-game date")] = None, party_level: Annotated[int | None, Field(description="Average party level", ge=1, le=20)] = None, party_funds: Annotated[str | None, Field(description="Party treasure/funds")] = None, in_combat: Annotated[bool | None, Field(description="Whether party is in combat")] = None, notes: Annotated[str | None, Field(description="Current situation notes")] = None, ) -> str: """Update the current game state.""" kwargs = {} if current_location is not None: kwargs["current_location"] = current_location if current_session is not None: kwargs["current_session"] = current_session if current_date_in_game is not None: kwargs["current_date_in_game"] = current_date_in_game if party_level is not None: kwargs["party_level"] = party_level if party_funds is not None: kwargs["party_funds"] = party_funds if in_combat is not None: kwargs["in_combat"] = in_combat if notes is not None: kwargs["notes"] = notes storage.update_game_state(**kwargs) return "Updated game state" @mcp.tool def get_game_state() -> str: """Get the current game state.""" game_state = storage.get_game_state() if not game_state: return "No game state available." state_info = f"""**Game State** **Campaign:** {game_state.campaign_name} **Session:** {game_state.current_session} **Location:** {game_state.current_location or 'Unknown'} **Date (In-Game):** {game_state.current_date_in_game or 'Unknown'} **Party Level:** {game_state.party_level} **Party Funds:** {game_state.party_funds} **In Combat:** {'Yes' if game_state.in_combat else 'No'} **Active Quests:** {len(game_state.active_quests)} **Notes:** {game_state.notes or 'No current notes.'} """ return state_info # Combat Management Tools @mcp.tool def start_combat( participants: Annotated[list[dict], Field(description="Combat participants with initiative order")] ) -> str: """Start a combat encounter.""" # Sort by initiative (highest first) initiative_order = sorted(participants, key=lambda x: x.get("initiative", 0), reverse=True) storage.update_game_state( in_combat=True, initiative_order=initiative_order, current_turn=initiative_order[0]["name"] if initiative_order else None ) order_text = "\n".join([ f"{i+1}. {p['name']} (Initiative: {p.get('initiative', 0)})" for i, p in enumerate(initiative_order) ]) return f"**Combat Started!**\n\n**Initiative Order:**\n{order_text}\n\n**Current Turn:** {initiative_order[0]['name'] if initiative_order else 'None'}" @mcp.tool def end_combat() -> str: """End the current combat encounter.""" storage.update_game_state( in_combat=False, initiative_order=[], current_turn=None ) return "Combat ended." @mcp.tool def next_turn() -> str: """Advance to the next turn in combat.""" game_state = storage.get_game_state() if not game_state or not game_state.in_combat: return "Not currently in combat." if not game_state.initiative_order: return "No initiative order set." # Find current turn index and advance current_index = 0 if game_state.current_turn: for i, participant in enumerate(game_state.initiative_order): if participant["name"] == game_state.current_turn: current_index = i break next_index = (current_index + 1) % len(game_state.initiative_order) next_participant = game_state.initiative_order[next_index] storage.update_game_state(current_turn=next_participant["name"]) return f"**Next Turn:** {next_participant['name']}" # Session Management Tools @mcp.tool def add_session_note( session_number: Annotated[int, Field(description="Session number", ge=1)], summary: Annotated[str, Field(description="Session summary")], title: Annotated[str | None, Field(description="Session title")] = None, events: Annotated[list[str] | None, Field(description="Key events that occurred")] = None, characters_present: Annotated[list[str] | None, Field(description="Characters present in session")] = None, experience_gained: Annotated[int | None, Field(description="Experience points gained", ge=0)] = None, treasure_found: Annotated[list[str] | None, Field(description="Treasure or items found")] = None, notes: Annotated[str, Field(description="Additional notes")] = "", ) -> str: """Add notes for a game session.""" session_note = SessionNote( session_number=session_number, title=title, summary=summary, events=events or [], characters_present=characters_present or [], experience_gained=experience_gained, treasure_found=treasure_found or [], notes=notes ) storage.add_session_note(session_note) return f"Added session note for Session {session_note.session_number}" @mcp.tool def get_sessions() -> str: """Get all session notes.""" sessions = storage.get_sessions() if not sessions: return "No session notes recorded." session_list = [] for session in sorted(sessions, key=lambda s: s.session_number): title = session.title or "No title" date = session.date.strftime("%Y-%m-%d") session_list.append(f"**Session {session.session_number}** ({date}): {title}") session_list.append(f" {session.summary[:100]}{'...' if len(session.summary) > 100 else ''}") session_list.append("") return "**Session Notes:**\n\n" + "\n".join(session_list) # Adventure Log Tools @mcp.tool def add_event( event_type: Annotated[Literal["combat", "roleplay", "exploration", "quest", "character", "world", "session"], Field(description="Type of event")], title: Annotated[str, Field(description="Event title")], description: Annotated[str, Field(description="Event description")], session_number: Annotated[int | None, Field(description="Session number", ge=1)] = None, characters_involved: Annotated[list[str] | None, Field(description="Characters involved in the event")] = None, location: Annotated[str | None, Field(description="Location where event occurred")] = None, importance: Annotated[int, Field(description="Event importance (1-5)", ge=1, le=5)] = 3, tags: Annotated[list[str] | None, Field(description="Tags for categorizing the event")] = None, ) -> str: """Add an event to the adventure log.""" event = AdventureEvent( event_type=EventType(event_type), title=title, description=description, session_number=session_number, characters_involved=characters_involved or [], location=location, importance=importance, tags=tags or [] ) storage.add_event(event) return f"Added {event_type.lower} event: '{event.title}'" @mcp.tool def get_events( limit: Annotated[int | None, Field(description="Maximum number of events to return", ge=1)] = None, event_type: Annotated[Literal["combat", "roleplay", "exploration", "quest", "character", "world", "session"] | None, Field(description="Filter by event type")] = None, search: Annotated[str | None, Field(description="Search events by title/description")] = None, ) -> str: """Get events from the adventure log.""" if search: events = storage.search_events(search) else: events = storage.get_events(limit=limit, event_type=event_type) if not events: return "No events found." event_list = [] for event in events: timestamp = event.timestamp.strftime("%Y-%m-%d %H:%M") session_text = f" (Session {event.session_number})" if event.session_number else "" importance_stars = "โ˜…" * event.importance event_list.append(f"**{event.title}** [{event.event_type}] {importance_stars}") event_list.append(f" {timestamp}{session_text}") event_list.append(f" {event.description[:150]}{'...' if len(event.description) > 150 else ''}") if event.location: event_list.append(f" ๐Ÿ“ {event.location}") event_list.append("") return "**Adventure Log:**\n\n" + "\n".join(event_list) # Utility Tools @mcp.tool def roll_dice( dice_notation: Annotated[str, Field(description="Dice notation (e.g., '1d20', '3d6+2')")], advantage: Annotated[bool, Field(description="Roll with advantage")] = False, disadvantage: Annotated[bool, Field(description="Roll with disadvantage")] = False, ) -> str: """Roll dice with D&D notation.""" dice_notation = dice_notation.lower().strip() # Parse dice notation (e.g., "1d20", "3d6+2", "2d8-1") pattern = r'(\d+)d(\d+)([+-]\d+)?' match = re.match(pattern, dice_notation) if not match: return f"Invalid dice notation: {dice_notation}" num_dice = int(match.group(1)) die_size = int(match.group(2)) modifier = int(match.group(3)) if match.group(3) else 0 # Roll dice if advantage or disadvantage: if num_dice != 1 or die_size != 20: return "Advantage/disadvantage only applies to single d20 rolls" roll1 = random.randint(1, 20) roll2 = random.randint(1, 20) if advantage: result = max(roll1, roll2) roll_text = f"Advantage: {roll1}, {roll2} (taking {result})" else: result = min(roll1, roll2) roll_text = f"Disadvantage: {roll1}, {roll2} (taking {result})" total = result + modifier modifier_text = f" {modifier:+d}" if modifier != 0 else "" return f"๐ŸŽฒ **{dice_notation}** {roll_text}{modifier_text} = **{total}**" else: rolls = [random.randint(1, die_size) for _ in range(num_dice)] roll_sum = sum(rolls) total = roll_sum + modifier rolls_text = ", ".join(map(str, rolls)) if num_dice > 1 else str(rolls[0]) modifier_text = f" {modifier:+d}" if modifier != 0 else "" return f"๐ŸŽฒ **{dice_notation}** [{rolls_text}]{modifier_text} = **{total}**" @mcp.tool def calculate_experience( party_size: Annotated[int, Field(description="Number of party members", ge=1)], party_level: Annotated[int, Field(description="Average party level", ge=1, le=20)], encounter_xp: Annotated[int, Field(description="Total encounter XP value", ge=0)], ) -> str: """Calculate experience points for an encounter.""" # D&D 5e encounter multipliers based on party size if party_size < 3: multiplier = 1.5 elif party_size > 5: multiplier = 0.5 else: multiplier = 1.0 adjusted_xp = int(encounter_xp * multiplier) xp_per_player = adjusted_xp // party_size return f"""**Experience Calculation:** Base Encounter XP: {encounter_xp} Party Size Multiplier: {multiplier}x Adjusted XP: {adjusted_xp} **XP per Player: {xp_per_player}**""" logger.debug("โœ… All tools successfully registered. Gamemaster-MCP server running! ๐ŸŽฒ") def main() -> None: """Main entry point for the D&D MCP Server.""" mcp.run() if __name__ == "__main__": main()

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/study-flamingo/gamemaster-mcp'

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