| """ |
| 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 |
|
|
| |
| 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 |
|
|
| import asyncio |
|
|
| |
| INITIAL_GAME = os.environ.get("GAME", "zork1") |
|
|
| |
| 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 clean_text(self, text: str) -> str: |
| """Transforme 'obj91: pants parent87...' en 'pants'.""" |
| text = str(text).lower() |
| if ":" in text: |
| |
| name_part = text.split(":", 1)[1].strip() |
| |
| for stop_word in [" parent", " sibling", " child", " attributes"]: |
| if stop_word in name_part: |
| name_part = name_part.split(stop_word)[0].strip() |
| return name_part |
| return text |
| |
| 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 |
| |
| |
| self.history.append((action, result)) |
| if len(self.history) > 50: |
| self.history = self.history[-50:] |
| |
| |
| 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_heuristic_actions(self, structured_data: dict) -> str: |
| """ |
| Fournit au LLM les briques nécessaires pour construire ses propres actions |
| en se basant sur la vérité de la RAM. |
| """ |
| self._debug_log("HEURISTIC - COMPONENT BASED") |
|
|
| |
| |
| inventory_items = [i['name'] for i in self.state.inventory_raw] |
| room_items = structured_data.get("raw_ram_objects", []) |
| |
| |
| |
| available_verbs = ["examine", "take", "drop", "look", "inventory", "wait"] |
| |
| obs = self.state.observation.lower() |
| |
| if any(k in obs for k in ["door", "window", "container", "box"]): |
| available_verbs += ["open", "close", "unlock"] |
| |
| if any(k in obs for k in ["noise", "sound", "squeal", "quiet"]): |
| available_verbs.append("listen") |
| |
| if "dark" in obs or "torch" in str(inventory_items): |
| available_verbs += ["turn on", "light"] |
|
|
| |
| final_output = [ |
| "### ACTION CONSTRUCTION KIT", |
| f"VERBS: {', '.join(sorted(set(available_verbs)))}", |
| f"INVENTORY: {', '.join(inventory_items) if inventory_items else 'Empty'}", |
| f"ROOM OBJECTS: {', '.join(room_items) if room_items else 'None visible'}", |
| "\nNAVIGATION: n, s, e, w, ne, nw, se, sw, u, d, in, out", |
| "\nEXAMPLES: 'examine tracks', 'turn on torch', 'open mailbox', 'listen'" |
| ] |
|
|
| return "\n".join(final_output) |
|
|
|
|
| def _debug_log(self, message: str): |
| """Envoie un log de debug vers stderr pour ne pas polluer le flux MCP.""" |
| print(f"DEBUG_HEURISTIC: {message}", file=sys.stderr, flush=True) |
|
|
| 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)}" |
|
|
|
|
| |
| _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.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) |
| |
| |
| 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() |
|
|
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| @mcp.tool() |
| async def get_valid_actions_cheat() -> str: |
| """ |
| [GOD MODE] Returns the list of ALL valid actions currently possible. |
| """ |
| |
| try: |
| game = get_game() |
| jericho_env = game.env.env |
| |
| |
| valid_actions = jericho_env.get_valid_actions(use_parallel=False) |
|
|
| if not valid_actions: |
| return "Jericho returned an empty list of actions." |
| |
| return "VALID ACTIONS:\n" + ", ".join(valid_actions) |
|
|
| except Exception as e: |
| return f"Error retrieving valid actions: {str(e)}" |
|
|
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| @mcp.tool() |
| def detect_objects_cheat() -> str: |
| """ |
| [GOD MODE] Scans the game memory to find interactive objects in the current location. |
| Returns their internal game names. |
| """ |
| try: |
| |
| |
| objs = env.identify_interactive_objects() |
| return f"OBJECTS IN MEMORY: {objs}" |
| except Exception as e: |
| return f"Feature not available: {e}" |
|
|
|
|
| @mcp.tool() |
| def cheat_sense_surroundings() -> str: |
| try: |
| game = get_game() |
| j_env = game.env.env |
| |
| player_loc = 164 |
| |
| |
| world_objs = j_env.get_world_objects() |
| contents = [] |
| |
| for obj in world_objs: |
| |
| if obj.parent == player_loc: |
| name = str(obj.name).replace('\x00', '').strip() |
| |
| if name and "player" not in name.lower(): |
| contents.append(f"{name} (ID:{obj.num})") |
|
|
| return "VISIBLE IN MEMORY: " + (", ".join(contents) if contents else "Empty Room") |
| except Exception as e: |
| return f"Error: {e}" |
|
|
| @mcp.tool() |
| def get_game_dictionary() -> list[str]: |
| """Récupère tous les mots compris par le jeu.""" |
| game = get_game() |
| |
| words = game.env.env.get_dictionary() |
| return [w.word for w in words] |
|
|
| @mcp.tool() |
| def get_action_grammar() -> str: |
| """Récupère les modèles de phrases autorisées (ex: 'put X on Y').""" |
| game = get_game() |
| return game.env.env.bindings.get('grammar', 'Non disponible') |
|
|
|
|
| @mcp.tool() |
| def cheat_get_valid_exits() -> str: |
| """[GOD MODE] Analyse les propriétés de la pièce actuelle pour trouver les sorties.""" |
| try: |
| game = get_game() |
| j_env = game.env.env |
| |
| room_id = 166 |
| |
| |
| directions = ['north', 'south', 'east', 'west', 'up', 'down', 'ne', 'nw', 'se', 'sw', 'in', 'out'] |
| valid_exits = [] |
|
|
| |
| |
| for d in directions: |
| try: |
| |
| dest_id = j_env.get_next_location(room_id, d) |
| if dest_id > 0: |
| valid_exits.append(d) |
| except: |
| continue |
|
|
| |
| if not valid_exits: |
| obs = game.state.observation.lower() |
| |
| for d in directions: |
| if f" {d}" in obs and ("exit" in obs or "lead" in obs or "way" in obs): |
| valid_exits.append(d) |
| |
| return f"STRICT VALID EXITS: {', '.join(valid_exits) if valid_exits else 'None (Try exploring manually)'}" |
| except Exception as e: |
| return f"Error: {e}" |
|
|
| @mcp.tool() |
| def cheat_get_status() -> str: |
| """ |
| [GOD MODE] Returns the internal state: Score, Moves, and Inventory. |
| """ |
| try: |
| score = env.get_score() |
| |
| inv_objs = env.get_inventory() |
| inv_names = [obj.name for obj in inv_objs] |
| |
| return f""" |
| SCORE: {score} |
| INVENTORY (INTERNAL): {inv_names} |
| """ |
| except Exception as e: |
| return str(e) |
|
|
|
|
| @mcp.tool() |
| def get_location_info() -> dict: |
| """ |
| Scan la pièce actuelle et le contenu immédiat des objets (Niveau +1). |
| Permet au LLM de déduire les types via les noms et la structure. |
| """ |
| try: |
| game = get_game() |
| env = game.env.env |
| |
| def clean_name(name): |
| return str(name).replace('\x00', '').strip() if name else "Inconnu" |
|
|
| |
| loc = env.get_player_location() |
| room_id = loc.num if hasattr(loc, 'num') else int(loc) |
| room_obj = env.get_object(room_id) |
| |
| world_objs = env.get_world_objects() |
|
|
| |
| detected_elements = [] |
| for obj in world_objs: |
| |
| if obj.parent == room_id: |
| name = clean_name(obj.name) |
| if "player" in name.lower(): continue |
|
|
| |
| sub_contents = [] |
| for sub_obj in world_objs: |
| if sub_obj.parent == obj.num: |
| sub_contents.append({ |
| "id": sub_obj.num, |
| "name": clean_name(sub_obj.name) |
| }) |
|
|
| |
| element = { |
| "id": obj.num, |
| "name": name, |
| "contains_count": len(sub_contents), |
| "contents": sub_contents |
| } |
| detected_elements.append(element) |
|
|
| return { |
| "status": "success", |
| "location": { |
| "id": room_id, |
| "name": clean_name(room_obj.name) |
| }, |
| "detected_objects": detected_elements, |
| "inventory": [ |
| {"id": i.num, "name": clean_name(i.name)} |
| for i in env.get_inventory() |
| ], |
| "world_hash": env.get_world_state_hash() |
| } |
|
|
| except Exception as e: |
| return {"status": "error", "message": str(e)} |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| @mcp.tool() |
| def get_current_room_name() -> str: |
| """Récupère le nom officiel de la pièce dans la mémoire du jeu.""" |
| try: |
| game = get_game() |
| env = game.env.env |
| |
| |
| room_id = env.get_player_location() |
| |
| |
| room_obj = env.get_object(room_id) |
| |
| |
| return str(room_obj.name).strip() |
| except Exception as e: |
| return f"Error: {e}" |
|
|
| @mcp.tool() |
| def test_dictionary_access() -> str: |
| """[TEST] Récupère un échantillon du dictionnaire interne du jeu.""" |
| try: |
| game = get_game() |
| |
| words = game.env.env.get_dictionary() |
| |
| sample = [w.word for w in words[:20]] |
| return f"DICTIONARY SAMPLE ({len(words)} words total): " + ", ".join(sample) |
| except Exception as e: |
| return f"Dictionary Test Failed: {e}" |
|
|
| @mcp.tool() |
| def test_grammar_access() -> str: |
| """[TEST] Récupère les modèles d'actions (Grammaire).""" |
| try: |
| game = get_game() |
| |
| bindings = game.env.env.bindings |
| grammar = bindings.get('grammar', 'No grammar found') |
| |
| return f"GRAMMAR SNIPPET: {grammar[:300]}..." |
| except Exception as e: |
| return f"Grammar Test Failed: {e}" |
|
|
| @mcp.tool() |
| def get_object_tree_simple() -> str: |
| """[TEST] Liste brute des 5 premiers objets pour voir leur structure.""" |
| try: |
| game = get_game() |
| objs = game.env.env.get_world_objects() |
| res = [] |
| for o in objs[:5]: |
| res.append(f"Obj{o.num}: {o.name} (Parent:{o.parent})") |
| return "OBJECT TREE SAMPLE: " + " | ".join(res) |
| except Exception as e: |
| return f"Object Tree Failed: {e}" |
| |
| |
| |
|
|
| if __name__ == "__main__": |
| mcp.run() |
|
|