File size: 9,282 Bytes
42bd784
 
 
 
 
1978788
42bd784
1978788
42bd784
 
 
1978788
42bd784
 
 
 
 
 
1978788
42bd784
 
 
1978788
42bd784
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1978788
42bd784
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1978788
42bd784
1978788
42bd784
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f96dc9d
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
import os
import requests
import base64
from dotenv import load_dotenv
from mcp.server.fastmcp import FastMCP, Image

load_dotenv()

# --- Configuration ---
# URL of the Flask app API - configurable via environment variable
FLASK_API_URL = os.getenv("FLASK_API_URL", "http://127.0.0.1:5000")

# --- MCP Server Setup ---
mcp = FastMCP(
    name="GeoGuessrAgent",
    host="0.0.0.0",
    port=7860,
)

# --- Game State Management ---
# Store the current game ID and basic state
active_game = {}

# --- Flask API Helper Functions ---
def call_flask_api(endpoint, method='GET', json_data=None):
    """Helper function to call Flask API endpoints"""
    url = f"{FLASK_API_URL}{endpoint}"
    try:
        if method == 'POST':
            response = requests.post(url, json=json_data, headers={'Content-Type': 'application/json'}, timeout=30)
        else:
            response = requests.get(url, timeout=30)
        
        if response.status_code in [200, 201]:
            return response.json()
        else:
            error_msg = f"API call failed: {response.status_code} - {response.text}"
            print(f"Flask API Error: {error_msg}")
            raise Exception(error_msg)
    except requests.exceptions.ConnectionError as e:
        error_msg = f"Could not connect to Flask API at {FLASK_API_URL}. Make sure the Flask server is running. Error: {str(e)}"
        print(f"Connection Error: {error_msg}")
        raise Exception(error_msg)
    except requests.exceptions.Timeout as e:
        error_msg = f"Timeout calling Flask API at {FLASK_API_URL}. Error: {str(e)}"
        print(f"Timeout Error: {error_msg}")
        raise Exception(error_msg)
    except Exception as e:
        error_msg = f"API call error: {str(e)}"
        print(f"General Error: {error_msg}")
        raise Exception(error_msg)

def base64_to_image_bytes(base64_string):
    """Convert base64 string to image bytes"""
    try:
        if not base64_string:
            raise ValueError("Empty base64 string provided")
        
        # Remove any data URL prefix if present
        if base64_string.startswith('data:'):
            base64_string = base64_string.split(',', 1)[1]
        
        # Decode the base64 string
        image_bytes = base64.b64decode(base64_string)
        
        if len(image_bytes) == 0:
            raise ValueError("Decoded image is empty")
        
        print(f"Successfully decoded image: {len(image_bytes)} bytes")
        return image_bytes
        
    except Exception as e:
        print(f"Error decoding base64 image: {e}")
        raise ValueError(f"Failed to decode base64 image: {str(e)}")

# --- MCP Tools ---

@mcp.tool()
def start_game(difficulty: str = "easy", player_name: str = "MCP Agent") -> Image:
    """
    Starts a new GeoGuessr game by calling the Flask API.
    Args:
        difficulty (str): The difficulty of the game ('easy', 'medium', 'hard').
        player_name (str): The name of the player/agent.
    Returns:
        Image: The first Street View image with compass overlay.
    """
    global active_game
    
    # Call Flask API to start game
    game_data = call_flask_api('/start_game', 'POST', {
        'difficulty': difficulty,
        'player_name': player_name
    })
    
    # Store game state
    active_game = {
        'game_id': game_data['game_id'],
        'player_name': game_data['player_name'],
        'game_over': False
    }
    
    # Convert base64 image to bytes and return as Image
    if game_data.get('streetview_image'):
        try:
            image_bytes = base64_to_image_bytes(game_data['streetview_image'])
            print(f"Successfully started game {active_game['game_id']} for player {active_game['player_name']}")
            return Image(data=image_bytes, format="jpeg")
        except Exception as e:
            print(f"Error processing Street View image: {e}")
            raise Exception(f"Failed to process Street View image: {str(e)}")
    else:
        raise Exception("No Street View image received from the game")



