text-adventure-agent / mcp_server.py
Sunxt25's picture
Update mcp_server.py
296522c verified
"""
Student MCP Server for Text Adventure Games
Integrates: Automated map mapping, deep location extraction,
Jericho valid actions, and state monitoring.
"""
import sys
import os
import re
import json
from collections import defaultdict
# 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:
def __init__(self):
self.env: TextAdventureEnv = None
self.state = None
self.game_name: str = ""
# Core features: Map mapping and location tracking
self.current_location = "START"
self.explored_locations = defaultdict(dict) # {old_loc: {action: new_loc}}
def initialize(self, game: str = "zork1"):
"""Resets the environment and initializes the starting location."""
self.game_name = game
self.env = TextAdventureEnv(game)
self.state = self.env.reset()
# Extract initial location
self.current_location = self._extract_location(self.state.observation)
self.explored_locations.clear()
return self.state.observation
def step(self, action: str) -> str:
"""Executes a move, updates the map topology, and tracks location changes."""
if self.env is None:
self.initialize()
raw_action = action.strip().lower()
old_loc = self.current_location
# Execute action in game environment
self.state = self.env.step(raw_action)
obs = self.state.observation
# Core Logic: Extract location and update automated map
new_loc = self._extract_location(obs)
if old_loc and new_loc and old_loc != new_loc:
self.explored_locations[old_loc][raw_action] = new_loc
self.current_location = new_loc
return obs
def _extract_location(self, obs: str) -> str:
"""Accurately extracts room names from observation text across different game styles."""
# 1. Match [Room Name] format (Classic Zork style)
match = re.search(r"\[([^\]]+)\]", obs)
if match:
return match.group(1).upper()
# 2. Match short uppercase lines at the start (Standard header style)
lines = [l.strip() for l in obs.split("\n") if l.strip()]
for line in lines[:5]:
if (line[0].isupper() and
not line.endswith(".") and
not line.endswith("!") and
len(line) < 40):
return line.upper()
return self.current_location # Fallback to current location if extraction fails
def get_score(self) -> int:
return self.state.score if self.state else 0
def get_moves(self) -> int:
return self.state.moves if self.state else 0
# Global game manager instance
_game = GameManager()
def get_game() -> GameManager:
"""Singleton-style accessor for the global game manager."""
global _game
if _game.env is None:
game = os.environ.get("GAME", "zork1")
_game.initialize(game)
return _game
# =============================================================================
# MCP Tools - Full Functionality Implementation
# =============================================================================
@mcp.tool()
def play_action(action: str) -> str:
"""Executes an action and returns results, automatically injecting a location tag."""
game = get_game()
obs = game.step(action)
# Inject location tag to ensure the Agent always maintains spatial awareness
return f"[{game.current_location}] {obs}"
@mcp.tool()
def memory() -> str:
"""Retrieves current state summary: Location, Score, and Move count."""
game = get_game()
return f"LOC: {game.current_location} | SCORE: {game.get_score()} | MOVES: {game.get_moves()}"
@mcp.tool()
def inventory() -> str:
"""Queries the current items in the player's inventory."""
game = get_game()
return game.step("inventory")
@mcp.tool()
def get_map() -> str:
"""Returns the JSON topology of the explored map for BFS pathfinding."""
game = get_game()
if not game.explored_locations:
return "Map: Empty"
return json.dumps(game.explored_locations)
@mcp.tool()
def get_valid_actions() -> str:
"""Fetches currently valid action candidates from the Jericho engine."""
game = get_game()
# Access the underlying Jericho interface via the TextAdventureEnv wrapper
if game.env and hasattr(game.env, 'env'):
valid = game.env.env.get_valid_actions()
return json.dumps(valid[:50])
return "[]"
if __name__ == "__main__":
mcp.run()