fegounna commited on
Commit
bceae40
·
verified ·
1 Parent(s): f747b46

Update mcp_server.py

Browse files
Files changed (1) hide show
  1. mcp_server.py +163 -156
mcp_server.py CHANGED
@@ -1,27 +1,8 @@
1
  """
2
- Student MCP Server for Text Adventure Games
3
 
4
- This is your MCP server submission. Implement the tools that your agent
5
- will use to play text adventure games.
6
-
7
- Required tool:
8
- play_action(action: str) -> str
9
- Execute a game command and return the result.
10
-
11
- Recommended tools:
12
- memory() -> str
13
- Return current game state, score, and recent history.
14
-
15
- inventory() -> str
16
- Return the player's current inventory.
17
-
18
- get_map() -> str
19
- Return a map of explored locations.
20
-
21
- Test your server with:
22
- fastmcp dev submission_template/mcp_server.py
23
-
24
- Then open the MCP Inspector in your browser to test the tools interactively.
25
  """
26
 
27
  import sys
@@ -31,179 +12,205 @@ import os
31
  sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
32
 
33
  from fastmcp import FastMCP
34
- from games.zork_env import TextAdventureEnv
35
-
36
 
37
- # =============================================================================
38
- # Create the MCP Server
39
- # =============================================================================
40
 
41
- mcp = FastMCP("Student Text Adventure Server")
 
42
 
 
 
43
 
44
- # =============================================================================
45
- # Game State Management
46
- # =============================================================================
47
 
48
- class GameManager:
49
- """
50
- Manages the text adventure game state.
51
-
52
- TODO: Extend this class to track:
53
- - Action history (for memory tool)
54
- - Explored locations (for mapping)
55
- - Current score and moves
56
- """
57
 
58
- def __init__(self):
59
- self.env: TextAdventureEnv = None
60
- self.state = None
61
- self.game_name: str = ""
62
- # TODO: Add more state tracking
63
- # self.history: list[tuple[str, str]] = []
64
- # self.explored_locations: dict[str, set[str]] = {}
65
- # self.current_location: str = ""
66
-
67
- def initialize(self, game: str = "zork1"):
68
- """Initialize or reset the game."""
69
  self.game_name = game
70
  self.env = TextAdventureEnv(game)
71
  self.state = self.env.reset()
72
- # TODO: Reset your state tracking here
73
- return self.state.observation
 
74
 
75
- def step(self, action: str) -> str:
76
- """Execute an action and return the result."""
77
- if self.env is None:
78
- self.initialize()
79
-
 
 
80
  self.state = self.env.step(action)
 
 
 
 
 
 
81
 
82
- # TODO: Update your state tracking here
83
- # self.history.append((action, self.state.observation))
84
- # Update location tracking, etc.
 
 
 
 
 
 
85
 
86
- return self.state.observation
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
- def get_score(self) -> int:
89
- """Get current score."""
90
- return self.state.score if self.state else 0
 
 
 
 
 
 
 
 
 
 
91
 
92
- def get_moves(self) -> int:
93
- """Get number of moves taken."""
94
- return self.state.moves if self.state else 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
 
97
- # Global game manager
98
- _game = GameManager()
99
 
100
 
101
- def get_game() -> GameManager:
102
- """Get or initialize the game manager."""
103
- global _game
104
- if _game.env is None:
105
- # Get game from environment variable (set by evaluator)
106
- game = os.environ.get("GAME", "zork1")
107
- _game.initialize(game)
108
- return _game
109
 
110
 
111
  # =============================================================================
112
- # MCP Tools - IMPLEMENT THESE
113
  # =============================================================================
114
 
115
  @mcp.tool()
116
  def play_action(action: str) -> str:
117
  """
118
- Execute a game command and return the result.
119
-
120
- This is the main tool for interacting with the game.
121
 
122
  Args:
123
- action: The command to execute (e.g., "north", "take lamp", "open mailbox")
124
-
125
  Returns:
126
- The game's response to the action
127
-
128
- Valid commands include:
129
- - Movement: north, south, east, west, up, down, enter, exit
130
- - Objects: take <item>, drop <item>, open <thing>, examine <thing>
131
- - Other: look, inventory, read <thing>, turn on lamp
132
  """
133
  game = get_game()
 
 
 
 
134
 
135
- # TODO: You might want to add action validation here
136
- # TODO: You might want to include score changes in the response
137
 
138
- result = game.step(action)
 
 
139
 
140
- # Optional: Append score info
141
- # result += f"\n[Score: {game.get_score()} | Moves: {game.get_moves()}]"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
 