@mcp.tool()
def move(direction: str = None, degree: float = None, distance: float = 0.1) -> Image:
    """
    Moves the player in a specified direction by calling the Flask API.
    Args:
        direction (str, optional): Direction to move (N, NE, E, SE, S, SW, W, NW).
        degree (float, optional): Precise direction in degrees (0-360).
        distance (float): Distance to move in kilometers (default: 0.1km = 100m).
    Returns:
        Image: The new Street View image with compass overlay.
    """
    global active_game
    if not active_game or not active_game.get('game_id'):
        raise ValueError("Game not started. Call start_game() first.")
    if active_game.get('game_over'):
        raise ValueError("Game is over.")
    
    
    # Prepare move data
    move_data = {'distance': distance}
    if direction:
        move_data['direction'] = direction
    elif degree is not None:
        move_data['degree'] = degree
    else:
        raise ValueError("Must provide either direction or degree parameter.")
    
    # Call Flask API to move
    game_id = active_game['game_id']
    move_result = call_flask_api(f'/game/{game_id}/move', 'POST', move_data)
    
    # Convert base64 image to bytes and return as Image
    if move_result.get('streetview_image'):
        try:
            image_bytes = base64_to_image_bytes(move_result['streetview_image'])
            direction_info = move_result.get('moved_direction', 'unknown direction')
            distance_info = move_result.get('distance_moved_km', 0) * 1000
            print(f"Successfully moved {direction_info} for {distance_info:.0f}m in game {game_id}")
            return Image(data=image_bytes, format="jpeg")
        except Exception as e:
            print(f"Error processing move Street View image: {e}")
            raise Exception(f"Failed to process move Street View image: {str(e)}")
    else:
        raise Exception("No Street View image received from the move")

@mcp.tool()
def make_placeholder_guess(lat: float, lng: float) -> dict:
    """
    Records a temporary guess for the location (stored locally until final guess).
    Args:
        lat (float): The latitude of the guess.
        lng (float): The longitude of the guess.
    Returns:
        dict: A status message.
    """
    global active_game
    if not active_game or not active_game.get('game_id'):
        raise ValueError("Game not started.")
    if active_game.get('game_over'):
        raise ValueError("Game is over.")

    active_game['placeholder_guess'] = {'lat': lat, 'lng': lng}
    return {"status": "success", "message": f"Placeholder guess recorded: {lat:.6f}, {lng:.6f}"}


@mcp.tool()
def make_final_guess() -> dict:
    """
    Makes the final guess for the active game by calling the Flask API.
    Uses the stored placeholder guess coordinates.
    Returns:
        dict: The results of the guess including distance, score, and actual location.
    """
    global active_game
    if not active_game or not active_game.get('game_id'):
        raise ValueError("Game not started.")
    if active_game.get('game_over'):
        raise ValueError("Game is already over.")
    if 'placeholder_guess' not in active_game:
        raise ValueError("No placeholder guess was made. Call make_placeholder_guess() first.")

    # Get the placeholder guess
    guess_location = active_game['placeholder_guess']
    
    # Call Flask API to make the final guess
    game_id = active_game['game_id']
    guess_result = call_flask_api(f'/game/{game_id}/guess', 'POST', {
        'lat': guess_location['lat'],
        'lng': guess_location['lng']
    })

    # Mark game as over
    active_game['game_over'] = True
    
    return {
        "distance_km": guess_result['distance_km'],
        "score": guess_result['score'],
        "actual_location": guess_result['actual_location'],
        "guess_location": guess_result['guess_location']
    }

@mcp.tool()
def get_game_state() -> dict:
    """
    Gets the current game state from the Flask API.
    Returns:
        dict: Current game state including moves, actions, and game status.
    """
    global active_game
    if not active_game or not active_game.get('game_id'):
        raise ValueError("Game not started.")
    
    game_id = active_game['game_id']
    game_state = call_flask_api(f'/game/{game_id}/state')
    
    # Don't expose the actual coordinates - keep the guessing challenge
    state_info = {
        "game_id": game_state.get('game_id', game_id),
        "player_name": game_state.get('player_name', active_game.get('player_name')),
        "moves": game_state.get('moves', 0),
        "game_over": game_state.get('game_over', False),
        "total_actions": len(game_state.get('actions', [])),
        "guesses_made": len(game_state.get('guesses', [])),
        "placeholder_guess": active_game.get('placeholder_guess')
    }
    
    # Update local game state
    active_game['game_over'] = game_state.get('game_over', False)
    
    return state_info

@mcp.tool()
def test_connection() -> str:
    """
    Simple test to verify MCP server is working.
    Returns:
        str: A test message.
    """
    return "MCP server is working correctly!"

if __name__ == "__main__":
    mcp.run(transport="sse")