Spaces:
Sleeping
Sleeping
Valentin Badea
Implemented memory-driven agent with two-phase LLM approach (Priorization/Summarization)
b113a1e | """ | |
| Student MCP Server for Text Adventure Games | |
| This is your MCP server submission. Implement the tools that your agent | |
| will use to play text adventure games. | |
| Required tool: | |
| play_action(action: str) -> str | |
| Execute a game command and return the result. | |
| Recommended tools: | |
| memory() -> str | |
| Return current game state, score, and recent history. | |
| inventory() -> str | |
| Return the player's current inventory. | |
| get_map() -> str | |
| Return a map of explored locations. | |
| Test your server with: | |
| fastmcp dev submission_template/mcp_server.py | |
| Then open the MCP Inspector in your browser to test the tools interactively. | |
| """ | |
| import sys | |
| import os | |
| # Add parent directory to path to import games module | |
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | |
| from fastmcp import FastMCP | |
| from games.zork_env import TextAdventureEnv | |
| # ============================================================================= | |
| # Create the MCP Server | |
| # ============================================================================= | |
| mcp = FastMCP("Student Text Adventure Server") | |
| # ============================================================================= | |
| # Game State Management | |
| # ============================================================================= | |
| class GameManager: | |
| """ | |
| Manages the text adventure game state. | |
| Tracks: | |
| - Action history (for memory tool, though agent manages its own memory) | |
| - Current location (using Jericho API) | |
| - Current score and moves | |
| Note: The agent handles its own location_memory system and doesn't rely | |
| on server-side tracking beyond the core MCP tools. | |
| """ | |
| def __init__(self): | |
| self.env: TextAdventureEnv = None | |
| self.state = None | |
| self.game_name: str = "" | |
| # State tracking (for optional tools - agent manages its own memory) | |
| self.history: list[tuple[str, str]] = [] # (action, observation) | |
| self.current_location: str = "Unknown" | |
| def initialize(self, game: str = "zork1"): | |
| """Initialize or reset the game.""" | |
| self.game_name = game | |
| self.env = TextAdventureEnv(game) | |
| self.state = self.env.reset() | |
| # Reset tracking data | |
| self.history = [] | |
| self.current_location = self._get_player_location_internal() | |
| return self.state.observation | |
| def _get_player_location_internal(self) -> str: | |
| """ | |
| Get current player location using Jericho API. | |
| """ | |
| if self.env and hasattr(self.env, 'env') and self.env.env: | |
| try: | |
| # Access Jericho's get_player_location() which returns a ZObject | |
| loc_obj = self.env.env.get_player_location() | |
| # ZObject has a .name attribute | |
| if hasattr(loc_obj, 'name'): | |
| return loc_obj.name | |
| except Exception: | |
| pass | |
| return "Unknown" | |
| def step(self, action: str) -> str: | |
| """Execute an action and return the result.""" | |
| if self.env is None: | |
| self.initialize() | |
| self.state = self.env.step(action) | |
| obs = self.state.observation | |
| # Record history | |
| self.history.append((action, obs)) | |
| if len(self.history) > 50: | |
| self.history = self.history[-50:] | |
| # Update current location using Jericho API | |
| self.current_location = self._get_player_location_internal() | |
| return obs | |
| def get_score(self) -> int: | |
| """Get current score.""" | |
| return self.state.score if self.state else 0 | |
| def get_moves(self) -> int: | |
| """Get number of moves taken.""" | |
| return self.state.moves if self.state else 0 | |
| # Global game manager | |
| _game = GameManager() | |
| # ============================================================================= | |
| # MCP Tools - IMPLEMENT THESE | |
| # ============================================================================= | |
| def play_action(action: str) -> str: | |
| """ | |
| Execute a game command and return the result. | |
| This is the main tool for interacting with the game. | |
| Args: | |
| action: The command to execute (e.g., "north", "take lamp", "open mailbox") | |
| Returns: | |
| The game's response to the action | |
| Valid commands include: | |
| - Movement: north, south, east, west, up, down, enter, exit | |
| - Objects: take <item>, drop <item>, open <thing>, examine <thing> | |
| - Other: look, inventory, read <thing>, turn on lamp | |
| """ | |
| game = get_game() | |
| obs = game.step(action) | |
| # Append score/moves info for the agent | |
| score_info = f"\n\n[Score: {game.get_score()} | Moves: {game.get_moves()}]" | |
| # If the environment exposes reward/done, include them (optional but helpful) | |
| try: | |
| if getattr(game.state, "reward", 0) and game.state.reward > 0: | |
| score_info = f"\n\n+{game.state.reward} points! (Total: {game.get_score()})" | |
| except Exception: | |
| pass | |
| done_info = "" | |
| try: | |
| if getattr(game.state, "done", False): | |
| done_info = "\n\nGAME OVER" | |
| except Exception: | |
| pass | |
| return obs + score_info + done_info | |
| def memory() -> str: | |
| """ | |
| Get a summary of the current game state. | |
| Returns: | |
| A summary including current location, score, moves, and recent history | |
| """ | |
| game = get_game() | |
| recent = game.history[-5:] if game.history else [] | |
| if recent: | |
| recent_str = "\n".join([f" > {a} -> {obs[:80]}..." for a, obs in recent]) | |
| else: | |
| recent_str = " (none yet)" | |
| return ( | |
| "Current State:\n" | |
| f"- Location: {game.current_location}\n" | |
| f"- Score: {game.get_score()} points\n" | |
| f"- Moves: {game.get_moves()}\n" | |
| f"- Game: {game.game_name}\n\n" | |
| "Recent Actions:\n" | |
| f"{recent_str}\n\n" | |
| "Current Observation:\n" | |
| f"{game.state.observation if game.state else ''}" | |
| ) | |
| def get_map() -> str: | |
| """ | |
| Get a map of explored locations. | |
| Note: This tool is not used by the current agent implementation. | |
| The agent manages its own location memory internally. | |
| Returns: | |
| A message indicating the agent doesn't use this tool | |
| """ | |
| game = get_game() | |
| return f"Current location: {game.current_location}\\n\\nNote: The agent manages location tracking internally via its location_memory system." | |
| def inventory() -> str: | |
| """ | |
| Check what items you are currently carrying. | |
| """ | |
| game = get_game() | |
| items = [] | |
| try: | |
| if getattr(game.state, "inventory", None): | |
| items = game.state.inventory | |
| except Exception: | |
| items = [] | |
| if not items: | |
| return "Inventory: You are empty-handed." | |
| # Convert items to readable names | |
| item_names = [] | |
| for item in items: | |
| s = str(item) | |
| s_lower = s.lower() | |
| if "parent" in s_lower: | |
| idx = s_lower.index("parent") | |
| name = s[:idx].strip() | |
| if ":" in name: | |
| name = name.split(":", 1)[1].strip() | |
| item_names.append(name) | |
| elif ":" in s: | |
| item_names.append(s.split(":", 1)[1].strip()) | |
| else: | |
| item_names.append(s) | |
| return "Inventory: " + ", ".join(item_names) | |
| def get_game() -> GameManager: | |
| """Get or initialize the game manager.""" | |
| global _game | |
| game_name = os.environ.get("GAME", "zork1") | |
| if _game.env is None: | |
| _game.initialize(game_name) | |
| elif _game.game_name != game_name: | |
| _game.initialize(game_name) | |
| return _game | |
| def get_player_location() -> str: | |
| """ | |
| Get the current player location name from Jericho's location tracking. | |
| Returns: | |
| The name of the current location (e.g., "West of House", "Forest") | |
| """ | |
| game = get_game() | |
| if game.env and hasattr(game.env, 'env') and game.env.env: | |
| try: | |
| # Access Jericho's get_player_location() which returns a ZObject | |
| loc_obj = game.env.env.get_player_location() | |
| # ZObject has a .name attribute | |
| if hasattr(loc_obj, 'name'): | |
| return loc_obj.name | |
| except Exception as e: | |
| pass | |
| # Fallback: use heuristic location extraction | |
| return game.current_location | |
| def get_valid_actions() -> str: | |
| """ | |
| Get valid actions from Jericho's action space at the current location. | |
| Returns: | |
| JSON string with valid actions: {"available": true, "actions": [...], "count": N} | |
| """ | |
| game = get_game() | |
| if game.env and hasattr(game.env, 'env') and game.env.env: | |
| try: | |
| # CRITICAL: use_parallel=False to prevent deadlock on Lost Pig | |
| valid_actions = game.env.env.get_valid_actions( | |
| use_object_tree=True, | |
| use_ctypes=True, | |
| use_parallel=False # Prevent multiprocessing deadlock | |
| ) | |
| import json | |
| return json.dumps({ | |
| "available": True, | |
| "actions": valid_actions, | |
| "count": len(valid_actions), | |
| "source": "jericho" | |
| }) | |
| except Exception as e: | |
| import json | |
| return json.dumps({ | |
| "available": False, | |
| "error": str(e), | |
| "actions": [], | |
| "count": 0 | |
| }) | |
| import json | |
| return json.dumps({ | |
| "available": False, | |
| "error": "Game environment not initialized", | |
| "actions": [], | |
| "count": 0 | |
| }) | |
| # ============================================================================= | |
| # Run the server | |
| # ============================================================================= | |
| if __name__ == "__main__": | |
| # This runs the server with stdio transport (for MCP clients) | |
| mcp.run() | |