143
- return result
144
-
145
-
146
- # TODO: Implement additional tools to help your agent
147
-
148
- # @mcp.tool()
149
- # def memory() -> str:
150
- # """
151
- # Get the current game state summary.
152
- #
153
- # Returns:
154
- # A summary including current location, score, moves, and recent history
155
- # """
156
- # game = get_game()
157
- # # TODO: Return useful state information
158
- # pass
159
-
160
-
161
- # @mcp.tool()
162
- # def inventory() -> str:
163
- # """
164
- # Check what the player is carrying.
165
- #
166
- # Returns:
167
- # List of items in the player's inventory
168
- # """
169
- # game = get_game()
170
- # result = game.step("inventory")
171
- # return result
172
-
173
-
174
- # @mcp.tool()
175
- # def get_map() -> str:
176
- # """
177
- # Get a map of explored locations.
178
- #
179
- # Returns:
180
- # A text representation of explored locations and connections
181
- # """
182
- # game = get_game()
183
- # # TODO: Return map of explored locations
184
- # pass
185
-
186
-
187
- # @mcp.tool()
188
- # def get_valid_actions() -> str:
189
- # """
190
- # Get a list of likely valid actions from the current location.
191
- #
192
- # Returns:
193
- # List of actions that might work here
194
- # """
195
- # # This is a hint: Jericho provides get_valid_actions()
196
- # game = get_game()
197
- # if game.env and game.env.env:
198
- # valid = game.env.env.get_valid_actions()
199
- # return "Valid actions: " + ", ".join(valid[:20])
200
- # return "Could not determine valid actions"
201
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
 
203
  # =============================================================================
204
- # Run the server
205
  # =============================================================================
206
 
207
  if __name__ == "__main__":
208
- # This runs the server with stdio transport (for MCP clients)
209
- mcp.run()
 
1
  """
2
+ Example: MCP Server for Text Adventures
3
 
4
+ A complete MCP server that exposes text adventure games via tools.
5
+ This demonstrates a full-featured server with memory, mapping, and inventory.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  """
7
 
8
  import sys
 
12
  sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
13
 
14
  from fastmcp import FastMCP
15
+ from games.zork_env import TextAdventureEnv, list_available_games
 
16
 
 
 
 
17
 
18
+ # Get game from environment variable (default: zork1)
19
+ INITIAL_GAME = os.environ.get("GAME", "zork1")
20
 
21
+ # Create the MCP server
22
+ mcp = FastMCP("Text Adventure Server")
23
 
 
 
 
24
 
25
+ class GameState:
26
+ """Manages the text adventure game state and exploration data."""
 
 
 
 
 
 
 
27
 
28
+ def __init__(self, game: str = "zork1"):
 
 
 
 
 
 
 
 
 
 
29
  self.game_name = game
30
  self.env = TextAdventureEnv(game)
31
  self.state = self.env.reset()
32
+ self.history: list[tuple[str, str]] = []
33
+ self.explored_locations: dict[str, set[str]] = {}
34
+ self.current_location: str = self._extract_location(self.state.observation)
35
 
36
+ def _extract_location(self, observation: str) -> str:
37
+ """Extract location name from observation (usually first line)."""
38
+ lines = observation.strip().split('\n')
39
+ return lines[0] if lines else "Unknown"
40
+
41
+ def take_action(self, action: str) -> str:
42
+ """Execute a game action and return the result."""
43
  self.state = self.env.step(action)
44
+ result = self.state.observation
45
+
46
+ # Track history
47
+ self.history.append((action, result))
48
+ if len(self.history) > 50:
49
+ self.history = self.history[-50:]
50
 
51
+ # Update map
52
+ new_location = self._extract_location(result)
53
+ if action in ["north", "south", "east", "west", "up", "down",
54
+ "enter", "exit", "n", "s", "e", "w", "u", "d"]:
55
+ if self.current_location not in self.explored_locations:
56
+ self.explored_locations[self.current_location] = set()
57
+ if new_location != self.current_location:
58
+ self.explored_locations[self.current_location].add(f"{action} -> {new_location}")
59
+ self.current_location = new_location
60
 
61
+ return result
62
+
63
+ def get_memory(self) -> str:
64
+ """Get a summary of current game state."""
65
+ recent = self.history[-5:] if self.history else []
66
+ recent_str = "\n".join([f" > {a} -> {r[:60]}..." for a, r in recent]) if recent else " (none yet)"
67
+
68
+ return f"""Current State:
69
+ - Location: {self.current_location}
70
+ - Score: {self.state.score} points
71
+ - Moves: {self.state.moves}
72
+ - Game: {self.game_name}
73
+
74
+ Recent Actions:
75
+ {recent_str}
76
+
77
+ Current Observation:
78
+ {self.state.observation}"""
79
 
