Jofthomas commited on
Commit
1978788
·
verified ·
1 Parent(s): 5886b94

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +302 -0
app.py ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import requests
3
+ import base64
4
+ from dotenv import load_dotenv
5
+ from mcp.server.fastmcp import FastMCP, Image
6
+
7
+ load_dotenv()
8
+
9
+ # --- Configuration ---
10
+ # URL of the Flask app API - configurable via environment variable
11
+ FLASK_API_URL = os.getenv("FLASK_API_URL", "http://127.0.0.1:5000")
12
+
13
+ # --- MCP Server Setup ---
14
+ mcp = FastMCP(
15
+ name="GeoGuessrAgent",
16
+ host="127.0.0.1",
17
+ port=7860,
18
+ )
19
+
20
+ # --- Game State Management ---
21
+ # Store the current game ID and basic state
22
+ active_game = {}
23
+
24
+ # --- Flask API Helper Functions ---
25
+ def call_flask_api(endpoint, method='GET', json_data=None):
26
+ """Helper function to call Flask API endpoints"""
27
+ url = f"{FLASK_API_URL}{endpoint}"
28
+ try:
29
+ if method == 'POST':
30
+ response = requests.post(url, json=json_data, headers={'Content-Type': 'application/json'}, timeout=30)
31
+ else:
32
+ response = requests.get(url, timeout=30)
33
+
34
+ if response.status_code in [200, 201]:
35
+ return response.json()
36
+ else:
37
+ error_msg = f"API call failed: {response.status_code} - {response.text}"
38
+ print(f"Flask API Error: {error_msg}")
39
+ raise Exception(error_msg)
40
+ except requests.exceptions.ConnectionError as e:
41
+ error_msg = f"Could not connect to Flask API at {FLASK_API_URL}. Make sure the Flask server is running. Error: {str(e)}"
42
+ print(f"Connection Error: {error_msg}")
43
+ raise Exception(error_msg)
44
+ except requests.exceptions.Timeout as e:
45
+ error_msg = f"Timeout calling Flask API at {FLASK_API_URL}. Error: {str(e)}"
46
+ print(f"Timeout Error: {error_msg}")
47
+ raise Exception(error_msg)
48
+ except Exception as e:
49
+ error_msg = f"API call error: {str(e)}"
50
+ print(f"General Error: {error_msg}")
51
+ raise Exception(error_msg)
52
+
53
+ def base64_to_image_bytes(base64_string):
54
+ """Convert base64 string to image bytes"""
55
+ try:
56
+ if not base64_string:
57
+ raise ValueError("Empty base64 string provided")
58
+
59
+ # Remove any data URL prefix if present
60
+ if base64_string.startswith('data:'):
61
+ base64_string = base64_string.split(',', 1)[1]
62
+
63
+ # Decode the base64 string
64
+ image_bytes = base64.b64decode(base64_string)
65
+
66
+ if len(image_bytes) == 0:
67
+ raise ValueError("Decoded image is empty")
68
+
69
+ print(f"Successfully decoded image: {len(image_bytes)} bytes")
70
+ return image_bytes
71
+
72
+ except Exception as e:
73
+ print(f"Error decoding base64 image: {e}")
74
+ raise ValueError(f"Failed to decode base64 image: {str(e)}")
75
+
76
+ # --- MCP Tools ---
77
+
78
+ @mcp.tool()
79
+ def start_game(difficulty: str = "easy", player_name: str = "MCP Agent") -> Image:
80
+ """
81
+ Starts a new GeoGuessr game by calling the Flask API.
82
+ Args:
83
+ difficulty (str): The difficulty of the game ('easy', 'medium', 'hard').
84
+ player_name (str): The name of the player/agent.
85
+ Returns:
86
+ Image: The first Street View image with compass overlay.
87
+ """
88
+ global active_game
89
+
90
+ # Call Flask API to start game
91
+ game_data = call_flask_api('/start_game', 'POST', {
92
+ 'difficulty': difficulty,
93
+ 'player_name': player_name
94
+ })
95
+
96
+ # Store game state
97
+ active_game = {
98
+ 'game_id': game_data['game_id'],
99
+ 'player_name': game_data['player_name'],
100
+ 'game_over': False
101
+ }
102
+
103
+ # Convert base64 image to bytes and return as Image
104
+ if game_data.get('streetview_image'):
105
+ try:
106
+ image_bytes = base64_to_image_bytes(game_data['streetview_image'])
107
+ print(f"Successfully started game {active_game['game_id']} for player {active_game['player_name']}")
108
+ return Image(data=image_bytes, format="jpeg")
109
+ except Exception as e:
110
+ print(f"Error processing Street View image: {e}")
111
+ raise Exception(f"Failed to process Street View image: {str(e)}")
112
+ else:
113
+ raise Exception("No Street View image received from the game")
114
+
115
+
116
+
117
+ @mcp.tool()
118
+ def move(direction: str = None, degree: float = None, distance: float = 0.1) -> Image:
119
+ """
120
+ Moves the player in a specified direction by calling the Flask API.
121
+ Args:
122
+ direction (str, optional): Direction to move (N, NE, E, SE, S, SW, W, NW).
123
+ degree (float, optional): Precise direction in degrees (0-360).
124
+ distance (float): Distance to move in kilometers (default: 0.1km = 100m).
125
+ Returns:
126
+ Image: The new Street View image with compass overlay.
127
+ """
128
+ global active_game
129
+ if not active_game or not active_game.get('game_id'):
130
+ raise ValueError("Game not started. Call start_game() first.")
131
+ if active_game.get('game_over'):
132
+ raise ValueError("Game is over.")
133
+
134
+
135
+ # Prepare move data
136
+ move_data = {'distance': distance}
137
+ if direction:
138
+ move_data['direction'] = direction
139
+ elif degree is not None:
140
+ move_data['degree'] = degree
141
+ else:
142
+ raise ValueError("Must provide either direction or degree parameter.")
143
+
144
+ # Call Flask API to move
145
+ game_id = active_game['game_id']
146
+ move_result = call_flask_api(f'/game/{game_id}/move', 'POST', move_data)
147
+
148
+ # Convert base64 image to bytes and return as Image
149
+ if move_result.get('streetview_image'):
150
+ try:
151
+ image_bytes = base64_to_image_bytes(move_result['streetview_image'])
152
+ direction_info = move_result.get('moved_direction', 'unknown direction')
153
+ distance_info = move_result.get('distance_moved_km', 0) * 1000
154
+ print(f"Successfully moved {direction_info} for {distance_info:.0f}m in game {game_id}")
155
+ return Image(data=image_bytes, format="jpeg")
156
+ except Exception as e:
157
+ print(f"Error processing move Street View image: {e}")
158
+ raise Exception(f"Failed to process move Street View image: {str(e)}")
159
+ else:
160
+ raise Exception("No Street View image received from the move")
161
+
162
+ @mcp.tool()
163
+ def make_placeholder_guess(lat: float, lng: float) -> dict:
164
+ """
165
+ Records a temporary guess for the location (stored locally until final guess).
166
+ Args:
167
+ lat (float): The latitude of the guess.
168
+ lng (float): The longitude of the guess.
169
+ Returns:
170
+ dict: A status message.
171
+ """
172
+ global active_game
173
+ if not active_game or not active_game.get('game_id'):
174
+ raise ValueError("Game not started.")
175
+ if active_game.get('game_over'):
176
+ raise ValueError("Game is over.")
177
+
178
+ active_game['placeholder_guess'] = {'lat': lat, 'lng': lng}
179
+ return {"status": "success", "message": f"Placeholder guess recorded: {lat:.6f}, {lng:.6f}"}
180
+
181
+
182
+ @mcp.tool()
183
+ def make_final_guess() -> dict:
184
+ """
185
+ Makes the final guess for the active game by calling the Flask API.
186
+ Uses the stored placeholder guess coordinates.
187
+ Returns:
188
+ dict: The results of the guess including distance, score, and actual location.
189
+ """
190
+ global active_game
191
+ if not active_game or not active_game.get('game_id'):
192
+ raise ValueError("Game not started.")
193
+ if active_game.get('game_over'):
194
+ raise ValueError("Game is already over.")
195
+ if 'placeholder_guess' not in active_game:
196
+ raise ValueError("No placeholder guess was made. Call make_placeholder_guess() first.")
197
+
198
+ # Get the placeholder guess
199
+ guess_location = active_game['placeholder_guess']
200
+
201
+ # Call Flask API to make the final guess
202
+ game_id = active_game['game_id']
203
+ guess_result = call_flask_api(f'/game/{game_id}/guess', 'POST', {
204
+ 'lat': guess_location['lat'],
205
+ 'lng': guess_location['lng']
206
+ })
207
+
208
+ # Mark game as over
209
+ active_game['game_over'] = True
210
+
211
+ return {
212
+ "distance_km": guess_result['distance_km'],
213
+ "score": guess_result['score'],
214
+ "actual_location": guess_result['actual_location'],
215
+ "guess_location": guess_result['guess_location']
216
+ }
217
+
218
+ @mcp.tool()
219
+ def get_game_state() -> dict:
220
+ """
221
+ Gets the current game state from the Flask API.
222
+ Returns:
223
+ dict: Current game state including moves, actions, and game status.
224
+ """
225
+ global active_game
226
+ if not active_game or not active_game.get('game_id'):
227
+ raise ValueError("Game not started.")
228
+
229
+ game_id = active_game['game_id']
230
+ game_state = call_flask_api(f'/game/{game_id}/state')
231
+
232
+ # Don't expose the actual coordinates - keep the guessing challenge
233
+ state_info = {
234
+ "game_id": game_state.get('game_id', game_id),
235
+ "player_name": game_state.get('player_name', active_game.get('player_name')),
236
+ "moves": game_state.get('moves', 0),
237
+ "game_over": game_state.get('game_over', False),
238
+ "total_actions": len(game_state.get('actions', [])),
239
+ "guesses_made": len(game_state.get('guesses', [])),
240
+ "placeholder_guess": active_game.get('placeholder_guess')
241
+ }
242
+
243
+ # Update local game state
244
+ active_game['game_over'] = game_state.get('game_over', False)
245
+
246
+ return state_info
247
+
248
+ @mcp.tool()
249
+ def test_connection() -> str:
250
+ """
251
+ Simple test to verify MCP server is working.
252
+ Returns:
253
+ str: A test message.
254
+ """
255
+ return "MCP server is working correctly!"
256
+
257
+ @mcp.get("/health")
258
+ def health_check():
259
+ """Health check endpoint for Docker"""
260
+ return {"status": "healthy", "flask_api_url": FLASK_API_URL}
261
+
262
+ @mcp.tool()
263
+ def check_flask_api() -> dict:
264
+ """
265
+ Checks if the Flask API is available and responding.
266
+ Returns:
267
+ dict: Status of the Flask API connection.
268
+ """
269
+ try:
270
+ response = requests.get(f"{FLASK_API_URL}/", timeout=5)
271
+ if response.status_code == 200:
272
+ return {
273
+ "status": "connected",
274
+ "message": f"Flask API is available at {FLASK_API_URL}",
275
+ "flask_status_code": response.status_code
276
+ }
277
+ else:
278
+ return {
279
+ "status": "error",
280
+ "message": f"Flask API returned status code {response.status_code}",
281
+ "flask_status_code": response.status_code
282
+ }
283
+ except requests.exceptions.ConnectionError:
284
+ return {
285
+ "status": "disconnected",
286
+ "message": f"Cannot connect to Flask API at {FLASK_API_URL}. Make sure app.py is running.",
287
+ "flask_status_code": None
288
+ }
289
+ except Exception as e:
290
+ return {
291
+ "status": "error",
292
+ "message": f"Error checking Flask API: {str(e)}",
293
+ "flask_status_code": None
294
+ }
295
+
296
+ # --- Server Execution ---
297
+ if __name__ == "__main__":
298
+ print(f"GeoGuessr MCP Server starting...")
299
+ print(f"Flask API URL: {FLASK_API_URL}")
300
+ print("Make sure the Flask server (app.py) is running before using this MCP server.")
301
+ print("Running GeoGuessr MCP server with SSE transport")
302
+ mcp.run(transport="sse")