text-adventure-template / mcp_server.py
DevZoneX's picture
Implement ReAct agent and MCP tools using the example submission
7ec4d32
"""
Example: MCP Server for Text Adventures
A complete MCP server that exposes text adventure games via tools.
This demonstrates a full-featured server with memory, mapping, and inventory.
"""
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, list_available_games
# Get game from environment variable (default: zork1)
INITIAL_GAME = os.environ.get("GAME", "zork1")
# Create the MCP server
mcp = FastMCP("Text Adventure Server")
class GameState:
"""Manages the text adventure game state and exploration data."""
def __init__(self, game: str = "zork1"):
self.game_name = game
self.env = TextAdventureEnv(game)
self.state = self.env.reset()
self.history: list[tuple[str, str]] = []
self.explored_locations: dict[str, set[str]] = {}
self.current_location: str = self._extract_location(self.state.observation)
def _extract_location(self, observation: str) -> str:
"""Extract location name from observation (usually first line)."""
lines = observation.strip().split('\n')
return lines[0] if lines else "Unknown"
def take_action(self, action: str) -> str:
"""Execute a game action and return the result."""
self.state = self.env.step(action)
result = self.state.observation
# Track history
self.history.append((action, result))
if len(self.history) > 50:
self.history = self.history[-50:]
# Update map
new_location = self._extract_location(result)
if action in ["north", "south", "east", "west", "up", "down",
"enter", "exit", "n", "s", "e", "w", "u", "d"]:
if self.current_location not in self.explored_locations:
self.explored_locations[self.current_location] = set()
if new_location != self.current_location:
self.explored_locations[self.current_location].add(f"{action} -> {new_location}")
self.current_location = new_location
return result
def get_memory(self) -> str:
"""Get a summary of current game state."""
recent = self.history[-5:] if self.history else []
recent_str = "\n".join([f" > {a} -> {r[:60]}..." for a, r in recent]) if recent else " (none yet)"
return f"""Current State:
- Location: {self.current_location}
- Score: {self.state.score} points
- Moves: {self.state.moves}
- Game: {self.game_name}
Recent Actions:
{recent_str}
Current Observation:
{self.state.observation}"""
def get_map(self) -> str:
"""Get a map of explored locations."""
if not self.explored_locations:
return "Map: No locations explored yet. Try moving around!"
lines = ["Explored Locations and Exits:"]
for loc, exits in sorted(self.explored_locations.items()):
lines.append(f"\n* {loc}")
for exit_info in sorted(exits):
lines.append(f" -> {exit_info}")
lines.append(f"\n[Current] {self.current_location}")
return "\n".join(lines)
def get_inventory(self) -> str:
"""Get current inventory."""
items = self.state.inventory if hasattr(self.state, 'inventory') and self.state.inventory else []
if not items:
return "Inventory: You are empty-handed."
item_names = []
for item in items:
item_str = str(item)
item_lower = item_str.lower()
if "parent" in item_lower:
idx = item_lower.index("parent")
name = item_str[:idx].strip()
if ":" in name:
name = name.split(":", 1)[1].strip()
item_names.append(name)
elif ":" in item_str:
name = item_str.split(":")[1].strip()
item_names.append(name)
else:
item_names.append(item_str)
return f"Inventory: {', '.join(item_names)}"
# Global game state
_game_state: GameState | None = None
def get_game() -> GameState:
"""Get or initialize the game state."""
global _game_state
if _game_state is None:
_game_state = GameState(INITIAL_GAME)
return _game_state
# =============================================================================
# MCP Tools
# =============================================================================
@mcp.tool()
def play_action(action: str) -> str:
"""
Execute a game action in the text adventure.
Args:
action: The command to execute (e.g., 'north', 'take lamp', 'open mailbox')
Returns:
The game's response to your action
"""
game = get_game()
result = game.take_action(action)
# Add score info
score_info = f"\n\n[Score: {game.state.score} | Moves: {game.state.moves}]"
if game.state.reward > 0:
score_info = f"\n\n+{game.state.reward} points! (Total: {game.state.score})"
done_info = ""
if game.state.done:
done_info = "\n\nGAME OVER"
return result + score_info + done_info
@mcp.tool()
def memory() -> str:
"""
Get a summary of the current game state.
Returns location, score, moves, recent actions, and current observation.
"""
return get_game().get_memory()
@mcp.tool()
def get_map() -> str:
"""
Get a map showing explored locations and connections.
Useful for navigation and avoiding getting lost.
"""
return get_game().get_map()
@mcp.tool()
def inventory() -> str:
"""
Check what items you are currently carrying.
"""
return get_game().get_inventory()
# =============================================================================
# Main
# =============================================================================
if __name__ == "__main__":
mcp.run()