80
+ def get_map(self) -> str:
81
+ """Get a map of explored locations."""
82
+ if not self.explored_locations:
83
+ return "Map: No locations explored yet. Try moving around!"
84
+
85
+ lines = ["Explored Locations and Exits:"]
86
+ for loc, exits in sorted(self.explored_locations.items()):
87
+ lines.append(f"\n* {loc}")
88
+ for exit_info in sorted(exits):
89
+ lines.append(f" -> {exit_info}")
90
+
91
+ lines.append(f"\n[Current] {self.current_location}")
92
+ return "\n".join(lines)
93
 
94
+ def get_inventory(self) -> str:
95
+ """Get current inventory."""
96
+ items = self.state.inventory if hasattr(self.state, 'inventory') and self.state.inventory else []
97
+
98
+ if not items:
99
+ return "Inventory: You are empty-handed."
100
+
101
+ item_names = []
102
+ for item in items:
103
+ item_str = str(item)
104
+ item_lower = item_str.lower()
105
+ if "parent" in item_lower:
106
+ idx = item_lower.index("parent")
107
+ name = item_str[:idx].strip()
108
+ if ":" in name:
109
+ name = name.split(":", 1)[1].strip()
110
+ item_names.append(name)
111
+ elif ":" in item_str:
112
+ name = item_str.split(":")[1].strip()
113
+ item_names.append(name)
114
+ else:
115
+ item_names.append(item_str)
116
+
117
+ return f"Inventory: {', '.join(item_names)}"
118
 
119
 
120
+ # Global game state
121
+ _game_state: GameState | None = None
122
 
123
 
124
+ def get_game() -> GameState:
125
+ """Get or initialize the game state."""
126
+ global _game_state
127
+ if _game_state is None:
128
+ _game_state = GameState(INITIAL_GAME)
129
+ return _game_state
 
 
130
 
131
 
132
  # =============================================================================
133
+ # MCP Tools
134
  # =============================================================================
135
 
136
  @mcp.tool()
137
  def play_action(action: str) -> str:
138
  """
139
+ Execute a game action in the text adventure.
 
 
140
 
141
  Args:
142
+ action: The command to execute (e.g., 'north', 'take lamp', 'open mailbox')
143
+
144
  Returns:
145
+ The game's response to your action
 
 
 
 
 
146
  """
147
  game = get_game()
148
+ result = game.take_action(action)
149
+
150
+ # Add score info
151
+ score_info = f"\n\n[Score: {game.state.score} | Moves: {game.state.moves}]"
152
 
153
+ if game.state.reward > 0:
154
+ score_info = f"\n\n+{game.state.reward} points! (Total: {game.state.score})"
155
 
156
+ done_info = ""
157
+ if game.state.done:
158
+ done_info = "\n\nGAME OVER"
159
 
160
+ return result + score_info + done_info
161
+
162
+ import concurrent.futures
163
+
164
+ @mcp.tool()
165
+ def available_actions() -> str:
166
+ """Get the list of valid actions at the current location."""
167
+ game = get_game()
168
+ with concurrent.futures.ThreadPoolExecutor() as executor:
169
+ future = executor.submit(game.env.get_valid_actions)
170
+ try:
171
+ actions = future.result(timeout=5)
172
+ return ", ".join(actions)
173
+ except concurrent.futures.TimeoutError:
174
+ return "Timed out. Common actions: look, north, south, east, west, up, down, take, open, examine"
175
+
176
+
177
+ @mcp.tool()
178
+ def memory() -> str:
179
+ """
180
+ Get a summary of the current game state.
181
 
182
+ Returns location, score, moves, recent actions, and current observation.
183
+ """
184
+ return get_game().get_memory()
185
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
 
187
+ @mcp.tool()
188
+ def get_map() -> str:
189
+ """
190
+ Get a map showing explored locations and connections.
191
+
192
+ Useful for navigation and avoiding getting lost.
193
+ """
194
+ return get_game().get_map()
195
+
196
+
197
+ @mcp.tool()
198
+ def inventory() -> str:
199
+ """
200
+ Check what items you are currently carrying.
201
+ """
202
+ return get_game().get_inventory()
203
+
204
+
205
+ @mcp.tool()
206
+ def get_location() -> str:
207
+ """Get the current player location reliably using the game engine."""
208
+ game = get_game()
209
+ return game.state.location
210
 
211
  # =============================================================================
212
+ # Main
213
  # =============================================================================
214
 
215
  if __name__ == "__main__":
216
+ mcp.run()